slick 0.16.3 → 0.17.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +7 -8
- data/.slick +0 -0
- data/.travis.yml +3 -5
- data/LICENSE +20 -0
- data/README.md +3 -32
- data/Rakefile +1 -0
- data/config.rb +8 -0
- data/config.ru +4 -0
- data/exe/slick +8 -0
- data/lib/slick.rb +50 -3
- data/lib/slick/command.rb +29 -0
- data/lib/slick/commands/create_database.rb +9 -0
- data/lib/slick/commands/drop_database.rb +9 -0
- data/lib/slick/commands/init_database.rb +9 -0
- data/lib/slick/commands/list_commands.rb +14 -0
- data/lib/slick/commands/migrate_database.rb +9 -0
- data/lib/slick/commands/reset_database.rb +10 -0
- data/lib/slick/commands/start_console.rb +14 -0
- data/lib/slick/commands/start_server.rb +14 -0
- data/lib/slick/concern.rb +19 -0
- data/lib/slick/database.rb +168 -0
- data/lib/slick/database/abstract_row.rb +13 -0
- data/lib/slick/database/column.rb +64 -0
- data/lib/slick/database/migration.rb +36 -0
- data/lib/slick/database/migrator.rb +67 -0
- data/lib/slick/database/row.rb +127 -0
- data/lib/slick/database/table.rb +381 -0
- data/lib/slick/database/union.rb +86 -0
- data/lib/slick/helper.rb +24 -0
- data/lib/slick/helpers.rb +4 -0
- data/lib/slick/helpers/dir.rb +6 -0
- data/lib/slick/helpers/echo.rb +6 -0
- data/lib/slick/helpers/grab.rb +10 -0
- data/lib/slick/helpers/html_builder.rb +180 -0
- data/lib/slick/helpers/text_builder.rb +22 -0
- data/lib/slick/monkey_patches/html_escape.rb +8 -0
- data/lib/slick/monkey_patches/html_unescape.rb +8 -0
- data/lib/slick/monkey_patches/paramify.rb +11 -0
- data/lib/slick/monkey_patches/pluralize.rb +8 -0
- data/lib/slick/monkey_patches/require_all.rb +18 -0
- data/lib/slick/monkey_patches/singularize.rb +8 -0
- data/lib/slick/monkey_patches/url_encode.rb +8 -0
- data/lib/slick/monkey_patches/url_escape.rb +8 -0
- data/lib/slick/monkey_patches/url_unescape.rb +8 -0
- data/lib/slick/project.rb +53 -0
- data/lib/slick/project_watcher.rb +36 -0
- data/lib/slick/registry.rb +39 -0
- data/lib/slick/request.rb +6 -0
- data/lib/slick/resource_factories/config.rb +14 -0
- data/lib/slick/resource_factories/database.rb +6 -0
- data/lib/slick/resource_factories/database_schema_cache.rb +6 -0
- data/lib/slick/resource_factories/database_session_cache.rb +6 -0
- data/lib/slick/resource_factories/env.rb +14 -0
- data/lib/slick/resource_factories/environment.rb +6 -0
- data/lib/slick/resource_factories/file.rb +6 -0
- data/lib/slick/resource_factories/params.rb +6 -0
- data/lib/slick/resource_factories/project.rb +6 -0
- data/lib/slick/resource_factories/request.rb +6 -0
- data/lib/slick/resource_factories/response.rb +6 -0
- data/lib/slick/resource_factories/web.rb +11 -0
- data/lib/slick/resource_factory.rb +35 -0
- data/lib/slick/resource_provider.rb +51 -0
- data/lib/slick/response.rb +14 -0
- data/lib/slick/util.rb +72 -0
- data/lib/slick/util/inflections.rb +122 -0
- data/lib/slick/util/params_hash.rb +38 -0
- data/lib/slick/version.rb +1 -1
- data/lib/slick/web.rb +4 -0
- data/lib/slick/web/dir.rb +92 -0
- data/lib/slick/web/file.rb +66 -0
- data/lib/slick/web/file_interpreter.rb +24 -0
- data/lib/slick/web/file_interpreters/erb.rb +33 -0
- data/lib/slick/web/file_interpreters/js.rb +17 -0
- data/lib/slick/web/file_interpreters/rb.rb +14 -0
- data/lib/slick/web/file_interpreters/sass.rb +30 -0
- data/lib/slick/web/node.rb +35 -0
- data/lib/slick/workspace.rb +8 -0
- data/slick.gemspec +15 -4
- data/web/_layout.erb +13 -0
- data/web/index.erb +7 -0
- data/web/javascripts/index.js +2 -0
- data/web/stylesheets/index.scss +2 -0
- metadata +127 -11
- data/Gemfile.lock +0 -22
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Slick::Project
|
4
|
+
|
5
|
+
attr_reader :root_path, :load_paths, :name, :lib_names
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@root_path = find_root_path(Dir.pwd)
|
9
|
+
@load_paths = $LOAD_PATH.map{|load_path| find_root_path(load_path) }.compact
|
10
|
+
if @root_path
|
11
|
+
@name = gem_name(@root_path)
|
12
|
+
@load_paths.unshift(@root_path) unless @load_paths.include?(@root_path)
|
13
|
+
end
|
14
|
+
|
15
|
+
@lib_names = @load_paths.map{|load_path| gem_name(load_path) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def exist?
|
19
|
+
!@name.nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def find_file_path(path, pattern)
|
25
|
+
current_path = path
|
26
|
+
while true
|
27
|
+
Dir.open(current_path).each do |item|
|
28
|
+
candidate_file_path = "#{current_path}/#{item}"
|
29
|
+
return candidate_file_path if File.file?(candidate_file_path) && item.match(pattern)
|
30
|
+
end
|
31
|
+
if matches = current_path.match(/\A(.*)\/([^\/]+)\z/)
|
32
|
+
current_path = matches[1]
|
33
|
+
current_path = '/' if current_path == ''
|
34
|
+
else
|
35
|
+
break
|
36
|
+
end
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_root_path(path)
|
42
|
+
out = find_file_path(path, /\A\.slick\z/)
|
43
|
+
out.sub!(/\.slick\z/, '').sub!(/\A(.+)\/\z/, '\1') if out
|
44
|
+
out
|
45
|
+
end
|
46
|
+
|
47
|
+
def gem_name(path)
|
48
|
+
out = find_file_path(path, /\A[^\/]+\.gemspec\z/)
|
49
|
+
out.sub!(/\A.*\//, '').sub!(/\.gemspec\z/, '\1') if out
|
50
|
+
out
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
require "slick/helpers"
|
3
|
+
|
4
|
+
class Slick::ProjectWatcher
|
5
|
+
|
6
|
+
def self.watch
|
7
|
+
self.new.watch
|
8
|
+
end
|
9
|
+
|
10
|
+
include Slick::Helpers
|
11
|
+
|
12
|
+
def watch
|
13
|
+
last_modified_times = {}
|
14
|
+
|
15
|
+
while true
|
16
|
+
is_change = false
|
17
|
+
|
18
|
+
Dir.glob("#{project.root_path}/**/*").each do |file|
|
19
|
+
last_modified_time = File.mtime(file)
|
20
|
+
if last_modified_times[file] != last_modified_time
|
21
|
+
is_change = true
|
22
|
+
end
|
23
|
+
last_modified_times[file] = last_modified_time
|
24
|
+
end
|
25
|
+
|
26
|
+
if is_change
|
27
|
+
Process.kill('KILL', @child_process_pid) if @child_process_pid
|
28
|
+
@child_process_pid = Process.fork
|
29
|
+
break if !@child_process_pid
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
module Slick::Registry
|
3
|
+
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def registered?
|
7
|
+
registered_classes.values.include? self
|
8
|
+
end
|
9
|
+
|
10
|
+
def registered_classes
|
11
|
+
@registered_classes ||= superclass.respond_to?(:registered_classes) ? superclass.registered_classes : {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def register(name, *args, &block)
|
15
|
+
raise "Class has already been registered" if registered?
|
16
|
+
name = name.to_s
|
17
|
+
instance_eval{ @name = name }
|
18
|
+
registered_classes[name] = self
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def for(name, options = {})
|
23
|
+
name = name.to_s
|
24
|
+
registered_classes[name] || begin
|
25
|
+
klass = Class.new(self)
|
26
|
+
if options.paramify.register_if_not_exists == 'true'
|
27
|
+
klass.register(name, options)
|
28
|
+
else
|
29
|
+
klass.instance_eval{ @name = name }
|
30
|
+
end
|
31
|
+
klass
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def create(name, *args, &block)
|
36
|
+
self.for(name).new(*args, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
require "slick/resource_factory"
|
3
|
+
|
4
|
+
Class.new(Slick::ResourceFactory){ register "config", :shared => true }.define_method "create" do
|
5
|
+
if project.exist?
|
6
|
+
config_file_path = "#{project.root_path}/config.rb"
|
7
|
+
Slick::Workspace.new.instance_eval(
|
8
|
+
File.read(config_file_path),
|
9
|
+
config_file_path
|
10
|
+
).paramify
|
11
|
+
else
|
12
|
+
{}.paramify
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
require "slick/resource_factory"
|
3
|
+
|
4
|
+
Class.new(Slick::ResourceFactory){ register "env" }.define_method "create" do
|
5
|
+
{
|
6
|
+
"REQUEST_METHOD" => "GET",
|
7
|
+
"SCRIPT_NAME" => "",
|
8
|
+
"PATH_INFO" => "/",
|
9
|
+
"QUERY_STRING" => "",
|
10
|
+
"SERVER_NAME" => "localhost",
|
11
|
+
"SERVER_PORT" => "80",
|
12
|
+
"rack.input" => ""
|
13
|
+
}
|
14
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
require "slick/resource_factory"
|
3
|
+
|
4
|
+
Class.new(Slick::ResourceFactory){ register "web", :shared => true }.define_method "create" do
|
5
|
+
out = Slick::Web::Dir.new
|
6
|
+
project.load_paths.reverse.each do |load_path|
|
7
|
+
web_dir_path = "#{load_path}/web"
|
8
|
+
out.merge_dir(web_dir_path) if File.directory?(web_dir_path)
|
9
|
+
end
|
10
|
+
out
|
11
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
require "slick/registry"
|
3
|
+
require "slick/helper"
|
4
|
+
require "slick/helpers"
|
5
|
+
|
6
|
+
|
7
|
+
class Slick::ResourceFactory
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
include Slick::Registry
|
12
|
+
|
13
|
+
def register(name, options = {})
|
14
|
+
super(name)
|
15
|
+
|
16
|
+
@shared = options.paramify.shared == "true"
|
17
|
+
|
18
|
+
Class.new(Slick::Helper){ register name }.define_method "call" do
|
19
|
+
::Slick.resource_provider[self.class.name]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def shared?
|
24
|
+
@shared
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
include Slick::Helpers
|
30
|
+
|
31
|
+
def create
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class Slick::ResourceProvider
|
4
|
+
|
5
|
+
attr_reader :shared_resources
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@shared_resources = {}
|
9
|
+
@mutex = Mutex.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def thread_resources
|
13
|
+
Thread.current["SLICK_RESOURCES"] ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](name)
|
17
|
+
name = name.to_s
|
18
|
+
|
19
|
+
return thread_resources[name] if !thread_resources[name].nil?
|
20
|
+
return shared_resources[name] if !shared_resources[name].nil?
|
21
|
+
|
22
|
+
resource_factory = Slick::ResourceFactory.create(name)
|
23
|
+
|
24
|
+
if resource_factory.class.shared?
|
25
|
+
@mutex.synchronize { shared_resources[name] ||= resource_factory.create }
|
26
|
+
shared_resources[name]
|
27
|
+
else
|
28
|
+
thread_resources[name] = resource_factory.create
|
29
|
+
thread_resources[name]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def []=(name, value)
|
34
|
+
thread_resources[name.to_s] = value
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset(shared = false, thread = true, &block)
|
39
|
+
previous_shared_resources = shared_resources
|
40
|
+
previous_thread_resources = thread_resources
|
41
|
+
@mutex.syncronize { @shared_resources = {} } if shared
|
42
|
+
Thread.current["SLICK_RESOURCES"] = {} if thread
|
43
|
+
if block
|
44
|
+
out = block.call
|
45
|
+
@mutex.synchronize { @shared_resources = previous_shared_resources } if shared
|
46
|
+
Thread.current["SLICK_RESOURCES"] = previous_thread_resources if thread
|
47
|
+
out
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/lib/slick/util.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
module Slick::Util
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def pluralize(word)
|
9
|
+
word = word.to_s
|
10
|
+
Inflections.pluralize_rules.each do |rule|
|
11
|
+
pattern, replacement = rule
|
12
|
+
return word.sub(pattern, replacement) if word.match?(pattern)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def singularize(word)
|
17
|
+
word = word.to_s
|
18
|
+
Inflections.singularize_rules.each do |rule|
|
19
|
+
pattern, replacement = rule
|
20
|
+
return word.sub(pattern, replacement) if word.match?(pattern)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def html_escape(stringable)
|
25
|
+
CGI::escapeHTML(stringable.to_s)
|
26
|
+
end
|
27
|
+
|
28
|
+
def html_unescape(stringable)
|
29
|
+
CGI::unescapeHTML(stringable.to_s)
|
30
|
+
end
|
31
|
+
|
32
|
+
def url_escape(stringable)
|
33
|
+
CGI::escape(stringable.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
def url_unescape(stringable)
|
37
|
+
CGI::unescape(stringable.to_s)
|
38
|
+
end
|
39
|
+
|
40
|
+
def paramify(hashable)
|
41
|
+
if hashable.kind_of?(Hash)
|
42
|
+
out = ParamsHash.new
|
43
|
+
hashable.each{|name, value| out[name] = paramify(value)}
|
44
|
+
out
|
45
|
+
elsif hashable.kind_of?(Array)
|
46
|
+
out = ParamsHash.new
|
47
|
+
hashable.each_with_index{|value, index| out[index] = paramify(value)}
|
48
|
+
out
|
49
|
+
else
|
50
|
+
return hashable
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def url_encode(hashable, path = [], out = [])
|
55
|
+
hashable = paramify(hashable) if !hashable.kind_of?(ParamsHash)
|
56
|
+
if hashable.kind_of?(ParamsHash)
|
57
|
+
hashable.each do |name, value|
|
58
|
+
path.push(name)
|
59
|
+
url_encode(value, path, out)
|
60
|
+
path.pop
|
61
|
+
end
|
62
|
+
return out.join('&') if path.count == 0
|
63
|
+
elsif path.count == 0
|
64
|
+
return url_escape(hashable)
|
65
|
+
else
|
66
|
+
out << "#{url_escape(path.first)}#{path[1..path.length].map{|name| "[#{url_escape(name)}]"}.join}=#{url_escape(hashable)}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
|
2
|
+
module Slick::Util::Inflections
|
3
|
+
|
4
|
+
instance_eval do
|
5
|
+
@pluralize_rules = []
|
6
|
+
@singularize_rules = []
|
7
|
+
end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
attr_accessor :pluralize_rules
|
12
|
+
attr_accessor :singularize_rules
|
13
|
+
|
14
|
+
def plural(*args)
|
15
|
+
pluralize_rules.prepend(args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def singular(*args)
|
19
|
+
singularize_rules.prepend(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def irregular(singular, plural)
|
23
|
+
s0 = singular[0]
|
24
|
+
srest = singular[1..-1]
|
25
|
+
|
26
|
+
p0 = plural[0]
|
27
|
+
prest = plural[1..-1]
|
28
|
+
|
29
|
+
if s0.upcase == p0.upcase
|
30
|
+
plural(/(#{s0})#{srest}$/i, '\1' + prest)
|
31
|
+
plural(/(#{p0})#{prest}$/i, '\1' + prest)
|
32
|
+
|
33
|
+
singular(/(#{s0})#{srest}$/i, '\1' + srest)
|
34
|
+
singular(/(#{p0})#{prest}$/i, '\1' + srest)
|
35
|
+
else
|
36
|
+
plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest)
|
37
|
+
plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest)
|
38
|
+
plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest)
|
39
|
+
plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest)
|
40
|
+
|
41
|
+
singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest)
|
42
|
+
singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest)
|
43
|
+
singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest)
|
44
|
+
singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def uncountable(singular_and_plural)
|
49
|
+
irregular(singular_and_plural, singular_and_plural)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
plural(/$/, "s")
|
55
|
+
plural(/s$/i, "s")
|
56
|
+
plural(/^(ax|test)is$/i, '\1es')
|
57
|
+
plural(/(octop|vir)us$/i, '\1i')
|
58
|
+
plural(/(octop|vir)i$/i, '\1i')
|
59
|
+
plural(/(alias|status)$/i, '\1es')
|
60
|
+
plural(/(bu)s$/i, '\1ses')
|
61
|
+
plural(/(buffal|tomat)o$/i, '\1oes')
|
62
|
+
plural(/([ti])um$/i, '\1a')
|
63
|
+
plural(/([ti])a$/i, '\1a')
|
64
|
+
plural(/sis$/i, "ses")
|
65
|
+
plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
|
66
|
+
plural(/(hive)$/i, '\1s')
|
67
|
+
plural(/([^aeiouy]|qu)y$/i, '\1ies')
|
68
|
+
plural(/(x|ch|ss|sh)$/i, '\1es')
|
69
|
+
plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
|
70
|
+
plural(/^(m|l)ouse$/i, '\1ice')
|
71
|
+
plural(/^(m|l)ice$/i, '\1ice')
|
72
|
+
plural(/^(ox)$/i, '\1en')
|
73
|
+
plural(/^(oxen)$/i, '\1')
|
74
|
+
plural(/(quiz)$/i, '\1zes')
|
75
|
+
|
76
|
+
singular(/s$/i, "")
|
77
|
+
singular(/(ss)$/i, '\1')
|
78
|
+
singular(/(n)ews$/i, '\1ews')
|
79
|
+
singular(/([ti])a$/i, '\1um')
|
80
|
+
singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
|
81
|
+
singular(/(^analy)(sis|ses)$/i, '\1sis')
|
82
|
+
singular(/([^f])ves$/i, '\1fe')
|
83
|
+
singular(/(hive)s$/i, '\1')
|
84
|
+
singular(/(tive)s$/i, '\1')
|
85
|
+
singular(/([lr])ves$/i, '\1f')
|
86
|
+
singular(/([^aeiouy]|qu)ies$/i, '\1y')
|
87
|
+
singular(/(s)eries$/i, '\1eries')
|
88
|
+
singular(/(m)ovies$/i, '\1ovie')
|
89
|
+
singular(/(x|ch|ss|sh)es$/i, '\1')
|
90
|
+
singular(/^(m|l)ice$/i, '\1ouse')
|
91
|
+
singular(/(bus)(es)?$/i, '\1')
|
92
|
+
singular(/(o)es$/i, '\1')
|
93
|
+
singular(/(shoe)s$/i, '\1')
|
94
|
+
singular(/(cris|test)(is|es)$/i, '\1is')
|
95
|
+
singular(/^(a)x[ie]s$/i, '\1xis')
|
96
|
+
singular(/(octop|vir)(us|i)$/i, '\1us')
|
97
|
+
singular(/(alias|status)(es)?$/i, '\1')
|
98
|
+
singular(/^(ox)en/i, '\1')
|
99
|
+
singular(/(vert|ind)ices$/i, '\1ex')
|
100
|
+
singular(/(matr)ices$/i, '\1ix')
|
101
|
+
singular(/(quiz)zes$/i, '\1')
|
102
|
+
singular(/(database)s$/i, '\1')
|
103
|
+
|
104
|
+
irregular("person", "people")
|
105
|
+
irregular("man", "men")
|
106
|
+
irregular("child", "children")
|
107
|
+
irregular("sex", "sexes")
|
108
|
+
irregular("move", "moves")
|
109
|
+
irregular("zombie", "zombies")
|
110
|
+
|
111
|
+
uncountable "equipment"
|
112
|
+
uncountable "information"
|
113
|
+
uncountable "rice"
|
114
|
+
uncountable "money"
|
115
|
+
uncountable "species"
|
116
|
+
uncountable "series"
|
117
|
+
uncountable "fish"
|
118
|
+
uncountable "sheep"
|
119
|
+
uncountable "jeans"
|
120
|
+
uncountable "police"
|
121
|
+
|
122
|
+
end
|