gamefic 2.4.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +41 -40
  3. data/.rspec-opal +2 -0
  4. data/.solargraph.yml +20 -3
  5. data/CHANGELOG.md +9 -0
  6. data/Rakefile +11 -1
  7. data/bin/console +14 -0
  8. data/bin/setup +8 -0
  9. data/gamefic.gemspec +5 -2
  10. data/lib/gamefic/action.rb +52 -183
  11. data/lib/gamefic/active/cue.rb +25 -0
  12. data/lib/gamefic/active/epic.rb +68 -0
  13. data/lib/gamefic/active/messaging.rb +43 -0
  14. data/lib/gamefic/active/take.rb +69 -0
  15. data/lib/gamefic/active.rb +95 -192
  16. data/lib/gamefic/actor.rb +2 -0
  17. data/lib/gamefic/block.rb +28 -0
  18. data/lib/gamefic/command.rb +16 -6
  19. data/lib/gamefic/core_ext/array.rb +4 -4
  20. data/lib/gamefic/core_ext/string.rb +10 -5
  21. data/lib/gamefic/describable.rb +39 -65
  22. data/lib/gamefic/dispatcher.rb +63 -32
  23. data/lib/gamefic/entity.rb +44 -19
  24. data/lib/gamefic/logging.rb +32 -0
  25. data/lib/gamefic/messenger.rb +66 -0
  26. data/lib/gamefic/narrative.rb +104 -0
  27. data/lib/gamefic/node.rb +44 -53
  28. data/lib/gamefic/plot.rb +60 -93
  29. data/lib/gamefic/props/default.rb +41 -0
  30. data/lib/gamefic/props/multiple_choice.rb +65 -0
  31. data/lib/gamefic/props/pause.rb +11 -0
  32. data/lib/gamefic/props/yes_or_no.rb +21 -0
  33. data/lib/gamefic/props.rb +10 -0
  34. data/lib/gamefic/query/base.rb +45 -126
  35. data/lib/gamefic/query/general.rb +46 -0
  36. data/lib/gamefic/query/result.rb +20 -0
  37. data/lib/gamefic/query/scoped.rb +41 -0
  38. data/lib/gamefic/query/text.rb +30 -31
  39. data/lib/gamefic/query.rb +7 -15
  40. data/lib/gamefic/response.rb +118 -0
  41. data/lib/gamefic/rulebook/calls.rb +90 -0
  42. data/lib/gamefic/rulebook/events.rb +79 -0
  43. data/lib/gamefic/rulebook/hooks.rb +57 -0
  44. data/lib/gamefic/rulebook/scenes.rb +68 -0
  45. data/lib/gamefic/rulebook.rb +139 -0
  46. data/lib/gamefic/scanner.rb +103 -0
  47. data/lib/gamefic/scene/activity.rb +9 -17
  48. data/lib/gamefic/scene/conclusion.rb +6 -5
  49. data/lib/gamefic/scene/default.rb +88 -0
  50. data/lib/gamefic/scene/multiple_choice.rb +14 -69
  51. data/lib/gamefic/scene/pause.rb +9 -13
  52. data/lib/gamefic/scene/yes_or_no.rb +6 -46
  53. data/lib/gamefic/scene.rb +11 -7
  54. data/lib/gamefic/scope/base.rb +44 -0
  55. data/lib/gamefic/scope/children.rb +16 -0
  56. data/lib/gamefic/scope/family.rb +20 -0
  57. data/lib/gamefic/scope/myself.rb +13 -0
  58. data/lib/gamefic/scope/parent.rb +13 -0
  59. data/lib/gamefic/scope/siblings.rb +14 -0
  60. data/lib/gamefic/scope.rb +8 -0
  61. data/lib/gamefic/scriptable/actions.rb +156 -0
  62. data/lib/gamefic/scriptable/entities.rb +76 -0
  63. data/lib/gamefic/scriptable/events.rb +65 -0
  64. data/lib/gamefic/scriptable/proxy.rb +55 -0
  65. data/lib/gamefic/scriptable/queries.rb +73 -0
  66. data/lib/gamefic/scriptable/scenes.rb +162 -0
  67. data/lib/gamefic/scriptable.rb +167 -73
  68. data/lib/gamefic/snapshot.rb +36 -0
  69. data/lib/gamefic/stage.rb +51 -0
  70. data/lib/gamefic/subplot.rb +51 -79
  71. data/lib/gamefic/syntax/template.rb +67 -0
  72. data/lib/gamefic/syntax.rb +102 -83
  73. data/lib/gamefic/vault.rb +50 -0
  74. data/lib/gamefic/version.rb +1 -1
  75. data/lib/gamefic.rb +26 -15
  76. data/spec-opal/spec_helper.rb +24 -0
  77. metadata +91 -29
  78. data/lib/gamefic/element.rb +0 -46
  79. data/lib/gamefic/keywords.rb +0 -52
  80. data/lib/gamefic/messaging.rb +0 -43
  81. data/lib/gamefic/plot/darkroom.rb +0 -120
  82. data/lib/gamefic/plot/host.rb +0 -42
  83. data/lib/gamefic/plot/snapshot.rb +0 -27
  84. data/lib/gamefic/query/children.rb +0 -9
  85. data/lib/gamefic/query/descendants.rb +0 -15
  86. data/lib/gamefic/query/external.rb +0 -39
  87. data/lib/gamefic/query/family.rb +0 -18
  88. data/lib/gamefic/query/itself.rb +0 -13
  89. data/lib/gamefic/query/matches.rb +0 -75
  90. data/lib/gamefic/query/parent.rb +0 -9
  91. data/lib/gamefic/query/siblings.rb +0 -13
  92. data/lib/gamefic/query/tree.rb +0 -17
  93. data/lib/gamefic/scene/base.rb +0 -142
  94. data/lib/gamefic/scene/multiple_scene.rb +0 -29
  95. data/lib/gamefic/serialize.rb +0 -196
  96. data/lib/gamefic/world/callbacks.rb +0 -135
  97. data/lib/gamefic/world/commands.rb +0 -181
  98. data/lib/gamefic/world/entities.rb +0 -98
  99. data/lib/gamefic/world/playbook.rb +0 -233
  100. data/lib/gamefic/world/players.rb +0 -37
  101. data/lib/gamefic/world/scenes.rb +0 -228
  102. data/lib/gamefic/world.rb +0 -18
