madderlib 0.1.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.
- data/CHANGELOG +3 -0
- data/LICENSE +22 -0
- data/README.rdoc +173 -0
- data/Rakefile +134 -0
- data/lib/madderlib.rb +27 -0
- data/lib/madderlib/builder.rb +659 -0
- data/lib/madderlib/conditional/allowed.rb +144 -0
- data/lib/madderlib/conditional/helper.rb +135 -0
- data/lib/madderlib/conditional/likely.rb +162 -0
- data/lib/madderlib/conditional/recur.rb +103 -0
- data/lib/madderlib/conditional/registry.rb +56 -0
- data/lib/madderlib/conditional/repeat.rb +130 -0
- data/lib/madderlib/context.rb +140 -0
- data/lib/madderlib/core.rb +217 -0
- data/lib/madderlib/extensions.rb +40 -0
- data/lib/madderlib/instruction.rb +171 -0
- data/lib/madderlib/phrase.rb +284 -0
- data/lib/madderlib/sequencer.rb +337 -0
- data/madderlib.gemspec +72 -0
- data/spec/benchmark_spec.rb +69 -0
- data/spec/builder_spec.rb +321 -0
- data/spec/builder_to_other_spec.rb +47 -0
- data/spec/builder_to_sequencer_spec.rb +388 -0
- data/spec/conditional_allowed_spec.rb +130 -0
- data/spec/conditional_helper_spec.rb +131 -0
- data/spec/conditional_likely_spec.rb +138 -0
- data/spec/conditional_recur_spec.rb +102 -0
- data/spec/conditional_registry_spec.rb +94 -0
- data/spec/conditional_repeat_spec.rb +88 -0
- data/spec/doc_spec.rb +550 -0
- data/spec/error_spec.rb +33 -0
- data/spec/examples_spec.rb +151 -0
- data/spec/extensions_spec.rb +47 -0
- data/spec/grammar_spec.rb +101 -0
- data/spec/instruction_spec.rb +133 -0
- data/spec/kernel_spec.rb +58 -0
- data/spec/phrase_spec.rb +7 -0
- data/spec/sequencer_spec.rb +317 -0
- data/spec/spec_helper.rb +54 -0
- metadata +98 -0
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Dan Foley / CantRemember.com
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
= MadderLib
|
2
|
+
|
3
|
+
A Sentence-Building DSL for the easily amused.
|
4
|
+
|
5
|
+
== Overview
|
6
|
+
|
7
|
+
This is a library for describing the logic for building a sentence.
|
8
|
+
Or, more specifically, for building an Array of Strings.
|
9
|
+
|
10
|
+
The steps involved are to:
|
11
|
+
|
12
|
+
* create a MadderLib::Builder
|
13
|
+
* define the MadderLib::Phrase objects which will comprise the sentence
|
14
|
+
* define one or more MadderLib::Instruction objects for each Phrase (eg. its word(s))
|
15
|
+
* execute the Builder to produce the result
|
16
|
+
|
17
|
+
A simple example would be:
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'madderlib'
|
21
|
+
|
22
|
+
puts madderlib {
|
23
|
+
say 'hello,'
|
24
|
+
say('welcome to').or.say("you're viewing")
|
25
|
+
say('the README.doc')
|
26
|
+
}.sentence
|
27
|
+
|
28
|
+
Once constructed, the Builder contains all of the rules necessary to build and re-build its sentence.
|
29
|
+
Within the builder, its Phrase and Instruction set can define:
|
30
|
+
|
31
|
+
* the ordering of the Phrase
|
32
|
+
* conditional interdependence of Phrases (eg. only say <i>X</i> if <i>Y</i> has already been said)
|
33
|
+
* conditions for including / excluding a given Instruction (eg. only if a customer has enrolled in <i>X</i>)
|
34
|
+
* the likelihood (odds) for choosing one Instruction over another within a given Phrase
|
35
|
+
* how often an Instruction will repeat, if chosen
|
36
|
+
|
37
|
+
And other variations along that line.
|
38
|
+
|
39
|
+
Some of the more useful aspects of this library are:
|
40
|
+
|
41
|
+
* the descriptive Builder syntax itself
|
42
|
+
* a Builder, once defined, can be cloned and/or extended, allowing for re-use and re-purposing
|
43
|
+
* extensive support for Procs / lambdas to allow for dynamic rules and content, vs. having to alter Builder logic
|
44
|
+
|
45
|
+
== MadderLib::KernelMethods
|
46
|
+
|
47
|
+
A small set of methods are injected into the Kernel namespace / scope for easy creation of a Builder.
|
48
|
+
|
49
|
+
The most commonly used method would be madderlib; it initiates a Builder
|
50
|
+
|
51
|
+
== MadderLib::Builder
|
52
|
+
|
53
|
+
The Builder and its methods provides access to all of the core capabilities of the library.
|
54
|
+
Examples are provided in the documentation.
|
55
|
+
Some good starters are:
|
56
|
+
|
57
|
+
* Builder#append (extend)
|
58
|
+
* Builder#clone
|
59
|
+
* Builder#phrase (it)
|
60
|
+
* Builder#and_then (and, then, also)
|
61
|
+
* Builder#first
|
62
|
+
* Builder#before
|
63
|
+
* Builder#alternately (or)
|
64
|
+
* Builder#sentence (to_s)
|
65
|
+
|
66
|
+
== MadderLib::Phrase and MadderLib::Instruction
|
67
|
+
|
68
|
+
Each Phrase is a segement of the sentence, usually only a word or more.
|
69
|
+
A Phrase is comprised of at least one Instruction, and perhaps more if there are multiple options.
|
70
|
+
By and large, you won't even notice them as being separate from the Builder.
|
71
|
+
|
72
|
+
Some good examples are:
|
73
|
+
|
74
|
+
* Phrase#alternately
|
75
|
+
* AnytimePhrase#before
|
76
|
+
* Instruction#speak
|
77
|
+
|
78
|
+
== MadderLib::Conditional
|
79
|
+
|
80
|
+
These are additional aspects to the library which support various features.
|
81
|
+
It's easiest to reference them by example:
|
82
|
+
|
83
|
+
* Conditional::Allowed::Instruction#assuming (if)
|
84
|
+
* Conditional::Likely::Instruction#likely (weight, odds)
|
85
|
+
* Conditional::Repeat::Instruction#repeat
|
86
|
+
* Conditional::Recur::Phrase#recur
|
87
|
+
|
88
|
+
== Examples
|
89
|
+
|
90
|
+
This example comes from Snake 'n' Bacon (http://wiki.cantremember.com/Twitter/SnakeAndBacon):
|
91
|
+
|
92
|
+
builder = madderlib do
|
93
|
+
say "i'm"
|
94
|
+
|
95
|
+
and_then
|
96
|
+
['sometimes', 'always', nil].each {|word| alternately.say(word) }
|
97
|
+
|
98
|
+
say('quite').or(2).say('rather').or(2).say('so').or.nothing
|
99
|
+
|
100
|
+
say 'salty'
|
101
|
+
['smoky', 'peppery'].each {|word| alternately(2).say(word) }
|
102
|
+
end
|
103
|
+
builder.validate
|
104
|
+
|
105
|
+
5.times { puts builder.sentence }
|
106
|
+
|
107
|
+
This is one of the simpler Builders from The Conet Project (http://wiki.cantremember.com/Twitter/Conet):
|
108
|
+
|
109
|
+
builder = madderlib do
|
110
|
+
meta[:audio] = [
|
111
|
+
'http://www.archive.org/download/ird059/tcp_d1_06_the_lincolnshire_poacher_mi5_irdial.mp3',
|
112
|
+
'http://www.archive.org/download/ird059/tcp_d3_02_iran_iraq_jamming_efficacy_testting_irdial.mp3',
|
113
|
+
]
|
114
|
+
|
115
|
+
digits = lambda do |len|
|
116
|
+
s = rand(10 ** len).to_s
|
117
|
+
s = ('0' * (len - s.size)) + s
|
118
|
+
s
|
119
|
+
end
|
120
|
+
|
121
|
+
say 'Lincolnshire Poacher'
|
122
|
+
say { digits.call(5) }.repeat(10)
|
123
|
+
|
124
|
+
say('~').repeat(6)
|
125
|
+
|
126
|
+
200.times do
|
127
|
+
say { s = digits.call(5); [s, s] }
|
128
|
+
end
|
129
|
+
|
130
|
+
say('~').repeat(6)
|
131
|
+
|
132
|
+
say 'Lincolnshire Poacher'
|
133
|
+
end
|
134
|
+
builder.validate
|
135
|
+
|
136
|
+
5.times { puts builder.sentence }
|
137
|
+
|
138
|
+
Here is a rather boring example, yet which is a bit more practical:
|
139
|
+
|
140
|
+
user = Struct.new(:name)
|
141
|
+
|
142
|
+
builder = madderlib do
|
143
|
+
setup {|context| context[:hour] ||= Time.new.hour }
|
144
|
+
a(:morning).says('top of the morning,').if {|c| Range.new(8, 12).include?(c[:hour]) }
|
145
|
+
say('good afternoon,').if {|c| Range.new(12, 17).include?(c[:hour]) }
|
146
|
+
say("g'night").if {|c| Range.new(19, 24).include?(c[:hour]) }
|
147
|
+
say {|c| c[:user].name + '.' }
|
148
|
+
end
|
149
|
+
|
150
|
+
puts builder.sentence {|c| c[:user] = user.new('joe')}
|
151
|
+
|
152
|
+
puts builder.sentence {|c|
|
153
|
+
c[:user] = user.new('fred')
|
154
|
+
c[:hour] = 13
|
155
|
+
}
|
156
|
+
|
157
|
+
extended = builder.clone.extend { say('have a nice day!').if(:morning) }
|
158
|
+
puts extended.sentence {|c|
|
159
|
+
c[:user] = user.new('charlie')
|
160
|
+
c[:hour] = 8
|
161
|
+
}
|
162
|
+
|
163
|
+
== Contributing
|
164
|
+
|
165
|
+
=== Issue Tracking and Feature Requests
|
166
|
+
|
167
|
+
* http://madderlib.rubyforge.org
|
168
|
+
|
169
|
+
== Community
|
170
|
+
|
171
|
+
=== Wiki
|
172
|
+
|
173
|
+
* http://wiki.cantremember.com/MadderLib
|
data/Rakefile
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
# SPECS ===============================================================
|
8
|
+
|
9
|
+
desc 'Run specs with story style output'
|
10
|
+
task :spec do
|
11
|
+
sh 'spec --format specdoc spec/*_spec.rb'
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'Run specs with unit test style output'
|
15
|
+
task :test => FileList['spec/*_spec.rb'] do |t|
|
16
|
+
suite = t.prerequisites.map{|f| "-r#{f.chomp('.rb')}"}.join(' ')
|
17
|
+
sh "ruby -Ilib:spec #{suite} -e ''", :verbose => false
|
18
|
+
end
|
19
|
+
|
20
|
+
# PACKAGING ============================================================
|
21
|
+
|
22
|
+
# Load the gemspec using the same limitations as github
|
23
|
+
def spec
|
24
|
+
@spec ||=
|
25
|
+
begin
|
26
|
+
require 'rubygems/specification'
|
27
|
+
data = File.read('madderlib.gemspec')
|
28
|
+
spec = nil
|
29
|
+
# OS X didn't like SAFE = 2
|
30
|
+
# (eval):25:in `glob': Insecure operation - glob
|
31
|
+
Thread.new { spec = eval("$SAFE = 2\n#{data}") }.join
|
32
|
+
spec
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def package(ext='')
|
37
|
+
"dist/madderlib-#{spec.version}" + ext
|
38
|
+
end
|
39
|
+
|
40
|
+
desc 'Build packages'
|
41
|
+
task :package => %w[.gem .tar.gz].map {|e| package(e)}
|
42
|
+
|
43
|
+
desc 'Build and install as local gem'
|
44
|
+
task :install => package('.gem') do
|
45
|
+
sh "gem install #{package('.gem')}"
|
46
|
+
end
|
47
|
+
|
48
|
+
directory 'dist/'
|
49
|
+
CLEAN.include 'dist'
|
50
|
+
|
51
|
+
file package('.gem') => %w[dist/ madderlib.gemspec] + spec.files do |f|
|
52
|
+
sh "gem build madderlib.gemspec"
|
53
|
+
mv File.basename(f.name), f.name
|
54
|
+
end
|
55
|
+
|
56
|
+
file package('.tar.gz') => %w[dist/] + spec.files do |f|
|
57
|
+
sh "git archive --format=tar HEAD | gzip > #{f.name}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Rubyforge Release / Publish Tasks ==================================
|
61
|
+
|
62
|
+
desc 'Publish website to rubyforge'
|
63
|
+
task 'publish:doc' => 'doc/api/index.html' do
|
64
|
+
sh 'scp -rp doc/* cantremember@rubyforge.org:/var/www/gforge-projects/madderlib/'
|
65
|
+
end
|
66
|
+
|
67
|
+
task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t|
|
68
|
+
sh <<-end
|
69
|
+
rubyforge add_release madderlib madderlib #{spec.version} #{package('.gem')} &&
|
70
|
+
rubyforge add_file madderlib madderlib #{spec.version} #{package('.tar.gz')}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Website ============================================================
|
75
|
+
# Building docs
|
76
|
+
|
77
|
+
task 'doc' => ['doc:api','doc:site']
|
78
|
+
|
79
|
+
desc 'Generate RDoc under doc/api'
|
80
|
+
task 'doc:api' => ['doc/api/index.html']
|
81
|
+
|
82
|
+
file 'doc/api/index.html' => FileList['lib/**/*.rb','README.rdoc','CHANGELOG','LICENSE'] do |f|
|
83
|
+
rb_files = f.prerequisites
|
84
|
+
sh((<<-end).gsub(/\s+/, ' '))
|
85
|
+
rdoc --line-numbers --inline-source --title MadderLib --main README.rdoc
|
86
|
+
#{rb_files.join(' ')}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
CLEAN.include 'doc/api'
|
90
|
+
|
91
|
+
def rdoc_to_html(file_name)
|
92
|
+
require 'rdoc/markup/to_html'
|
93
|
+
rdoc = RDoc::Markup::ToHtml.new
|
94
|
+
rdoc.convert(File.read(file_name))
|
95
|
+
end
|
96
|
+
|
97
|
+
def haml(locals={})
|
98
|
+
require 'haml'
|
99
|
+
template = File.read('doc/template.haml')
|
100
|
+
haml = Haml::Engine.new(template, :format => :html4, :attr_wrapper => '"')
|
101
|
+
haml.render(Object.new, locals)
|
102
|
+
end
|
103
|
+
|
104
|
+
desc 'Build website HTML and stuff'
|
105
|
+
task 'doc:site' => ['doc/index.html']
|
106
|
+
|
107
|
+
file 'doc/index.html' => %w[README.rdoc doc/template.haml] do |file|
|
108
|
+
File.open(file.name, 'w') do |file|
|
109
|
+
file << haml(:title => 'MadderLib', :content => rdoc_to_html('README.rdoc'))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
CLEAN.include 'doc/index.html'
|
113
|
+
|
114
|
+
# Gemspec Helpers ====================================================
|
115
|
+
|
116
|
+
file 'madderlib.gemspec' => FileList['{lib,spec}/**','Rakefile'] do |f|
|
117
|
+
# read spec file and split out manifest section
|
118
|
+
spec = File.read(f.name)
|
119
|
+
parts = spec.split(/\s+# = MANIFEST =\n/)
|
120
|
+
fail 'bad spec' if parts.length != 3
|
121
|
+
# determine file list from git ls-files
|
122
|
+
files = `git ls-files`.
|
123
|
+
split("\n").
|
124
|
+
sort.
|
125
|
+
reject{ |file| file =~ /^\./ }.
|
126
|
+
reject { |file| file =~ /^doc/ }.
|
127
|
+
map{ |file| " #{file}" }.
|
128
|
+
join("\n")
|
129
|
+
# piece file back together and write...
|
130
|
+
parts[1] = " s.files = %w[\n#{files}\n ]\n"
|
131
|
+
spec = parts.join(" # = MANIFEST =\n")
|
132
|
+
File.open(f.name, 'w') { |io| io.write(spec) }
|
133
|
+
puts "updated #{f.name}"
|
134
|
+
end
|
data/lib/madderlib.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#Requires all the individual components, in the proper sequence.
|
2
|
+
#That makes the use of this gem as easy as:
|
3
|
+
#
|
4
|
+
# require 'madderlib'
|
5
|
+
#--
|
6
|
+
|
7
|
+
# external
|
8
|
+
%w{ }.each {|lib| require lib }
|
9
|
+
|
10
|
+
# internal, and in the proper sequence
|
11
|
+
%w{
|
12
|
+
madderlib/core
|
13
|
+
madderlib/context
|
14
|
+
madderlib/conditional/helper
|
15
|
+
madderlib/conditional/registry
|
16
|
+
madderlib/conditional/allowed
|
17
|
+
madderlib/conditional/repeat
|
18
|
+
madderlib/conditional/recur
|
19
|
+
madderlib/conditional/likely
|
20
|
+
madderlib/extensions
|
21
|
+
madderlib/instruction
|
22
|
+
madderlib/phrase
|
23
|
+
madderlib/sequencer
|
24
|
+
madderlib/builder
|
25
|
+
}.each do |file|
|
26
|
+
require File.expand_path(File.join(File.dirname(__FILE__), file))
|
27
|
+
end
|
@@ -0,0 +1,659 @@
|
|
1
|
+
%w{ generator }.each {|lib| require lib }
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
module MadderLib
|
6
|
+
#= Builder
|
7
|
+
#
|
8
|
+
#A builder object for MadderLib sentences.
|
9
|
+
#
|
10
|
+
#The builder is constructed to include all of the Phrases (rules) for building a sentence.
|
11
|
+
#
|
12
|
+
#Traditionally this is done through KernelMethods shortcuts:
|
13
|
+
# builder = madderlib :id do
|
14
|
+
# say 'through Kernel'
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
#This can also be done through standard construction
|
19
|
+
# builder = MadderLib::Builder.new(:id) do
|
20
|
+
# say('through construction')
|
21
|
+
# ...
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
#Each time you invoke the builder using one of the following methods, its Phrases are executed from scratch using a new build context:
|
25
|
+
#* sentence : returns the resulting words as a composite String
|
26
|
+
#* words : returns a list of all the resulting words
|
27
|
+
#* each_words : iterates through each of the resulting words
|
28
|
+
#
|
29
|
+
#You can clone an existing builder. The resulting object is 'deeply' cloned.
|
30
|
+
#
|
31
|
+
#You can extend or append additional rules to an existing builder.
|
32
|
+
# builder.extend { say 'something more'; ... }
|
33
|
+
#
|
34
|
+
#You can add multiple setup and teardown blocks to the builder, which provide access to the active build context.
|
35
|
+
#
|
36
|
+
#All of the other features of the Builder involve management of and dispatching to the current Phrase.
|
37
|
+
class Builder
|
38
|
+
include Enumerable
|
39
|
+
|
40
|
+
#The (optional) id of the Builder
|
41
|
+
attr_reader :id
|
42
|
+
#An Array of each Phrase (rule) within the Builder
|
43
|
+
attr_reader :phrases
|
44
|
+
#An Array containing the id from each Phrase which has one (they're optional)
|
45
|
+
attr_reader :phrase_ids
|
46
|
+
#A Hash of arbitrary meta-data for the Builder.
|
47
|
+
#It is reserved for custom developer logic; the Builder doesn't consider its meta-data
|
48
|
+
attr_reader :meta
|
49
|
+
|
50
|
+
#Constructs a new Builder.
|
51
|
+
#The id is optional.
|
52
|
+
#When a block is provided, it is used to extend the new (empty) Builder
|
53
|
+
#
|
54
|
+
#The new Builder is not automatically added to the active Grammar.
|
55
|
+
#That is the responsibility of the caller
|
56
|
+
#
|
57
|
+
#Examples:
|
58
|
+
# builder = MadderLib::Builder.new
|
59
|
+
# builder.id.should be_nil
|
60
|
+
# builder.should have(0).phrases
|
61
|
+
#
|
62
|
+
# builder = MadderLib::Builder.new :id
|
63
|
+
# builder.id.should equal(:id)
|
64
|
+
# builder.should have(0).phrases
|
65
|
+
#
|
66
|
+
# builder = MadderLib::Builder.new do
|
67
|
+
# say 'no id'
|
68
|
+
# end
|
69
|
+
# builder.id.should be_nil
|
70
|
+
# builder.sentence.should eql('no id')
|
71
|
+
#
|
72
|
+
# builder = MadderLib::Builder.new :id do
|
73
|
+
# say {|context| context.builder.id }
|
74
|
+
# end
|
75
|
+
# builder.sentence.should eql('id')
|
76
|
+
def initialize(id=nil, &block)
|
77
|
+
@id = id
|
78
|
+
@phrases, @phrase_ids = [], []
|
79
|
+
@ordered, @depends = [], []
|
80
|
+
@setup, @teardown = [], []
|
81
|
+
@meta = {}
|
82
|
+
|
83
|
+
self.extend &block if block_given?
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
#Executes the block provided within the context of the Builder instance.
|
88
|
+
#This provides easy contextual access to say, or, first, anytime, and all other instance methods.
|
89
|
+
#
|
90
|
+
#Examples:
|
91
|
+
# builder = MadderLib::Builder.new { say 'construct' }
|
92
|
+
# builder.append { say 'extended' }
|
93
|
+
# builder.extend { say 'appending' }
|
94
|
+
# builder.words.should eql(%w{ construct extended appending })
|
95
|
+
def append(&block)
|
96
|
+
raise Error, 'extending block is required' unless block_given?
|
97
|
+
|
98
|
+
# evaluate in our context
|
99
|
+
# available in scope
|
100
|
+
# closure locals
|
101
|
+
# static methods, if you specify self.class
|
102
|
+
# instance variables and methods
|
103
|
+
# unavailable
|
104
|
+
# any methods in closure scope (sorry, Tennessee)
|
105
|
+
self.instance_eval &block
|
106
|
+
self
|
107
|
+
end
|
108
|
+
alias :extend :append
|
109
|
+
|
110
|
+
#Creates a deep-clone of the builder.
|
111
|
+
#You can extend the new builder's Phrases, or add new setup blocks, without impacting the original.
|
112
|
+
#
|
113
|
+
#Note that the two builders will share the same <i>original</i> Phrase list.
|
114
|
+
#If you modify one of them behind-the-scenes, that change will be shared by <i>both</i> builders.
|
115
|
+
#
|
116
|
+
#Examples:
|
117
|
+
# original = MadderLib::Builder.new do
|
118
|
+
# meta[:meta] = :original
|
119
|
+
# say 'original'
|
120
|
+
# and_then(:shared).say('initial').likely(1)
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# cloned = original.clone
|
124
|
+
# cloned[:meta] = :cloned
|
125
|
+
# cloned.extend { say 'cloned' }
|
126
|
+
#
|
127
|
+
# done = :original
|
128
|
+
# cloned.setup { done = :cloned }
|
129
|
+
#
|
130
|
+
# shared = original.phrases.find {|phrase| phrase.id == :shared }
|
131
|
+
# shared.instructions.first.words << 'added'
|
132
|
+
#
|
133
|
+
# original[:meta].should equal(:original)
|
134
|
+
# original.sentence.should eql('original initial added')
|
135
|
+
# done.should equal(:original)
|
136
|
+
#
|
137
|
+
# cloned[:meta].should equal(:cloned)
|
138
|
+
# cloned.sentence.should eql('original initial added cloned')
|
139
|
+
# done.should equal(:cloned)
|
140
|
+
def clone(id=nil)
|
141
|
+
o = super()
|
142
|
+
|
143
|
+
# deeper copy
|
144
|
+
@phrases = @phrases.clone
|
145
|
+
@phrase_ids = @phrase_ids.clone
|
146
|
+
@ordered = @ordered.clone
|
147
|
+
@depends = @depends.clone
|
148
|
+
@setup = @setup.clone
|
149
|
+
@teardown = @teardown.clone
|
150
|
+
@meta = @meta.clone
|
151
|
+
|
152
|
+
# don't want two of them floating around with the same id
|
153
|
+
o.instance_variable_set :@id, id
|
154
|
+
|
155
|
+
# put it into the grammar
|
156
|
+
# most importantly,
|
157
|
+
grammar = Grammar.get_instance
|
158
|
+
grammar.add o if grammar.builders.include?(self)
|
159
|
+
o
|
160
|
+
end
|
161
|
+
|
162
|
+
|
163
|
+
|
164
|
+
#Adds a setup block to the builder.
|
165
|
+
#
|
166
|
+
#The block is executed before the builder invokes its Phrases (rules).
|
167
|
+
#The block can either take no arguments, or a Context.
|
168
|
+
#
|
169
|
+
#Subsequent blocks are executed in the order provided.
|
170
|
+
#If you provide <code>:first</code> as an argument, the block will occur prior to any existing blocks.
|
171
|
+
#It would of course be preceded by any subsequent block which says that it is <code>:first</code>.
|
172
|
+
#
|
173
|
+
#Examples:
|
174
|
+
# builder = MadderLib::Builder.new do
|
175
|
+
# say {|context| context[:setup] }
|
176
|
+
# end
|
177
|
+
# builder.setup {|context| context[:setup] << 2 }
|
178
|
+
# builder.setup {|context| context[:setup] << 3 }
|
179
|
+
# builder.setup(:first) {|context| context[:setup] = [1] }
|
180
|
+
#
|
181
|
+
# builder.sentence.should eql('1 2 3')
|
182
|
+
def setup(*args, &block)
|
183
|
+
Context.validate(block)
|
184
|
+
|
185
|
+
# ordering
|
186
|
+
if args.include?(:first)
|
187
|
+
@setup.unshift block
|
188
|
+
else
|
189
|
+
@setup.push block
|
190
|
+
end
|
191
|
+
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
#Adds a teardown block to the builder.
|
196
|
+
#
|
197
|
+
#The block is executed after the builder has invoked its Phrases (rules).
|
198
|
+
#The block can either take no arguments, or a Context.
|
199
|
+
#
|
200
|
+
#Subsequent blocks are executed in the order provided.
|
201
|
+
#If you provide <code>:first</code> as an argument, the block will occur prior to any existing blocks.
|
202
|
+
#It would of course be preceded by any subsequent block which says that it is <code>:first</code>.
|
203
|
+
#
|
204
|
+
#Examples:
|
205
|
+
# builder = MadderLib::Builder.new do
|
206
|
+
# say 'teardown'
|
207
|
+
# end
|
208
|
+
# markers = []
|
209
|
+
# builder.teardown {|context| markers << 2 }
|
210
|
+
# builder.teardown {|context| markers << 3 }
|
211
|
+
# builder.teardown(:first) {|context| markers = [1] }
|
212
|
+
#
|
213
|
+
# builder.sentence.should eql('teardown')
|
214
|
+
# markers.should eql([1, 2, 3])
|
215
|
+
def teardown(*args, &block)
|
216
|
+
Context.validate(block)
|
217
|
+
|
218
|
+
# ordering
|
219
|
+
if args.include?(:first)
|
220
|
+
@teardown.unshift block
|
221
|
+
else
|
222
|
+
@teardown.push block
|
223
|
+
end
|
224
|
+
|
225
|
+
self
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
|
230
|
+
#Provides convenient access to the meta Hash.
|
231
|
+
#
|
232
|
+
#Examples:
|
233
|
+
# builder = MadderLib::Builder.new do
|
234
|
+
# meta[:key] = :value
|
235
|
+
# end
|
236
|
+
# builder[:key].should equal(:value)
|
237
|
+
def [](k)
|
238
|
+
@meta[k]
|
239
|
+
end
|
240
|
+
|
241
|
+
#Provides convenient access to the meta Hash.
|
242
|
+
#
|
243
|
+
#See: []
|
244
|
+
def []=(k, v)
|
245
|
+
@meta[k] = v
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
|
250
|
+
#Returns the current Phrase.
|
251
|
+
#
|
252
|
+
#Examples:
|
253
|
+
# builder = MadderLib::Builder.new do
|
254
|
+
# say 'yes'
|
255
|
+
# phrase.if {|context| context.builder[:activated] == true }
|
256
|
+
# it.repeat(3)
|
257
|
+
# end
|
258
|
+
#
|
259
|
+
# builder.should have(1).phrases
|
260
|
+
# builder.phrase.should have(1).instructions
|
261
|
+
#
|
262
|
+
# builder.should have(0).words
|
263
|
+
#
|
264
|
+
# builder[:activated] = true
|
265
|
+
# builder.sentence.should eql('yes yes yes')
|
266
|
+
def phrase
|
267
|
+
@phrases.last
|
268
|
+
end
|
269
|
+
alias :it :phrase
|
270
|
+
|
271
|
+
#Allocates another Phrase.
|
272
|
+
#An optional Phrase id can be provided.
|
273
|
+
#
|
274
|
+
#Examples:
|
275
|
+
# builder = MadderLib::Builder.new do
|
276
|
+
# say 'first'
|
277
|
+
# and_then.say 'and_then'
|
278
|
+
# also.say 'also'
|
279
|
+
# end
|
280
|
+
# builder.and.say 'and'
|
281
|
+
# builder.then.say 'then'
|
282
|
+
def and_then(id=nil)
|
283
|
+
add_id id
|
284
|
+
@phrases << Phrase.new(self, id)
|
285
|
+
@phrases.last
|
286
|
+
end
|
287
|
+
alias :also :and_then
|
288
|
+
alias :and :and_then
|
289
|
+
alias :then :and_then
|
290
|
+
|
291
|
+
#Allocates another Phrase, where an id is required.
|
292
|
+
#This is semantic sugar
|
293
|
+
#
|
294
|
+
#Examples:
|
295
|
+
# builder = MadderLib::Builder.new do
|
296
|
+
# say 'first'
|
297
|
+
# a(:second).says 'second'
|
298
|
+
# an(:other).says 'other'
|
299
|
+
# end
|
300
|
+
#
|
301
|
+
# builder.sentence.should eql('first second other')
|
302
|
+
def an(id)
|
303
|
+
and_then id
|
304
|
+
end
|
305
|
+
alias :a :an
|
306
|
+
|
307
|
+
|
308
|
+
|
309
|
+
#Allocates a Phrase which will be said first, relative to any existing Phrases.
|
310
|
+
#An optional Phrase id can be provided
|
311
|
+
#
|
312
|
+
#This phrase would of course be preceded by any subsequent first Phrase
|
313
|
+
#
|
314
|
+
#Examples:
|
315
|
+
# builder = MadderLib::Builder.new do
|
316
|
+
# say 'something'
|
317
|
+
# first.say 'say'
|
318
|
+
# end
|
319
|
+
# builder.sentence.should eql('say something')
|
320
|
+
#
|
321
|
+
# builder.first.say 'first'
|
322
|
+
# builder.sentence.should eql('first say something')
|
323
|
+
def first(id=nil)
|
324
|
+
ordered and_then(id), :first
|
325
|
+
end
|
326
|
+
|
327
|
+
#Allocates a Phrase which will be said last, relative to any existing Phrases.
|
328
|
+
#An optional Phrase id can be provided
|
329
|
+
#
|
330
|
+
#This phrase would of course be followed by any subsequent last Phrase
|
331
|
+
#
|
332
|
+
#Examples:
|
333
|
+
# builder = MadderLib::Builder.new do
|
334
|
+
# last.say 'said'
|
335
|
+
# say 'something'
|
336
|
+
# end
|
337
|
+
# builder.sentence.should eql('something said')
|
338
|
+
#
|
339
|
+
# builder.last.say 'last'
|
340
|
+
# builder.sentence.should eql('something said last')
|
341
|
+
def last(id=nil)
|
342
|
+
ordered and_then(id), :last
|
343
|
+
end
|
344
|
+
alias :lastly :last
|
345
|
+
|
346
|
+
#Allocates a Phrase which will be said anywhere.
|
347
|
+
#It's position will be random (though not first or last, except when there is no alternative).
|
348
|
+
#The Phrase will only appear once; you may want to make the Phrase recur as well
|
349
|
+
#
|
350
|
+
#Examples:
|
351
|
+
# builder = MadderLib::Builder.new do
|
352
|
+
# say 'top'
|
353
|
+
# say 'here'
|
354
|
+
# say 'there'
|
355
|
+
# say 'bottom'
|
356
|
+
# end
|
357
|
+
# builder.anywhere.say 'anywhere'
|
358
|
+
#
|
359
|
+
# words = builder.words
|
360
|
+
# words.should have(5).words
|
361
|
+
# words.find_all {|word| word == 'anywhere'}.should have(1).word
|
362
|
+
#
|
363
|
+
# builder.it.recurs(2)
|
364
|
+
#
|
365
|
+
# words = builder.words
|
366
|
+
# words.should have(6).words
|
367
|
+
# words.find_all {|word| word == 'anywhere'}.should have(2).word
|
368
|
+
def anytime(id=nil)
|
369
|
+
add_id id
|
370
|
+
@phrases << AnytimePhrase.new(self, id)
|
371
|
+
ordered self.phrase, :anytime
|
372
|
+
end
|
373
|
+
alias :anywhere :anytime
|
374
|
+
|
375
|
+
#Allocates a phrase which is said before another Phrase.
|
376
|
+
#The resulting words are inserted immediately before the referenced Phrase.
|
377
|
+
#
|
378
|
+
#This phrase would of course be preceded by any subsequent before Phrase referenced against the same id.
|
379
|
+
#Think of it as adding layers to an onion.
|
380
|
+
#
|
381
|
+
#If the referenced Phrase is never said, due to conditionals / odds / etc, the dependent Phrase will not be said.
|
382
|
+
#This of course cascades throughout the dependency chain.
|
383
|
+
#
|
384
|
+
#The referenced Phrase must exist, by id.
|
385
|
+
#However, that is not checked until execution (eg. not during build).
|
386
|
+
#You can test your completed Builder using the validate method.
|
387
|
+
#
|
388
|
+
#Examples:
|
389
|
+
# builder = MadderLib::Builder.new do
|
390
|
+
# an(:always).says 'always'
|
391
|
+
# a(:sometimes).says('sometimes').if {|context| context.builder[:sometimes] == true }
|
392
|
+
# before(:always).say 'before-always'
|
393
|
+
# before(:sometimes, :depends).say 'before-sometimes'
|
394
|
+
# before(:depends).say 'depends'
|
395
|
+
# end
|
396
|
+
#
|
397
|
+
# builder.sentence.should eql('before-always always')
|
398
|
+
#
|
399
|
+
# builder[:sometimes] = true
|
400
|
+
#
|
401
|
+
# builder.sentence.should eql('before-always always depends before-sometimes sometimes')
|
402
|
+
def before(ref, id=nil)
|
403
|
+
depends and_then(id), :before, ref
|
404
|
+
end
|
405
|
+
|
406
|
+
#Allocates a phrase which is said after another Phrase.
|
407
|
+
#The resulting words are inserted immediately after the referenced Phrase.
|
408
|
+
#
|
409
|
+
#This phrase would of course be followed by any subsequent after Phrase referenced against the same id.
|
410
|
+
#Think of it as adding layers to an onion.
|
411
|
+
#
|
412
|
+
#If the referenced Phrase is never said, due to conditionals / odds / etc, the dependent Phrase will not be said.
|
413
|
+
#This of course cascades throughout the dependency chain.
|
414
|
+
#
|
415
|
+
#The referenced Phrase must exist, by id.
|
416
|
+
#However, that is not checked until execution (eg. not during build).
|
417
|
+
#You can test your completed Builder using the validate method.
|
418
|
+
#
|
419
|
+
#Examples:
|
420
|
+
# builder = MadderLib::Builder.new do
|
421
|
+
# an(:always).says 'always'
|
422
|
+
# a(:sometimes).says('sometimes').if {|context| context.builder[:sometimes] == true }
|
423
|
+
# after(:always).say 'after-always'
|
424
|
+
# after(:sometimes, :depends).say 'after-sometimes'
|
425
|
+
# after(:depends).say 'depends'
|
426
|
+
# end
|
427
|
+
#
|
428
|
+
# builder.sentence.should eql('always after-always')
|
429
|
+
#
|
430
|
+
# builder[:sometimes] = true
|
431
|
+
#
|
432
|
+
# builder.sentence.should eql('always after-always sometimes after-sometimes depends')
|
433
|
+
def after(ref, id=nil)
|
434
|
+
depends and_then(id), :after, ref
|
435
|
+
end
|
436
|
+
|
437
|
+
|
438
|
+
#A shorthand method for and_then.say .
|
439
|
+
#A new Phrase is allocated (without an id), and then Phrase#say method is invoked with the arguments provided.
|
440
|
+
#
|
441
|
+
#Examples:
|
442
|
+
# builder = MadderLib::Builder.new do
|
443
|
+
# says 'word'
|
444
|
+
# say :symbol
|
445
|
+
# say { 'lambda' }
|
446
|
+
# end
|
447
|
+
# builder.should have(3).phrases
|
448
|
+
# builder.sentence.should eql('word symbol lambda')
|
449
|
+
def say(*args, &block)
|
450
|
+
and_then.say *args, &block
|
451
|
+
end
|
452
|
+
alias :says :say
|
453
|
+
|
454
|
+
#A shorthand method for phrase.alternately .
|
455
|
+
#The Phrase#alternately method is invoked against the current Phrase with the arguments provided
|
456
|
+
#
|
457
|
+
#An Error will be raised if there is no current Phrase.
|
458
|
+
#It's an easy condition to recover from, but it's bad use of the syntax.
|
459
|
+
#
|
460
|
+
#Examples:
|
461
|
+
# builder = MadderLib::Builder.new do
|
462
|
+
# says 'word'
|
463
|
+
# alternately.says :symbol
|
464
|
+
# end
|
465
|
+
# builder.or.say { 'lambda' }
|
466
|
+
#
|
467
|
+
# builder.should have(1).phrases
|
468
|
+
# builder.phrase.should have(3).instructions
|
469
|
+
# %w{ word symbol lambda}.include?(builder.sentence).should be_true
|
470
|
+
def alternately(*args, &block)
|
471
|
+
raise Error, "there is no active phrase. start one with 'say'" unless self.phrase
|
472
|
+
self.phrase.or *args, &block
|
473
|
+
end
|
474
|
+
alias :or :alternately
|
475
|
+
|
476
|
+
|
477
|
+
|
478
|
+
#Iterates through each of the words resulting from execution of the Builder.
|
479
|
+
#
|
480
|
+
#An optional Hash of Context data can be provided.
|
481
|
+
#It is merged into Context#data before the Builder is executed
|
482
|
+
def each_word(data=nil)
|
483
|
+
# from our words
|
484
|
+
self.words(data).each {|word| yield word }
|
485
|
+
end
|
486
|
+
alias :each :each_word
|
487
|
+
|
488
|
+
#Returns the array of words resulting from execution of the Builder.
|
489
|
+
#
|
490
|
+
#An optional Hash of Context data can be provided.
|
491
|
+
#It is merged into Context#data before the Phrase rules are executed
|
492
|
+
#
|
493
|
+
#An optional block can be provided.
|
494
|
+
#It will be invoked before the Phrase rules are executed
|
495
|
+
#The block can either take no arguments, or a Context.
|
496
|
+
#
|
497
|
+
#All Phrase rules are applied.
|
498
|
+
#Each word in the Array is a String.
|
499
|
+
#The resulting Array is flattened (vs. any Array hierarchies in the ruleset)
|
500
|
+
#
|
501
|
+
#Examples:
|
502
|
+
# builder = MadderLib::Builder.new do
|
503
|
+
# says 'word'
|
504
|
+
# say :symbol, [:with, :hierarchy]
|
505
|
+
# say { 'lambda' }
|
506
|
+
# end
|
507
|
+
# builder.words.should eql(%w{ word symbol with hierarchy lambda })
|
508
|
+
def words(data=nil, &block)
|
509
|
+
# words from a sequencer
|
510
|
+
# pass on the context data
|
511
|
+
# pass on the block, to pull in the context
|
512
|
+
# a new Sequencer each time
|
513
|
+
# TODO: optimize
|
514
|
+
# dirty flag is hard since phrases is exposed
|
515
|
+
# hashsum? clone of last known phrases PLUS dirty flag?
|
516
|
+
self.to_sequencer.words data, &block
|
517
|
+
end
|
518
|
+
alias :to_a :words
|
519
|
+
|
520
|
+
#Returns the composite sentence resulting from execution of the Builder.
|
521
|
+
#It's really just a shortcut for words.join .
|
522
|
+
#
|
523
|
+
#An optional separator String can be provided.
|
524
|
+
#The default separator is a single space
|
525
|
+
#
|
526
|
+
#An optional Hash of Context data can be provided.
|
527
|
+
#It is merged into Context#data before the Phrase rules are executed
|
528
|
+
#
|
529
|
+
#An optional block can be provided.
|
530
|
+
#It will be invoked before the Phrase rules are executed
|
531
|
+
#The block can either take no arguments, or a Context.
|
532
|
+
#
|
533
|
+
#Examples:
|
534
|
+
# builder = MadderLib::Builder.new do
|
535
|
+
# says 'word'
|
536
|
+
# say :symbol, [:with, :hierarchy]
|
537
|
+
# say { 'lambda' }
|
538
|
+
# end
|
539
|
+
# builder.sentence.should eql('word symbol with hierarchy lambda')
|
540
|
+
def sentence(*args, &block)
|
541
|
+
# argument scan
|
542
|
+
sep, data = ' ', nil
|
543
|
+
args.each do |arg|
|
544
|
+
if String === arg
|
545
|
+
# separator
|
546
|
+
sep = arg
|
547
|
+
elsif Hash === arg
|
548
|
+
# context data
|
549
|
+
data = arg
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
self.words(data, &block).join(sep)
|
554
|
+
end
|
555
|
+
alias :to_s :sentence
|
556
|
+
|
557
|
+
|
558
|
+
|
559
|
+
|
560
|
+
def to_sequencer #:nodoc:
|
561
|
+
# general ordering
|
562
|
+
sequence = []
|
563
|
+
map = {}
|
564
|
+
@phrases.each do |phrase|
|
565
|
+
sequence << phrase unless self.ordered?(phrase) || self.depends?(phrase)
|
566
|
+
map[phrase.id] = phrase if phrase.id
|
567
|
+
end
|
568
|
+
|
569
|
+
# specified ordering
|
570
|
+
# anytimes
|
571
|
+
anytimes = []
|
572
|
+
@ordered.each do |o|
|
573
|
+
case o.type
|
574
|
+
when :first
|
575
|
+
# before all other firsts
|
576
|
+
sequence.unshift o.phrase
|
577
|
+
when :last
|
578
|
+
# after all other lasts
|
579
|
+
sequence.push o.phrase
|
580
|
+
|
581
|
+
when :anytime
|
582
|
+
# guarantee valid references
|
583
|
+
phrase = o.phrase
|
584
|
+
|
585
|
+
[phrase.before, phrase.after].each do |ref|
|
586
|
+
raise Error, "no such phrase : #{ref.inspect}" unless (!ref) || map[ref]
|
587
|
+
end
|
588
|
+
anytimes << phrase
|
589
|
+
|
590
|
+
else
|
591
|
+
raise Error, "unknown ordering : #{o.type.inspect}"
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
befores, afters = {}, {}
|
596
|
+
@depends.each do |o|
|
597
|
+
ref = o.ref
|
598
|
+
raise Error, "no such phrase : #{ref.inspect}" unless map[ref]
|
599
|
+
case o.type
|
600
|
+
when :before
|
601
|
+
phrases = befores[ref]
|
602
|
+
befores[ref] = (phrases = []) unless phrases
|
603
|
+
# before all other befores
|
604
|
+
phrases.unshift o.phrase
|
605
|
+
when :after
|
606
|
+
phrases = afters[ref]
|
607
|
+
afters[ref] = (phrases = []) unless phrases
|
608
|
+
# after all other afters
|
609
|
+
phrases.push o.phrase
|
610
|
+
else
|
611
|
+
raise Error, "unknown dependency : #{o.type.inspect}"
|
612
|
+
end
|
613
|
+
end
|
614
|
+
|
615
|
+
Sequencer.new(self, sequence, map.keys, {
|
616
|
+
:anytime => anytimes, :before => befores, :after => afters,
|
617
|
+
:setup => @setup, :teardown => @teardown
|
618
|
+
})
|
619
|
+
end
|
620
|
+
|
621
|
+
#Validates the Phrase rules in the Builder.
|
622
|
+
#If there are any execution-time semantic issues, it will raise the applicable Error
|
623
|
+
alias :validate :to_sequencer
|
624
|
+
|
625
|
+
|
626
|
+
|
627
|
+
protected
|
628
|
+
|
629
|
+
def add_id(id) #:nodoc:
|
630
|
+
if id
|
631
|
+
raise Error, "id already exists : #{id.inspect}" if @phrase_ids.include?(id)
|
632
|
+
@phrase_ids << id
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
ORDERED = Struct.new(:phrase, :type) #:nodoc:
|
637
|
+
def ordered(phrase, type) #:nodoc:
|
638
|
+
# simple tuple, but with order retained
|
639
|
+
(@ordered ||= []) << ORDERED.new(phrase, type)
|
640
|
+
phrase
|
641
|
+
end
|
642
|
+
|
643
|
+
def ordered?(phrase) #:nodoc:
|
644
|
+
!! @ordered.find {|o| o.phrase == phrase }
|
645
|
+
end
|
646
|
+
|
647
|
+
DEPENDS = Struct.new(:phrase, :type, :ref) #:nodoc:
|
648
|
+
def depends(phrase, type, ref) #:nodoc:
|
649
|
+
# simple tuple, but with order retained
|
650
|
+
(@depends ||= []) << DEPENDS.new(phrase, type, ref)
|
651
|
+
phrase
|
652
|
+
end
|
653
|
+
|
654
|
+
def depends?(phrase) #:nodoc:
|
655
|
+
!! @depends.find {|o| o.phrase == phrase }
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
end
|