inform-runtime 1.0.4
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 +7 -0
- data/LICENSE +623 -0
- data/README.md +185 -0
- data/Rakefile +65 -0
- data/config/database.yml +37 -0
- data/exe/inform.rb +6 -0
- data/game/config.yml +5 -0
- data/game/example.inf +76 -0
- data/game/example.rb +90 -0
- data/game/forms/example_form.rb +2 -0
- data/game/grammar/game_grammar.inf.rb +11 -0
- data/game/languages/english.rb +2 -0
- data/game/models/example_model.rb +2 -0
- data/game/modules/example_module.rb +9 -0
- data/game/rules/example_state.rb +2 -0
- data/game/scripts/example_script.rb +2 -0
- data/game/topics/example_topic.rb +2 -0
- data/game/verbs/game_verbs.rb +15 -0
- data/game/verbs/metaverbs.rb +2028 -0
- data/lib/runtime/articles.rb +138 -0
- data/lib/runtime/builtins.rb +359 -0
- data/lib/runtime/color.rb +145 -0
- data/lib/runtime/command.rb +470 -0
- data/lib/runtime/config.rb +48 -0
- data/lib/runtime/context.rb +78 -0
- data/lib/runtime/daemon.rb +266 -0
- data/lib/runtime/database.rb +500 -0
- data/lib/runtime/events.rb +771 -0
- data/lib/runtime/experimental/handler_dsl.rb +175 -0
- data/lib/runtime/game.rb +74 -0
- data/lib/runtime/game_loader.rb +132 -0
- data/lib/runtime/grammar_parser.rb +553 -0
- data/lib/runtime/helpers.rb +177 -0
- data/lib/runtime/history.rb +45 -0
- data/lib/runtime/inflector.rb +195 -0
- data/lib/runtime/io.rb +174 -0
- data/lib/runtime/kernel.rb +450 -0
- data/lib/runtime/library.rb +59 -0
- data/lib/runtime/library_loader.rb +135 -0
- data/lib/runtime/link.rb +158 -0
- data/lib/runtime/logging.rb +197 -0
- data/lib/runtime/mixins.rb +570 -0
- data/lib/runtime/module.rb +202 -0
- data/lib/runtime/object.rb +761 -0
- data/lib/runtime/options.rb +104 -0
- data/lib/runtime/persistence.rb +292 -0
- data/lib/runtime/plurals.rb +60 -0
- data/lib/runtime/prototype.rb +307 -0
- data/lib/runtime/publication.rb +92 -0
- data/lib/runtime/runtime.rb +321 -0
- data/lib/runtime/session.rb +202 -0
- data/lib/runtime/stdlib.rb +604 -0
- data/lib/runtime/subscription.rb +47 -0
- data/lib/runtime/tag.rb +287 -0
- data/lib/runtime/tree.rb +204 -0
- data/lib/runtime/version.rb +24 -0
- data/lib/runtime/world_tree.rb +69 -0
- data/lib/runtime.rb +35 -0
- metadata +199 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: false
|
|
3
|
+
|
|
4
|
+
# Copyright Nels Nelson 2008-2023 but freely usable (see license)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of the Inform Runtime.
|
|
7
|
+
#
|
|
8
|
+
# The Inform Runtime is free software: you can redistribute it and/or
|
|
9
|
+
# modify it under the terms of the GNU General Public License as published
|
|
10
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
# (at your option) any later version.
|
|
12
|
+
#
|
|
13
|
+
# The Inform Runtime is distributed in the hope that it will be useful,
|
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
# GNU General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU General Public License
|
|
19
|
+
# along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
require 'logger'
|
|
22
|
+
require 'optparse'
|
|
23
|
+
|
|
24
|
+
require_relative 'version'
|
|
25
|
+
|
|
26
|
+
# The Inform module
|
|
27
|
+
module Inform
|
|
28
|
+
# The Options module
|
|
29
|
+
module Options
|
|
30
|
+
# The ArgumentsParser
|
|
31
|
+
class ArgumentsParser
|
|
32
|
+
attr_reader :parser, :options
|
|
33
|
+
|
|
34
|
+
def initialize(args, option_parser = OptionParser.new, **params)
|
|
35
|
+
@parser = option_parser
|
|
36
|
+
@options = ::Inform::Runtime.default_config.dup
|
|
37
|
+
flags.each { |method_name| self.method(method_name).call }
|
|
38
|
+
@parser.parse!(args, **params)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def flags
|
|
42
|
+
@flags ||= %i[banner admin log_level help version]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def banner
|
|
46
|
+
@parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} [game_path] [options]"
|
|
47
|
+
@parser.separator ''
|
|
48
|
+
@parser.separator 'Arguments:'
|
|
49
|
+
@parser.separator ' game_path Path to game file or directory'
|
|
50
|
+
@parser.separator ''
|
|
51
|
+
@parser.separator 'Options:'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def admin
|
|
55
|
+
@parser.on_tail('--admin', 'Set player character as admin; default: false') do
|
|
56
|
+
@options[:admin] = true
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def log_level
|
|
61
|
+
@parser.on_tail('-v', '--verbose', 'Increase verbosity') do
|
|
62
|
+
@options[:log_level] ||= Logger::INFO
|
|
63
|
+
@options[:log_level] -= 1
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def help
|
|
68
|
+
@parser.on_tail('-?', '--help', 'Show this message') do
|
|
69
|
+
puts @parser
|
|
70
|
+
exit
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def version
|
|
75
|
+
@parser.on_tail('--version', 'Show version') do
|
|
76
|
+
puts "#{$PROGRAM_NAME} version #{Inform::VERSION}"
|
|
77
|
+
exit
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
# class ArgumentsParser
|
|
82
|
+
|
|
83
|
+
def demand(options, arg, positional = false)
|
|
84
|
+
return options[arg] unless options[arg].nil?
|
|
85
|
+
required_arg = positional ? "<#{arg}>" : "--#{arg.to_s.gsub(/_/, '-')}"
|
|
86
|
+
raise UserError, "Required argument: #{required_arg}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_arguments(args = ARGV, _file_path = ARGF)
|
|
90
|
+
arguments_parser = ArgumentsParser.new(args)
|
|
91
|
+
demand(arguments_parser.options, :game_path)
|
|
92
|
+
arguments_parser.options
|
|
93
|
+
rescue OptionParser::InvalidArgument, OptionParser::InvalidOption,
|
|
94
|
+
OptionParser::MissingArgument, OptionParser::NeedlessArgument => e
|
|
95
|
+
puts e.message
|
|
96
|
+
puts arguments_parser.parser
|
|
97
|
+
exit
|
|
98
|
+
rescue OptionParser::AmbiguousOption => e
|
|
99
|
+
abort e.message
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
# module Options
|
|
103
|
+
end
|
|
104
|
+
# module Inform
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: false
|
|
3
|
+
|
|
4
|
+
# Copyright Nels Nelson 2008-2023 but freely usable (see license)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of the Inform Runtime.
|
|
7
|
+
#
|
|
8
|
+
# The Inform Runtime is free software: you can redistribute it and/or
|
|
9
|
+
# modify it under the terms of the GNU General Public License as published
|
|
10
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
# (at your option) any later version.
|
|
12
|
+
#
|
|
13
|
+
# The Inform Runtime is distributed in the hope that it will be useful,
|
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
# GNU General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU General Public License
|
|
19
|
+
# along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
require 'etc'
|
|
22
|
+
require 'logger'
|
|
23
|
+
require 'uri'
|
|
24
|
+
require 'yaml'
|
|
25
|
+
|
|
26
|
+
require 'sequel'
|
|
27
|
+
|
|
28
|
+
require_relative 'database'
|
|
29
|
+
|
|
30
|
+
Sequel.extension :migration
|
|
31
|
+
Sequel.extension :connection_validator
|
|
32
|
+
|
|
33
|
+
# Re-open the Inform module to define a helper method
|
|
34
|
+
module Inform
|
|
35
|
+
def self.initialize_persistence_layer
|
|
36
|
+
log.debug "Inform.initialize_persistence_layer"
|
|
37
|
+
Inform::Persistence.init
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Simplistic in-memory cache
|
|
42
|
+
class EphemeralCache < Hash
|
|
43
|
+
def get(key)
|
|
44
|
+
self[key]
|
|
45
|
+
end
|
|
46
|
+
def set(key, value, _ttl)
|
|
47
|
+
self[key] = value
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The Persistence class
|
|
52
|
+
class Persistence
|
|
53
|
+
unless defined?(Persistence::GlobalCache)
|
|
54
|
+
GlobalCache = defined?(Java) ? java.util.concurrent.ConcurrentHashMap.new : EphemeralCache.new
|
|
55
|
+
end
|
|
56
|
+
Plugins = {
|
|
57
|
+
after_initialize: [],
|
|
58
|
+
caching: [Persistence::GlobalCache],
|
|
59
|
+
json_serializer: [],
|
|
60
|
+
xml_serializer: []
|
|
61
|
+
# tactical_eager_loading: [],
|
|
62
|
+
# eager_each: []
|
|
63
|
+
}.freeze
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The SequelLoggers module
|
|
67
|
+
module SequelLoggers
|
|
68
|
+
DatabaseLogging = Struct.new(:memo).new({})
|
|
69
|
+
|
|
70
|
+
def add_logger(database, logger)
|
|
71
|
+
database.loggers << logger unless database.loggers.include?(logger)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def delete_logger(database, logger)
|
|
75
|
+
database.loggers.delete(logger) if database.loggers.include?(logger)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def set_log_level(database, level)
|
|
79
|
+
DatabaseLogging.memo[:preserved_log_levels][database] = database.sql_log_level
|
|
80
|
+
database.sql_log_level = level unless level.nil?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def enable_query_logging
|
|
84
|
+
DatabaseLogging.memo[:logger] ||= Logger.new($stdout)
|
|
85
|
+
DatabaseLogging.memo[:preserved_log_levels] ||= {}
|
|
86
|
+
logger = DatabaseLogging.memo[:logger]
|
|
87
|
+
Sequel::DATABASES.each do |database|
|
|
88
|
+
add_logger(database, logger)
|
|
89
|
+
set_log_level(database, :debug)
|
|
90
|
+
logger.debug "Enabled query logging for #{database}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def disable_query_logging
|
|
95
|
+
logger = DatabaseLogging.memo[:logger]
|
|
96
|
+
Sequel::DATABASES.each do |database|
|
|
97
|
+
logger.debug "Disabling query logging for #{database}"
|
|
98
|
+
delete_logger(database, logger)
|
|
99
|
+
set_log_level(database, DatabaseLogging.memo[:preserved_log_levels][database])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
# module SequelLoggers
|
|
104
|
+
|
|
105
|
+
# The SequelPlugins module
|
|
106
|
+
module SequelPlugins
|
|
107
|
+
def enable_plugins
|
|
108
|
+
Persistence::Plugins.each { |plugin, parameters| enable_plugin(plugin, parameters) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def enable_plugin(plugin, parameters = [])
|
|
112
|
+
return Sequel::Model.plugin(plugin) if parameters.empty?
|
|
113
|
+
Sequel::Model.plugin(plugin, *parameters)
|
|
114
|
+
rescue LoadError => e
|
|
115
|
+
log.error e.message
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
# module SequelPlugins
|
|
119
|
+
|
|
120
|
+
# The Inform module
|
|
121
|
+
module Inform
|
|
122
|
+
# The Persistence class
|
|
123
|
+
class Persistence
|
|
124
|
+
include DatabaseConnectionHelpers
|
|
125
|
+
include SequelLoggers
|
|
126
|
+
include SequelPlugins
|
|
127
|
+
|
|
128
|
+
attr_reader :config, :environment
|
|
129
|
+
|
|
130
|
+
def self.init(env = nil)
|
|
131
|
+
Inform::Persistence.instance(env)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@instance_mutex = Mutex.new
|
|
135
|
+
|
|
136
|
+
def self.instance(*args)
|
|
137
|
+
return @instance unless @instance.nil?
|
|
138
|
+
@instance_mutex.synchronize do
|
|
139
|
+
@instance ||= new(*args)
|
|
140
|
+
end
|
|
141
|
+
@instance
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private_class_method :new
|
|
145
|
+
|
|
146
|
+
def initialize(env = nil)
|
|
147
|
+
@environment = env || Inform::Game.environment
|
|
148
|
+
log.debug "Initializing persistence layer for environment: #{@environment}"
|
|
149
|
+
caller[0..4].each { |t| log.debug t }
|
|
150
|
+
@config = database_config.fetch(@environment.to_s, {})
|
|
151
|
+
establish_database_connection
|
|
152
|
+
enable_plugins
|
|
153
|
+
enable_query_logging if ENV['ENABLE_SQL_LOGGING']
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def establish_database_connection
|
|
157
|
+
connect
|
|
158
|
+
rescue Sequel::DatabaseConnectionError
|
|
159
|
+
log.warn "Database requires initialization"
|
|
160
|
+
init_database
|
|
161
|
+
retry if ConnectionAttempts.memo < 2
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def database_config
|
|
165
|
+
@database_config ||= YAML.load_file(database_config_file_path)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def database_config_file_path
|
|
169
|
+
@database_config_file_path ||= File.expand_path(
|
|
170
|
+
File.join(Inform::Runtime.project_dir_path, 'config', 'database.yml')
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def init_database
|
|
175
|
+
@config = database_config.fetch('default')
|
|
176
|
+
connect
|
|
177
|
+
@database = Inform::Database.instance(Inform::Game.config[:database_name])
|
|
178
|
+
@database.bootstrap.up
|
|
179
|
+
@config = database_config.fetch(environment.to_s)
|
|
180
|
+
connect
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
# class Persistence
|
|
184
|
+
end
|
|
185
|
+
# module Inform
|
|
186
|
+
|
|
187
|
+
# The Inform module
|
|
188
|
+
module Inform
|
|
189
|
+
# The ImplicitMigration module
|
|
190
|
+
module ImplicitMigration
|
|
191
|
+
NoDatabasePattern = %r{No database associated with Sequel::Model}.freeze
|
|
192
|
+
MigrationSetupTemplate = '%<model>sSetup'.freeze
|
|
193
|
+
ModuleNamespaceDelimiterPattern = /::/.freeze
|
|
194
|
+
|
|
195
|
+
def before_inherited(subclass)
|
|
196
|
+
return if self != Sequel::Model
|
|
197
|
+
descendants << subclass
|
|
198
|
+
log.debug "#{subclass} << #{self} [#{descendants}]"
|
|
199
|
+
maybe_migrate(subclass)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def after_inherited(_subclass)
|
|
203
|
+
examine_schema
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# rubocop: disable Metrics/AbcSize
|
|
207
|
+
# rubocop: disable Metrics/MethodLength
|
|
208
|
+
def examine_schema
|
|
209
|
+
descendants.each do |model_class|
|
|
210
|
+
table_name = model_class.table_name
|
|
211
|
+
indexes = self.db.indexes(table_name)
|
|
212
|
+
columns = model_class.columns
|
|
213
|
+
associations = model_class.associations
|
|
214
|
+
|
|
215
|
+
log.debug "Table: #{table_name}"
|
|
216
|
+
log.debug "Columns: #{columns.join(', ')}"
|
|
217
|
+
log.debug "Indexes: #{indexes}"
|
|
218
|
+
|
|
219
|
+
associations.each do |assoc_name, assoc_data|
|
|
220
|
+
log.debug "Association: #{assoc_name} (#{assoc_data[:type]}) to #{assoc_data[:class_name]}"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
log.debug "==========="
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
# rubocop: enable Metrics/AbcSize
|
|
227
|
+
# rubocop: enable Metrics/MethodLength
|
|
228
|
+
|
|
229
|
+
def maybe_migrate(subclass)
|
|
230
|
+
migration_class = migration(subclass)
|
|
231
|
+
log.debug "Found migration class: #{migration_class}"
|
|
232
|
+
migration_class&.up
|
|
233
|
+
rescue Sequel::Error => e
|
|
234
|
+
if NoDatabasePattern.match?(e.message)
|
|
235
|
+
Inform::Persistence.instance.connect
|
|
236
|
+
retry
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def migration(model)
|
|
241
|
+
names = format(MigrationSetupTemplate, model: model).split(ModuleNamespaceDelimiterPattern)
|
|
242
|
+
names.inject(Object) do |mod, class_name|
|
|
243
|
+
mod.const_get(class_name)
|
|
244
|
+
rescue StandardError => e
|
|
245
|
+
log.warn "Error getting reference to migration model class: #{e.message}"
|
|
246
|
+
next
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
# module ImplicitMigration
|
|
251
|
+
end
|
|
252
|
+
# module Inform
|
|
253
|
+
|
|
254
|
+
# The Inform module
|
|
255
|
+
module Inform
|
|
256
|
+
# The InheritanceListener module
|
|
257
|
+
module InheritanceListener
|
|
258
|
+
# The ClassMethods module
|
|
259
|
+
module ClassMethods
|
|
260
|
+
include Inform::ImplicitMigration
|
|
261
|
+
|
|
262
|
+
def inherited(subclass)
|
|
263
|
+
before_inherited(subclass)
|
|
264
|
+
super
|
|
265
|
+
after_inherited(subclass)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Returns the list of Model descendants.
|
|
269
|
+
def descendants
|
|
270
|
+
@descendants ||= []
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def self.included(base)
|
|
275
|
+
base.extend(ClassMethods)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
# module InheritanceListener
|
|
279
|
+
end
|
|
280
|
+
# module Inform
|
|
281
|
+
|
|
282
|
+
# The Sequel module
|
|
283
|
+
module Sequel
|
|
284
|
+
# The Sequel::Model class
|
|
285
|
+
class Model
|
|
286
|
+
include Inform::InheritanceListener
|
|
287
|
+
|
|
288
|
+
def self.implicit_table_name
|
|
289
|
+
underscore(demodulize(name)).to_sym
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: false
|
|
3
|
+
|
|
4
|
+
# Copyright Nels Nelson 2008-2023 but freely usable (see license)
|
|
5
|
+
#
|
|
6
|
+
# This file is part of the Inform Runtime.
|
|
7
|
+
#
|
|
8
|
+
# The Inform Runtime is free software: you can redistribute it and/or
|
|
9
|
+
# modify it under the terms of the GNU General Public License as published
|
|
10
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
11
|
+
# (at your option) any later version.
|
|
12
|
+
#
|
|
13
|
+
# The Inform Runtime is distributed in the hope that it will be useful,
|
|
14
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
# GNU General Public License for more details.
|
|
17
|
+
#
|
|
18
|
+
# You should have received a copy of the GNU General Public License
|
|
19
|
+
# along with the Inform Runtime. If not, see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
require_relative 'inflector'
|
|
22
|
+
|
|
23
|
+
# The Inform module
|
|
24
|
+
module Inform
|
|
25
|
+
# The Plurals module
|
|
26
|
+
module Plurals
|
|
27
|
+
# Returns the plural form of the word in the string.
|
|
28
|
+
def pluralize(word, n = nil)
|
|
29
|
+
return word if n == 1
|
|
30
|
+
apply_inflections(word, Inform::Inflector.inflections.plurals)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The reverse of +pluralize+, returns the singular form of a word in a
|
|
34
|
+
# string.
|
|
35
|
+
def singularize(word)
|
|
36
|
+
apply_inflections(word, Inform::Inflector.inflections.singulars)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def singular?(s)
|
|
40
|
+
singularize(s) == s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def plural?(s)
|
|
44
|
+
pluralize(s) == s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Applies inflection rules for +singularize+ and +pluralize+.
|
|
48
|
+
def apply_inflections(word, rules)
|
|
49
|
+
return "" if word.empty?
|
|
50
|
+
result = word.to_s.dup
|
|
51
|
+
return result if Inform::Inflector.inflections.uncountables.include?(result.downcase[/\b\w+\Z/])
|
|
52
|
+
for (rule, replacement) in rules
|
|
53
|
+
break if result.sub!(rule, replacement)
|
|
54
|
+
end
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
# module Plurals
|
|
59
|
+
end
|
|
60
|
+
# module Inform
|