@@ -1,94 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
1
5
  module Gamefic
2
- # The Scriptable module provides a clean room (aka "theater") for scripts.
3
- #
4
- # @!method stage(*args, &block)
5
- # Execute a block of code in a subset of the owner's scope.
6
+ # A class module that enables scripting.
6
7
  #
7
- # The provided code is evaluated inside a clean room object that has its
8
- # own instance variables and access to the owner's public methods. The proc
9
- # can accept the method call's arguments.
8
+ # Narratives extend Scriptable to enable definition of scripts and seeds.
9
+ # Modules can also be extended with Scriptable to make them includable to
10
+ # other Scriptables.
10
11
  #
11
- # @example Execute a block of code
12
- # stage {
13
- # puts 'Hello'
14
- # }
12
+ # @example Include a scriptable module in a plot
13
+ # module MyScript
14
+ # extend Gamefic::Scriptable
15
15
  #
16
- # @example Execute a block of code with arguments
17
- # stage 'hello' { |message|
18
- # puts message # <- prints 'hello'
19
- # }
16
+ # respond :myscript do |actor|
17
+ # actor.tell "This command was added by MyScript"
18
+ # end
19
+ # end
20
20
  #
21
- # @example Use an instance variable
22
- # stage { @message = 'hello'" }
23
- # stage { puts @message } # <- prints 'hello'
21
+ # class MyPlot < Gamefic::Plot
22
+ # include MyScript
23
+ # end
24
24
  #
25
- # @yieldpublic [Gamefic::Plot]
26
- # @return [Object] The value returned by the executed code
27
- #
28
- # @!method theater
29
- # The object that acts as an isolated namespace for staged code.
30
- # @return [Object]
31
- #
32
- # @!parse alias cleanroom theater
33
25
  module Scriptable
