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.
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