gamefic 2.4.0 → 3.0.0
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/.github/workflows/rspec.yml +41 -40
- data/.rspec-opal +2 -0
- data/.solargraph.yml +20 -3
- data/CHANGELOG.md +9 -0
- data/Rakefile +11 -1
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gamefic.gemspec +5 -2
- data/lib/gamefic/action.rb +52 -183
- data/lib/gamefic/active/cue.rb +25 -0
- data/lib/gamefic/active/epic.rb +68 -0
- data/lib/gamefic/active/messaging.rb +43 -0
- data/lib/gamefic/active/take.rb +69 -0
- data/lib/gamefic/active.rb +95 -192
- data/lib/gamefic/actor.rb +2 -0
- data/lib/gamefic/block.rb +28 -0
- data/lib/gamefic/command.rb +16 -6
- data/lib/gamefic/core_ext/array.rb +4 -4
- data/lib/gamefic/core_ext/string.rb +10 -5
- data/lib/gamefic/describable.rb +39 -65
- data/lib/gamefic/dispatcher.rb +63 -32
- data/lib/gamefic/entity.rb +44 -19
- data/lib/gamefic/logging.rb +32 -0
- data/lib/gamefic/messenger.rb +66 -0
- data/lib/gamefic/narrative.rb +104 -0
- data/lib/gamefic/node.rb +44 -53
- data/lib/gamefic/plot.rb +60 -93
- data/lib/gamefic/props/default.rb +41 -0
- data/lib/gamefic/props/multiple_choice.rb +65 -0
- data/lib/gamefic/props/pause.rb +11 -0
- data/lib/gamefic/props/yes_or_no.rb +21 -0
- data/lib/gamefic/props.rb +10 -0
- data/lib/gamefic/query/base.rb +45 -126
- data/lib/gamefic/query/general.rb +46 -0
- data/lib/gamefic/query/result.rb +20 -0
- data/lib/gamefic/query/scoped.rb +41 -0
- data/lib/gamefic/query/text.rb +30 -31
- data/lib/gamefic/query.rb +7 -15
- data/lib/gamefic/response.rb +118 -0
- data/lib/gamefic/rulebook/calls.rb +90 -0
- data/lib/gamefic/rulebook/events.rb +79 -0
- data/lib/gamefic/rulebook/hooks.rb +57 -0
- data/lib/gamefic/rulebook/scenes.rb +68 -0
- data/lib/gamefic/rulebook.rb +139 -0
- data/lib/gamefic/scanner.rb +103 -0
- data/lib/gamefic/scene/activity.rb +9 -17
- data/lib/gamefic/scene/conclusion.rb +6 -5
- data/lib/gamefic/scene/default.rb +88 -0
- data/lib/gamefic/scene/multiple_choice.rb +14 -69
- data/lib/gamefic/scene/pause.rb +9 -13
- data/lib/gamefic/scene/yes_or_no.rb +6 -46
- data/lib/gamefic/scene.rb +11 -7
- data/lib/gamefic/scope/base.rb +44 -0
- data/lib/gamefic/scope/children.rb +16 -0
- data/lib/gamefic/scope/family.rb +20 -0
- data/lib/gamefic/scope/myself.rb +13 -0
- data/lib/gamefic/scope/parent.rb +13 -0
- data/lib/gamefic/scope/siblings.rb +14 -0
- data/lib/gamefic/scope.rb +8 -0
- data/lib/gamefic/scriptable/actions.rb +156 -0
- data/lib/gamefic/scriptable/entities.rb +76 -0
- data/lib/gamefic/scriptable/events.rb +65 -0
- data/lib/gamefic/scriptable/proxy.rb +55 -0
- data/lib/gamefic/scriptable/queries.rb +73 -0
- data/lib/gamefic/scriptable/scenes.rb +162 -0
- data/lib/gamefic/scriptable.rb +167 -73
- data/lib/gamefic/snapshot.rb +36 -0
- data/lib/gamefic/stage.rb +51 -0
- data/lib/gamefic/subplot.rb +51 -79
- data/lib/gamefic/syntax/template.rb +67 -0
- data/lib/gamefic/syntax.rb +102 -83
- data/lib/gamefic/vault.rb +50 -0
- data/lib/gamefic/version.rb +1 -1
- data/lib/gamefic.rb +26 -15
- data/spec-opal/spec_helper.rb +24 -0
- metadata +91 -29
- data/lib/gamefic/element.rb +0 -46
- data/lib/gamefic/keywords.rb +0 -52
- data/lib/gamefic/messaging.rb +0 -43
- data/lib/gamefic/plot/darkroom.rb +0 -120
- data/lib/gamefic/plot/host.rb +0 -42
- data/lib/gamefic/plot/snapshot.rb +0 -27
- data/lib/gamefic/query/children.rb +0 -9
- data/lib/gamefic/query/descendants.rb +0 -15
- data/lib/gamefic/query/external.rb +0 -39
- data/lib/gamefic/query/family.rb +0 -18
- data/lib/gamefic/query/itself.rb +0 -13
- data/lib/gamefic/query/matches.rb +0 -75
- data/lib/gamefic/query/parent.rb +0 -9
- data/lib/gamefic/query/siblings.rb +0 -13
- data/lib/gamefic/query/tree.rb +0 -17
- data/lib/gamefic/scene/base.rb +0 -142
- data/lib/gamefic/scene/multiple_scene.rb +0 -29
- data/lib/gamefic/serialize.rb +0 -196
- data/lib/gamefic/world/callbacks.rb +0 -135
- data/lib/gamefic/world/commands.rb +0 -181
- data/lib/gamefic/world/entities.rb +0 -98
- data/lib/gamefic/world/playbook.rb +0 -233
- data/lib/gamefic/world/players.rb +0 -37
- data/lib/gamefic/world/scenes.rb +0 -228
- data/lib/gamefic/world.rb +0 -18
data/lib/gamefic/active.rb
CHANGED
@@ -1,137 +1,93 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'set'
|
4
|
+
require 'gamefic/active/cue'
|
5
|
+
require 'gamefic/active/epic'
|
6
|
+
require 'gamefic/active/messaging'
|
7
|
+
require 'gamefic/active/take'
|
4
8
|
|
5
9
|
module Gamefic
|
6
|
-
class NotConclusionError < RuntimeError; end
|
7
|
-
|
8
10
|
# The Active module gives entities the ability to perform actions and
|
9
11
|
# participate in scenes. The Actor class, for example, is an Entity
|
10
12
|
# subclass that includes this module.
|
11
13
|
#
|
12
14
|
module Active
|
13
|
-
|
14
|
-
|
15
|
-
# @return [Gamefic::Scene::Base]
|
16
|
-
attr_reader :scene
|
15
|
+
include Logging
|
16
|
+
include Messaging
|
17
17
|
|
18
|
-
# The
|
19
|
-
#
|
18
|
+
# The cue that will be used to create a scene at the beginning of the next
|
19
|
+
# turn.
|
20
20
|
#
|
21
|
-
# @return [
|
22
|
-
attr_reader :
|
23
|
-
|
24
|
-
attr_reader :next_options
|
21
|
+
# @return [Active::Cue, nil]
|
22
|
+
attr_reader :next_cue
|
25
23
|
|
26
|
-
#
|
27
|
-
|
28
|
-
|
29
|
-
|
24
|
+
# @return [Symbol, nil]
|
25
|
+
def next_scene
|
26
|
+
next_cue&.scene
|
27
|
+
end
|
30
28
|
|
31
|
-
# The
|
29
|
+
# The rulebooks that will be used to perform commands. Every plot and
|
30
|
+
# subplot has its own rulebook.
|
32
31
|
#
|
33
|
-
# @return [
|
34
|
-
|
32
|
+
# @return [Set<Gamefic::World::Rulebook>]
|
33
|
+
# def rulebooks
|
34
|
+
# @rulebooks ||= Set.new
|
35
|
+
# end
|
35
36
|
|
36
|
-
# The
|
37
|
+
# The narratives in which the entity is participating.
|
37
38
|
#
|
38
|
-
# @return [
|
39
|
-
def
|
40
|
-
@
|
41
|
-
end
|
42
|
-
|
43
|
-
def syntaxes
|
44
|
-
playbooks.flat_map(&:syntaxes)
|
39
|
+
# @return [Epic]
|
40
|
+
def epic
|
41
|
+
@epic ||= Epic.new
|
45
42
|
end
|
46
43
|
|
47
|
-
# An array of
|
44
|
+
# An array of commands waiting to be executed.
|
48
45
|
#
|
49
46
|
# @return [Array<String>]
|
50
47
|
def queue
|
51
48
|
@queue ||= []
|
52
49
|
end
|
53
50
|
|
54
|
-
# A hash of
|
51
|
+
# A hash of data that will be sent to the user. The output is typically
|
52
|
+
# sent after a scene has started and before the user is prompted for input.
|
55
53
|
#
|
56
|
-
# @return [Hash
|
57
|
-
def state
|
58
|
-
@state ||= {}
|
59
|
-
end
|
60
|
-
|
54
|
+
# @return [Hash]
|
61
55
|
def output
|
62
56
|
@output ||= {}
|
63
57
|
end
|
64
58
|
|
65
|
-
# Send a message to the entity.
|
66
|
-
# This method will automatically wrap the message in HTML paragraphs.
|
67
|
-
# To send a message without paragraph formatting, use #stream instead.
|
68
|
-
#
|
69
|
-
# @param message [String]
|
70
|
-
def tell(message)
|
71
|
-
if buffer_stack > 0
|
72
|
-
append_buffer format(message)
|
73
|
-
else
|
74
|
-
super
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# Send a message to the Character as raw text.
|
79
|
-
# Unlike #tell, this method will not wrap the message in HTML paragraphs.
|
80
|
-
#
|
81
|
-
# @param message [String]
|
82
|
-
def stream(message)
|
83
|
-
if buffer_stack > 0
|
84
|
-
append_buffer message
|
85
|
-
else
|
86
|
-
super
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
59
|
# Perform a command.
|
91
60
|
#
|
92
|
-
# The command's action will be executed immediately regardless of the
|
61
|
+
# The command's action will be executed immediately, regardless of the
|
93
62
|
# entity's state.
|
94
63
|
#
|
95
64
|
# @example Send a command as a string
|
96
65
|
# character.perform "take the key"
|
97
66
|
#
|
98
|
-
# @param command [String
|
99
|
-
# @return [
|
100
|
-
def perform(
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
else
|
105
|
-
dispatchers.push Dispatcher.dispatch(self, command.first.to_s)
|
106
|
-
proceed
|
107
|
-
dispatchers.pop
|
108
|
-
end
|
67
|
+
# @param command [String]
|
68
|
+
# @return [void]
|
69
|
+
def perform(command)
|
70
|
+
dispatchers.push Dispatcher.dispatch(self, command)
|
71
|
+
dispatchers.last.execute
|
72
|
+
dispatchers.pop
|
109
73
|
end
|
110
74
|
|
111
75
|
# Quietly perform a command.
|
112
76
|
# This method executes the command exactly as #perform does, except it
|
113
|
-
# buffers the resulting output instead of sending it to
|
77
|
+
# buffers the resulting output instead of sending it to messages.
|
114
78
|
#
|
115
|
-
# @param command [String
|
79
|
+
# @param command [String]
|
116
80
|
# @return [String] The output that resulted from performing the command.
|
117
|
-
def quietly(
|
118
|
-
|
119
|
-
STDERR.puts "#{caller[0]}: Passing a verb and arguments to #quietly is deprecated. Use #execute instead"
|
120
|
-
execute command.first, *command[1..-1], quietly: true
|
121
|
-
else
|
122
|
-
dispatchers.push Dispatcher.dispatch(self, command.first.to_s)
|
123
|
-
result = proceed quietly: true
|
124
|
-
dispatchers.pop
|
125
|
-
result
|
126
|
-
end
|
81
|
+
def quietly(command)
|
82
|
+
messenger.buffer { perform command }
|
127
83
|
end
|
128
84
|
|
129
85
|
# Perform an action.
|
130
86
|
# This is functionally identical to the `perform` method, except the
|
131
|
-
# action must be declared as a verb with a list of
|
87
|
+
# action must be declared as a verb with a list of arguments. Use
|
132
88
|
# `perform` if you need to parse a string as a command.
|
133
89
|
#
|
134
|
-
# The command will be executed immediately regardless of the entity's
|
90
|
+
# The command will be executed immediately, regardless of the entity's
|
135
91
|
# state.
|
136
92
|
#
|
137
93
|
# @example
|
@@ -139,11 +95,10 @@ module Gamefic
|
|
139
95
|
#
|
140
96
|
# @param verb [Symbol]
|
141
97
|
# @param params [Array]
|
142
|
-
# @params quietly [Boolean]
|
143
98
|
# @return [Gamefic::Action]
|
144
|
-
def execute(verb, *params
|
99
|
+
def execute(verb, *params)
|
145
100
|
dispatchers.push Dispatcher.dispatch_from_params(self, verb, params)
|
146
|
-
|
101
|
+
dispatchers.last.execute
|
147
102
|
dispatchers.pop
|
148
103
|
end
|
149
104
|
|
@@ -170,138 +125,86 @@ module Gamefic
|
|
170
125
|
# end
|
171
126
|
# end
|
172
127
|
#
|
173
|
-
# @
|
174
|
-
|
175
|
-
|
176
|
-
return if dispatchers.empty?
|
177
|
-
a = dispatchers.last.next
|
178
|
-
return if a.nil?
|
179
|
-
prepare_buffer quietly
|
180
|
-
a.execute
|
181
|
-
flush_buffer quietly
|
128
|
+
# @return [void]
|
129
|
+
def proceed
|
130
|
+
dispatchers.last&.proceed&.execute
|
182
131
|
end
|
183
132
|
|
184
|
-
#
|
185
|
-
# Use #prepare if you want to declare a scene to be started at the
|
186
|
-
# beginning of the next turn.
|
133
|
+
# Cue a scene to start in the next turn.
|
187
134
|
#
|
188
|
-
# @
|
189
|
-
# @param data [Hash] Additional scene data
|
190
|
-
def cue new_scene, **data
|
191
|
-
@next_scene = nil
|
192
|
-
if new_scene.nil?
|
193
|
-
@scene = nil
|
194
|
-
else
|
195
|
-
@scene = new_scene.new(self, **data)
|
196
|
-
@scene.start
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# Prepare a scene to be started for this character at the beginning of the
|
201
|
-
# next turn. As opposed to #cue, a prepared scene will not start until the
|
202
|
-
# current scene finishes.
|
135
|
+
# @raise [ArgumentError] if the scene is not valid
|
203
136
|
#
|
204
|
-
# @param
|
205
|
-
# @
|
206
|
-
|
207
|
-
|
208
|
-
@
|
209
|
-
end
|
210
|
-
|
211
|
-
# Return true if the character is expected to be in the specified scene on
|
212
|
-
# the next turn.
|
213
|
-
#
|
214
|
-
# @return [Boolean]
|
215
|
-
def will_cue? scene
|
216
|
-
(@scene.class == scene and @next_scene.nil?) || @next_scene == scene
|
217
|
-
end
|
137
|
+
# @param scene [Symbol]
|
138
|
+
# @param context [Hash] Extra data to pass to the scene's props
|
139
|
+
# @return [Cue]
|
140
|
+
def cue scene, **context
|
141
|
+
return @next_cue if @next_cue&.scene == scene && @next_cue&.context == context
|
218
142
|
|
219
|
-
|
220
|
-
# NotConclusionError if the scene is not a Scene::Conclusion.
|
221
|
-
#
|
222
|
-
# @param new_scene [Class<Scene::Base>]
|
223
|
-
# @oaram data [Hash] Additional scene data
|
224
|
-
def conclude new_scene, **data
|
225
|
-
raise NotConclusionError unless new_scene <= Scene::Conclusion
|
226
|
-
cue new_scene, **data
|
227
|
-
end
|
143
|
+
logger.debug "Overwriting existing cue `#{@next_cue.scene}` with `#{scene}`" if @next_cue
|
228
144
|
|
229
|
-
|
230
|
-
#
|
231
|
-
# @return [Boolean]
|
232
|
-
def concluded?
|
233
|
-
!scene.nil? && scene.kind_of?(Scene::Conclusion)
|
145
|
+
@next_cue = Cue.new(scene, **context)
|
234
146
|
end
|
147
|
+
alias prepare cue
|
235
148
|
|
236
|
-
def
|
237
|
-
|
149
|
+
def start_take
|
150
|
+
ensure_cue
|
151
|
+
@last_cue = @next_cue
|
152
|
+
cue :default_scene
|
153
|
+
@props = Take.start(self, @last_cue)
|
238
154
|
end
|
239
155
|
|
240
|
-
def
|
241
|
-
|
242
|
-
end
|
156
|
+
def finish_take
|
157
|
+
return unless @last_cue
|
243
158
|
|
244
|
-
|
245
|
-
#
|
246
|
-
def entered scene
|
247
|
-
klass = (scene.is_a?(Gamefic::Scene::Base) ? scene.class : scene)
|
248
|
-
entered_scenes.add klass
|
159
|
+
Take.finish(self, @last_cue, @props)
|
249
160
|
end
|
250
161
|
|
251
|
-
#
|
162
|
+
# Restart the scene from the most recent cue.
|
252
163
|
#
|
253
|
-
# @return [
|
254
|
-
def
|
255
|
-
|
256
|
-
entered_scenes.include?(klass)
|
257
|
-
end
|
258
|
-
|
259
|
-
private
|
164
|
+
# @return [Cue, nil]
|
165
|
+
def recue
|
166
|
+
logger.warn "No scene to recue" unless @last_cue
|
260
167
|
|
261
|
-
|
262
|
-
if quietly
|
263
|
-
if buffer_stack == 0
|
264
|
-
@buffer = ""
|
265
|
-
end
|
266
|
-
set_buffer_stack(buffer_stack + 1)
|
267
|
-
end
|
168
|
+
@next_cue = @last_cue
|
268
169
|
end
|
269
170
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
171
|
+
# Cue a conclusion. This method works like #cue, except it will raise an
|
172
|
+
# error if the scene is not a conclusion.
|
173
|
+
#
|
174
|
+
# @raise [ArgumentError] if the requested scene is not a conclusion
|
175
|
+
#
|
176
|
+
# @param new_scene [Symbol]
|
177
|
+
# @oaram context [Hash] Additional scene data
|
178
|
+
def conclude scene, **context
|
179
|
+
cue scene, **context
|
180
|
+
available = epic.select_scene(scene)
|
181
|
+
raise ArgumentError, "`#{scene}` is not a conclusion" unless available.conclusion?
|
276
182
|
|
277
|
-
|
278
|
-
def entered_scenes
|
279
|
-
@entered_scenes ||= Set.new
|
183
|
+
@next_cue
|
280
184
|
end
|
281
185
|
|
282
|
-
|
283
|
-
|
186
|
+
# True if the actor is ready to leave the game.
|
187
|
+
#
|
188
|
+
def concluding?
|
189
|
+
epic.empty? || (@last_cue && epic.conclusion?(@last_cue.scene))
|
284
190
|
end
|
285
191
|
|
286
|
-
def
|
287
|
-
|
192
|
+
def accessible?
|
193
|
+
false
|
288
194
|
end
|
289
195
|
|
290
|
-
|
291
|
-
def buffer
|
292
|
-
@buffer ||= ''
|
293
|
-
end
|
196
|
+
private
|
294
197
|
|
295
|
-
|
296
|
-
|
198
|
+
# @return [Array<Dispatcher>]
|
199
|
+
def dispatchers
|
200
|
+
@dispatchers ||= []
|
297
201
|
end
|
298
202
|
|
299
|
-
def
|
300
|
-
|
301
|
-
end
|
203
|
+
def ensure_cue
|
204
|
+
return if next_cue
|
302
205
|
|
303
|
-
|
304
|
-
|
206
|
+
logger.debug "Using default scene for actor without cue"
|
207
|
+
cue :default_scene
|
305
208
|
end
|
306
209
|
end
|
307
210
|
end
|
data/lib/gamefic/actor.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gamefic
|
4
|
+
# A code container for seeds and scripts.
|
5
|
+
#
|
6
|
+
class Block
|
7
|
+
# @return [Symbol]
|
8
|
+
attr_reader :type
|
9
|
+
|
10
|
+
# @return [Proc]
|
11
|
+
attr_reader :code
|
12
|
+
|
13
|
+
# @param type [Symbol]
|
14
|
+
# @param code [Proc]
|
15
|
+
def initialize type, code
|
16
|
+
@type = type
|
17
|
+
@code = code
|
18
|
+
end
|
19
|
+
|
20
|
+
def script?
|
21
|
+
type == :script
|
22
|
+
end
|
23
|
+
|
24
|
+
def seed?
|
25
|
+
type == :seed
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/gamefic/command.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Gamefic
|
2
|
-
# A
|
3
|
-
#
|
4
|
+
# A decomposition of a text-based command into its verb and arguments.
|
5
|
+
#
|
6
|
+
# Commands are typically derived from tokenization against syntaxes.
|
4
7
|
#
|
5
8
|
class Command
|
6
|
-
# A Symbol representing the command's verb or verbal phrase.
|
7
|
-
#
|
8
9
|
# @return [Symbol]
|
9
10
|
attr_reader :verb
|
10
11
|
|
11
|
-
# An Array of arguments to be mapped to an Action's Queries.
|
12
|
-
#
|
13
12
|
# @return [Array<String>]
|
14
13
|
attr_reader :arguments
|
15
14
|
|
@@ -17,5 +16,16 @@ module Gamefic
|
|
17
16
|
@verb = verb
|
18
17
|
@arguments = arguments
|
19
18
|
end
|
19
|
+
|
20
|
+
# Compare two syntaxes for the purpose of ordering them by relevance while
|
21
|
+
# dispatching.
|
22
|
+
#
|
23
|
+
def compare other
|
24
|
+
if verb == other.verb
|
25
|
+
other.arguments.compact.length <=> arguments.compact.length
|
26
|
+
else
|
27
|
+
(other.verb ? 1 : 0) <=> (verb ? 1 : 0)
|
28
|
+
end
|
29
|
+
end
|
20
30
|
end
|
21
31
|
end
|
@@ -14,7 +14,7 @@ class Array
|
|
14
14
|
#
|
15
15
|
# @return [Array]
|
16
16
|
def that_are(*cls)
|
17
|
-
result =
|
17
|
+
result = dup
|
18
18
|
cls.each do |c|
|
19
19
|
_keep result, c, true
|
20
20
|
end
|
@@ -26,7 +26,7 @@ class Array
|
|
26
26
|
#
|
27
27
|
# @return [Array]
|
28
28
|
def that_are_not(*cls)
|
29
|
-
result =
|
29
|
+
result = dup
|
30
30
|
cls.each do |c|
|
31
31
|
_keep result, c, false
|
32
32
|
end
|
@@ -72,8 +72,8 @@ class Array
|
|
72
72
|
case cls
|
73
73
|
when Class, Module
|
74
74
|
arr.keep_if { |i| i.is_a?(cls) == bool }
|
75
|
-
when
|
76
|
-
arr.keep_if { |i|
|
75
|
+
when Proc
|
76
|
+
arr.keep_if { |i| !!cls.call(i) == bool }
|
77
77
|
else
|
78
78
|
arr.keep_if { |i| (i == cls) == bool }
|
79
79
|
end
|
@@ -1,19 +1,24 @@
|
|
1
|
-
|
2
|
-
include Gamefic::Keywords
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
3
|
+
class String
|
4
4
|
# Capitalize the first letter without changing the rest of the string.
|
5
5
|
# (String#capitalize makes the rest of the string lower-case.)
|
6
6
|
#
|
7
7
|
# @return [String] The capitalized text
|
8
8
|
def capitalize_first
|
9
|
-
"#{self[0, 1].upcase}#{self[1,
|
9
|
+
"#{self[0, 1].upcase}#{self[1, length]}"
|
10
10
|
end
|
11
11
|
alias cap_first capitalize_first
|
12
12
|
|
13
13
|
# Get an array of words split by any whitespace.
|
14
14
|
#
|
15
15
|
# @return [Array]
|
16
|
-
def
|
17
|
-
|
16
|
+
def keywords
|
17
|
+
gsub(/[\s-]+/, ' ').strip.downcase.split - %w[a an the]
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String]
|
21
|
+
def normalize
|
22
|
+
keywords.join(' ')
|
18
23
|
end
|
19
24
|
end
|