34
- module ClassMethods
35
- # An array of blocks that were added by the `script` class method.
36
- #
37
- # @return [Array<Proc>]
38
- def blocks
39
- @blocks ||= []
40
- end
26
+ autoload :Actions, 'gamefic/scriptable/actions'
27
+ autoload :Entities, 'gamefic/scriptable/entities'
28
+ autoload :Events, 'gamefic/scriptable/events'
29
+ autoload :Queries, 'gamefic/scriptable/queries'
30
+ autoload :Proxy, 'gamefic/scriptable/proxy'
31
+ autoload :Scenes, 'gamefic/scriptable/scenes'
41
32
 
42
- # Add a block to be executed by the instance's `stage` method.
43
- #
44
- # Note that `script` does not execute the block instantly, but stores
45
- # it in the `blocks` array to be executed later.
46
- #
47
- # @yieldpublic [Gamefic::Plot]
48
- def script &block
49
- blocks.push block
50
- end
33
+ include Proxy
34
+ include Queries
35
+ # @!parse
36
+ # include Scriptable::Actions
37
+ # include Scriptable::Events
38
+ # include Scriptable::Scenes
39
+
40
+ # @return [Array<Block>]
41
+ def blocks
42
+ @blocks ||= []
43
+ end
44
+ alias scripts blocks
45
+
46
+ # Add a block of code to be executed during initialization.
47
+ #
48
+ # These blocks are primarily used to define actions, scenes, and hooks in
49
+ # the narrative's rulebook. Entities and game data should be initialized
50
+ # with `seed`.
51
+ #
52
+ # @example
53
+ # class MyPlot < Gamefic::Plot
54
+ # script do
55
+ # introduction do |actor|
56
+ # actor.tell 'Hello, world!'
57
+ # end
58
+ #
59
+ # respond :wait do |actor|
60
+ # actor.tell 'Time passes.'
61
+ # end
62
+ # end
63
+ # end
64
+ #
65
+ def script &block
66
+ blocks.push Block.new(:script, block)
51
67
  end
52
68
 
53
- def self.included klass
54
- klass.extend ClassMethods
69
+ # Add a block of code to generate content after initialization.
70
+ #
71
+ # Seeds run after the initial scripts have been executed. Their primary
72
+ # use is to add entities and other data components, especially randomized
73
+ # or procedurally generated content that can vary between instances.
74
+ #
75
+ # @note Seeds do not get executed when a narrative is restored from a
76
+ # snapshot.
77
+ #
78
+ # @example
79
+ # class MyPlot < Gamefic::Plot
80
+ # seed do
81
+ # @thing = make Gamefic::Entity, name: 'a thing'
82
+ # end
83
+ # end
84
+ #
85
+ def seed &block
86
+ blocks.push Block.new(:seed, block)
55
87
  end
56
88
 
57
- private
89
+ # @return [Array<Block>]
90
+ def included_blocks
91
+ included_modules.that_are(Scriptable)
92
+ .uniq
93
+ .reverse
94
+ .flat_map(&:blocks)
95
+ .concat(blocks)
96
+ end
58
97
 
59
- # Execute all the scripts that were added by the `script` class method.
98
+ # Seed an entity.
99
+ #
100
+ # @example
101
+ # make_seed Gamefic::Entity, name: 'thing'
60
102
  #
61
- def run_scripts
62
- self.class.blocks.each { |blk| stage &blk }
103
+ # @param klass [Class<Gamefic::Entity>]
104
+ def make_seed klass, **opts
105
+ @count ||= 0
106
+ seed { make(klass, **opts) }
107
+ @count.tap { @count += 1 }
63
108
  end
64
- end
65
- end
66
109
 
67
- # @note #stage and #theater are implemented this way so the clean room object
68
- # defines its classes and modules in the root namespace.
69
- Gamefic::Scriptable.module_exec do
70
- define_method :stage do |*args, &block|
71
- theater.instance_exec *args, &block
72
- end
110
+ # Seed an entity with an attribute method.
111
+ #
112
+ # @example
113
+ # class Plot < Gamefic::Plot
114
+ # attr_seed :thing, Gamefic::Entity, name: 'thing'
115
+ # end
116
+ #
117
+ # plot = Plot.new
118
+ # plot.thing #=> #<Gamefic::Entity a thing>
119
+ #
120
+ # @param klass [Class<Gamefic::Entity>]
121
+ def attr_seed name, klass, **opts
122
+ @count ||= 0
123
+ seed do
124
+ instance_variable_set("@#{name}", make(klass, **opts))
125
+ self.class.define_method(name) { instance_variable_get("@#{name}") }
126
+ end
127
+ @count.tap { @count += 1 }
128
+ end
129
+
130
+ if RUBY_ENGINE == 'opal'
131
+ # :nocov:
132
+ def method_missing method, *args, &block
133
+ return super unless respond_to_missing?(method)
134
+
135
+ script { send(method, *args, &block) }
136
+ end
137
+ # :nocov:
138
+ else
139
+ def method_missing method, *args, **kwargs, &block
140
+ return super unless respond_to_missing?(method)
141
+
142
+ script { send(method, *args, **kwargs, &block) }
143
+ end
144
+ end
145
+
146
+ def respond_to_missing?(method, _with_private = false)
147
+ [Scriptable::Actions, Scriptable::Events, Scriptable::Scenes].flat_map(&:public_instance_methods)
148
+ .include?(method)
149
+ end
73
150
 
74
- define_method :theater do
75
- @theater ||= begin
76
- instance = self
77
- theater ||= Object.new
78
- theater.instance_exec do
79
- if RUBY_ENGINE == 'opal' || RUBY_VERSION =~ /^2\.[456]\./
80
- define_singleton_method :method_missing do |symbol, *args, &block|
81
- instance.public_send :public_send, symbol, *args, &block
82
- end
83
- else
84
- define_singleton_method :method_missing do |symbol, *args, **splat, &block|
85
- instance.public_send :public_send, symbol, *args, **splat, &block
86
- end
87
- end
151
+ # Create an anonymous module that includes the features of a Scriptable
152
+ # module but does not include its scripts.
153
+ #
154
+ # This can be useful when you need access to the Scriptable's constants and
155
+ # instance methods, but you don't want to duplicate its rules.
156
+ #
157
+ # @example
158
+ # # Plot and Subplot will both include the `info` method, but
159
+ # # only Plot will implement the `think` action.
160
+ #
161
+ # module Shared
162
+ # extend Gamefic::Scriptable
163
+ #
164
+ # def info
165
+ # "This method was added by the Shared module."
166
+ # end
167
+ #
168
+ # respond :think do |actor|
169
+ # actor.tell 'You ponder your predicament.'
170
+ # end
171
+ # end
172
+ #
173
+ # class Plot < Gamefic::Plot
174
+ # include Shared
175
+ # end
176
+ #
177
+ # class Subplot < Gamefic::Subplot
178
+ # include Shared.no_scripts
179
+ # end
180
+ #
181
+ # @return [Module]
182
+ def no_scripts
183
+ Module.new.tap do |mod|
184
+ append_features(mod)
88
185
  end
89
- theater.extend Gamefic::Serialize
90
- theater
91
186
  end
92
187
  end
93
- alias cleanroom theater
94
188
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'corelib/marshal' if RUBY_ENGINE == 'opal' # Required in browser
4
+ require 'base64'
5
+
6
+ module Gamefic
7
+ # Save and restore plots.
8
+ #
9
+ module Snapshot
10
+ # Save a base64-encoded snapshot of a plot.
11
+ #
12
+ # @param plot [Plot]
13
+ # @return [String]
14
+ def self.save plot
15
+ cache = plot.detach
16
+ binary = Marshal.dump(plot)
17
+ plot.attach cache
18
+ Base64.encode64(binary)
19
+ end
20
+
21
+ # Restore a plot from a base64-encoded string.
22
+ #
23
+ # @param snapshot [String]
24
+ # @return [Plot]
25
+ def self.restore snapshot
26
+ binary = Base64.decode64(snapshot)
27
+ Marshal.load(binary).tap do |plot|
28
+ plot.hydrate
29
+ # @todo Opal marshal dumps are not idempotent
30
+ next if RUBY_ENGINE == 'opal' || Snapshot.save(plot) == snapshot
31
+
32
+ Logging.logger.warn "Scripts modified #{plot.class} data. Snapshot may not have restored properly"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ # A safe execution environment for narrative code.
5
+ #
6
+ module Stage
7
+ module_function
8
+
9
+ # @param narrative [Narrative]
10
+ def run(narrative, *args, &code)
11
+ container = narrative.clone
12
+ narrative.instance_exec(*args, &code).tap { validate_changes narrative, container, code }
13
+ end
14
+
15
+ OVERWRITEABLE_CLASSES = [String, Numeric, Symbol].freeze
16
+
17
+ SWAPPABLE_VALUES = [true, false, nil].freeze
18
+
19
+ class << self
20
+ private
21
+
22
+ def validate_changes narrative, container, code
23
+ container.instance_variables.each do |var|
24
+ next unless narrative.instance_variables.include?(var)
25
+
26
+ cval = container.instance_variable_get(var)
27
+
28
+ nval = narrative.instance_variable_get(var)
29
+ next if cval == nval
30
+
31
+ validate_overwriteable(cval, nval, "Unsafe reassignment of #{var} in #{code}")
32
+ end
33
+ end
34
+
35
+ def validate_overwriteable cval, nval, error
36
+ raise error unless overwriteable?(cval, nval)
37
+ end
38
+
39
+ def overwriteable? cval, nval
40
+ return true if swappable?(cval, nval)
41
+
42
+ allowed = OVERWRITEABLE_CLASSES.find { |klass| cval.is_a?(klass) }
43
+ allowed && cval.is_a?(allowed)
44
+ end
45
+
46
+ def swappable? *values
47
+ values.all? { |val| SWAPPABLE_VALUES.include?(val) }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,106 +1,78 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'gamefic/plot'
2
4
 
3
5
  module Gamefic
4
6
  # Subplots are disposable plots that run inside a parent plot. They can be
5
- # started and concluded at any time during the parent plot's execution.
7
+ # started and concluded at any time during the parent plot's runtime.
6
8
  #
7
- class Subplot
8
- include World
9
- include Scriptable
10
- include Gamefic::Serialize
11
- # @!parse extend Scriptable::ClassMethods
9
+ class Subplot < Narrative
10
+ # @return [Hash]
11
+ attr_reader :config
12
12
 
13
- # @return [Gamefic::Plot]
13
+ # @return [Plot]
14
14
  attr_reader :plot
15
15
 
16
16
  # @param plot [Gamefic::Plot]
17
- # @param introduce [Gamefic::Actor, nil]
18
- # @param next_cue [Class<Gamefic::Scene::Base>, nil]
19
- # @param more [Hash]
20
- def initialize plot, introduce: nil, next_cue: nil, **more
17
+ # @param introduce [Gamefic::Actor, Array<Gamefic::Actor>, nil]
18
+ # @param config [Hash]
19
+ def initialize plot, introduce: [], **config
21
20
  @plot = plot
22
- @next_cue = next_cue
23
- @concluded = false
24
- @more = more
25
- configure **more
26
- run_scripts
27
- playbook.freeze
28
- self.introduce introduce unless introduce.nil?
29
- @static = [self] + scene_classes + entities
30
- end
31
-
32
- def static
33
- plot.static
34
- end
35
-
36
- def players
37
- @players ||= []
38
- end
39
-
40
- def subplot
41
- self
42
- end
43
-
44
- def default_scene
45
- plot.default_scene
46
- end
47
-
48
- def default_conclusion
49
- plot.default_conclusion
50
- end
51
-
52
- def playbook
53
- @playbook ||= Gamefic::Plot::Playbook.new
21
+ @config = config
22
+ configure
23
+ @config.freeze
24
+ super()
25
+ [introduce].flatten.each { |pl| self.introduce pl }
54
26
  end
55
27
 
56
- def cast cls, args = {}, &block
57
- ent = super
58
- ent.playbooks.push plot.playbook unless ent.playbooks.include?(plot.playbook)
59
- ent
60
- end
61
-
62
- def exeunt player
63
- player_conclude_procs.each { |block| block.call player }
64
- player.playbooks.delete playbook
65
- player.cue (@next_cue || default_scene)
66
- players.delete player
28
+ def ready
29
+ super
30
+ conclude if concluding?
67
31
  end
68
32
 
69
33
  def conclude
70
- @concluded = true
71
- # Players needed to exit first in case any player_conclude procs need to
72
- # interact with the subplot's entities.
73
- players.each { |p| exeunt p }
74
- # @todo I'm not sure why rejecting nils is necessary here. It's only an
75
- # issue in Opal.
76
- entities.reject(&:nil?).each { |e| destroy e }
77
- # plot.static.remove(scene_classes + entities)
34
+ rulebook.run_conclude_blocks
35
+ players.each do |plyr|
36
+ rulebook.run_player_conclude_blocks plyr
37
+ uncast plyr
38
+ end
39
+ entities.each { |ent| destroy ent }
78
40
  end
79
41
 
80
- def concluded?
81
- @concluded
82
- end
83
-
84
- def ready
85
- # @todo We might not want to conclude subplots without players. There
86
- # might be cases where a subplot gets created with the intention of
87
- # introducing players in a later turn.
88
- conclude if players.empty?
89
- return if concluded?
90
- playbook.freeze
91
- call_ready
92
- call_player_ready
42
+ # Make an entity that persists in the subplot's parent plot.
43
+ #
44
+ # @see Plot#make
45
+ #
46
+ def persist klass, **args
47
+ plot.make klass, *args
93
48
  end
94
49
 
95
- def update
96
- call_player_update
97
- call_update
50
+ # Start a new subplot based on the provided class.
51
+ #
52
+ # @note A subplot's host is always the base plot, regardless of whether
53
+ # it was branched from another subplot.
54
+ #
55
+ # @param subplot_class [Class<Gamefic::Subplot>] The Subplot class
56
+ # @param introduce [Gamefic::Actor, Array<Gamefic::Actor>, nil] Players to introduce
57
+ # @param config [Hash] Subplot configuration
58
+ # @return [Gamefic::Subplot]
59
+ def branch subplot_class = Gamefic::Subplot, introduce: [], **config
60
+ plot.branch subplot_class, introduce: introduce, **config
98
61
  end
99
62
 
100
63
  # Subclasses can override this method to handle additional configuration
101
64
  # options.
102
65
  #
103
- def configure **more
66
+ def configure; end
67
+
68
+ def inspect
69
+ "#<#{self.class}>"
70
+ end
71
+
72
+ def hydrate
73
+ @rulebook = Rulebook.new(self)
74
+ @rulebook.script
75
+ @rulebook.freeze
104
76
  end
105
77
  end
106
78
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gamefic
4
+ class Syntax
5
+ # Template data for syntaxes.
6
+ #
7
+ class Template
8
+ PARAM_REGEXP = /^:[a-z0-9_]+$/i.freeze
9
+
10
+ # @return [String]
11
+ attr_reader :text
12
+
13
+ # @return [Array<String>]
14
+ attr_reader :params
15
+
16
+ def initialize text
17
+ @text = text.normalize
18
+ @params = @text.keywords.select { |word| word.start_with?(':') }
19
+ end
20
+
21
+ def keywords
22
+ text.keywords
23
+ end
24
+
25
+ def to_s
26
+ text
27
+ end
28
+
29
+ def regexp
30
+ @regexp ||= Regexp.new("^#{make_tokens.join(' ')}$", Regexp::IGNORECASE)
31
+ end
32
+
33
+ def verb
34
+ @verb ||= Syntax.literal_or_nil(keywords.first)
35
+ end
36
+
37
+ def compare other
38
+ if keywords.length == other.keywords.length
39
+ other.verb <=> verb
40
+ else
41
+ other.keywords.length <=> keywords.length
42
+ end
43
+ end
44
+
45
+ # @param tmpl_or_str [Template, String]
46
+ # @return [Template]
47
+ def self.to_template tmpl_or_str
48
+ return tmpl_or_str if tmpl_or_str.is_a?(Template)
49
+
50
+ Template.new(tmpl_or_str)
51
+ end
52
+
53
+ private
54
+
55
+ # @return [Array<String>]
56
+ def make_tokens
57
+ keywords.map.with_index do |word, idx|
58
+ next word unless word.match?(PARAM_REGEXP)
59
+
60
+ next nil if idx.positive? && keywords[idx - 1].match?(PARAM_REGEXP)
61
+
62
+ '([\w\W\s\S]*?)'
63
+ end.compact
64
+ end
65
+ end
66
+ end
67
+ end