graffle 0.1.8 → 0.1.9
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/History.txt +5 -0
- data/Manifest.txt +22 -11
- data/README.txt +2 -2
- data/Rakefile.hoe +2 -1
- data/design-notes/graphical-tests-for-rails-objects.graffle +644 -0
- data/examples/objects with notes.expected +5 -0
- data/examples/objects with notes.graffle +338 -0
- data/examples/objects with notes.rb +42 -0
- data/examples/rails-workflow-test.expected +1 -1
- data/examples/rails-workflow-test.graffle/data.plist +86 -7
- data/examples/rails-workflow-test.rb +11 -9
- data/graffle.tmproj +82 -190
- data/lib/graffle.rb +19 -2
- data/lib/graffle/point.rb +1 -2
- data/lib/graffle/stereotypes.rb +62 -16
- data/lib/graffle/version.rb +1 -1
- data/lib/graphical_tests_for_rails.rb +8 -5
- data/lib/graphical_tests_for_rails/graphic-volunteers.rb +75 -0
- data/lib/graphical_tests_for_rails/orderings.rb +90 -84
- data/lib/graphical_tests_for_rails/picture-appliers.rb +225 -0
- data/lib/graphical_tests_for_rails/text-appliers.rb +135 -0
- data/lib/graphical_tests_for_rails/volunteer-pool.rb +115 -0
- data/test/abstract-graphic-tests.rb +48 -0
- data/test/document-tests.rb +5 -5
- data/test/examples-tests.rb +42 -0
- data/test/graphical_tests_for_rails/{graphic-interpreter-tests.rb → deprecated-graphic-interpreter-tests.rb} +11 -21
- data/test/graphical_tests_for_rails/graphic-volunteer-tests.rb +218 -0
- data/test/graphical_tests_for_rails/in-workflow-order-tests.rb +1 -1
- data/test/graphical_tests_for_rails/picture-applier-tests.rb +215 -0
- data/test/graphical_tests_for_rails/{text-interpreter-tests.rb → text-applier-tests.rb} +17 -3
- data/test/graphical_tests_for_rails/util.rb +16 -0
- data/test/line-graphic-tests.rb +9 -1
- data/test/note-tests.rb +62 -0
- data/test/{graffle-file-types → sample-files}/as-a-package.graffle/data.plist +0 -0
- data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image1.png +0 -0
- data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image2.png +0 -0
- data/test/{graffle-file-types → sample-files}/as-a-package.graffle/image3.png +0 -0
- data/test/{graffle-file-types → sample-files}/multiple-canvases.graffle +0 -0
- data/test/{graffle-file-types → sample-files}/opening-tests.rb +9 -4
- data/test/{graffle-file-types → sample-files}/two-boxes-and-a-line.graffle +0 -0
- data/test/shaped-graphic-tests.rb +2 -3
- metadata +42 -18
- data/lib/graphical_tests_for_rails/interpreters.rb +0 -147
- data/test/tests-of-examples/workflow-slowtests.rb +0 -19
@@ -0,0 +1,225 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-21.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
require 'enumerator'
|
7
|
+
require 'test/unit/assertionfailederror'
|
8
|
+
require 'fluid'
|
9
|
+
require 'graphical_tests_for_rails/text-appliers'
|
10
|
+
require 'graphical_tests_for_rails/volunteer-pool'
|
11
|
+
|
12
|
+
|
13
|
+
module GraphicalTestsForRails
|
14
|
+
|
15
|
+
# A PictureApplier takes an array of graphics and arranges for
|
16
|
+
# messages derived from them to be sent to a _target_. PictureAppliers
|
17
|
+
# are "programmed" with other objects that handle the details of
|
18
|
+
# transforming graphics and the text they contain into messages.
|
19
|
+
#
|
20
|
+
# The
|
21
|
+
# graphics are processed in order. GraphicOrderer objects should be
|
22
|
+
# used to arrange the array before it's given to PictureApplier.
|
23
|
+
class PictureApplier
|
24
|
+
include Graffle
|
25
|
+
include GraphicalTestsForRails::TextAppliers
|
26
|
+
|
27
|
+
# The block tells the PictureApplier what to do by sending given,
|
28
|
+
# matching, use, and skip messages. That looks like this:
|
29
|
+
#
|
30
|
+
# PictureApplier.new {
|
31
|
+
# given('note').matching('ignore').skip
|
32
|
+
# given('shaped graphic content').use(ArgsFromQuotedText.new)
|
33
|
+
# }
|
34
|
+
#
|
35
|
+
# Each declaration sees if it can match a graphical object. If so,
|
36
|
+
# the last action is performed. Declarations are processed in
|
37
|
+
# order, so one match can be given precedence. Consider this:
|
38
|
+
#
|
39
|
+
# PictureApplier.new {
|
40
|
+
# given('note').matching('ignore').skip
|
41
|
+
# given('shaped graphic content').use(ArgsFromQuotedText.new)
|
42
|
+
# }
|
43
|
+
#
|
44
|
+
# This means that shaped graphic objects irrelevant to the test
|
45
|
+
# can be ignored by attaching the appropriate annotation.
|
46
|
+
|
47
|
+
def initialize(&block)
|
48
|
+
@universe = GraphicVolunteerPool.new
|
49
|
+
@mobilized = []
|
50
|
+
instance_eval(&block) if block
|
51
|
+
@mobilized << EagerButUselessVolunteer.new
|
52
|
+
end
|
53
|
+
|
54
|
+
# Claim that the PictureApplier matches graphics matching
|
55
|
+
# the _form_description_, a string. Here are the form descriptions
|
56
|
+
# currently supported:
|
57
|
+
#
|
58
|
+
# * 'shaped graphic content': A shaped graphic is a rectangle, oval,
|
59
|
+
# embedded picture, etc.: anything that is neither a line nor a
|
60
|
+
# group. The content is the text you type in when you double-click
|
61
|
+
# on the object. When given the form description, the PictureApplier
|
62
|
+
# will match any shaped graphic that contains text.
|
63
|
+
# * 'line label content': This matches any line that has a label.
|
64
|
+
# (Note: lines can have more than one label. That's not handled.)
|
65
|
+
# * 'note': Matches any object that contains an annotation. (Annotations are
|
66
|
+
# only available in OmniGraffle Pro. They're arbitrary text
|
67
|
+
# associated with a line or shaped graphic. You can see the
|
68
|
+
# annotations if you hover over the object.)
|
69
|
+
#
|
70
|
+
# The _form_description_ describes the first of two matching steps.
|
71
|
+
# If it matches, it extracts the appropriate text (content or
|
72
|
+
# annotation) and hands it to the next step, described by given.
|
73
|
+
#
|
74
|
+
# This method returns self so that other methods can be chained onto it.
|
75
|
+
def given(form_description)
|
76
|
+
@mobilized << @universe.mobilize_form_watcher(form_description)
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
# Claim that the PictureApplier matches particular text extracted from
|
81
|
+
# a graphical object. These _text_descriptions_ are currently supported:
|
82
|
+
#
|
83
|
+
# * 'text...': match if the text ends in ellipses (ignoring whitespace).
|
84
|
+
# * 'ignore': match if the text ends in the word "ignore" (ignoring whitespace).
|
85
|
+
#
|
86
|
+
# This method returns self so that other methods can be chained onto it.
|
87
|
+
def matching(text_description)
|
88
|
+
@mobilized.last.next = @universe.mobilize_content_watcher(text_description)
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# When a PictureApplier matches an object, the extracted text is
|
93
|
+
# passed to the _text_applier_ to be turned into messages.
|
94
|
+
def use(text_applier)
|
95
|
+
@mobilized.last.text_applier = text_applier
|
96
|
+
end
|
97
|
+
|
98
|
+
# After a PictureApplier matches a graphical object, it's ignored.
|
99
|
+
# This is used to prevent text associated with the object from being
|
100
|
+
# turned into messages.
|
101
|
+
def skip
|
102
|
+
use(TextApplierThatDoesNothing.new)
|
103
|
+
end
|
104
|
+
|
105
|
+
# After the PictureApplier has been programmed, this method
|
106
|
+
# applies it to the _graphics_, resulting in methods that are sent
|
107
|
+
# to the _target_.
|
108
|
+
#
|
109
|
+
# If there's a problem, a log of what was sent to the _target_ is
|
110
|
+
# included in the exception message. The _log_ can be started out with
|
111
|
+
# the third argument, in which case new entries are appended. This can
|
112
|
+
# be useful when a single test is made up of several graphics lists.
|
113
|
+
def apply(graphics, target, log=[])
|
114
|
+
while_logging_for_test_errors(log) do
|
115
|
+
graphics.each { |g| find_volunteer_for(g).apply(g, target) }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Given a _graphic_ find a GraphicVolunteer that can apply it.
|
120
|
+
# (Probably not for external use.)
|
121
|
+
def find_volunteer_for(graphic)
|
122
|
+
@mobilized.find { | m | m.likes?(graphic) }
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def while_logging_for_test_errors(log)
|
128
|
+
Fluid.let(:log, log) do
|
129
|
+
begin
|
130
|
+
yield
|
131
|
+
rescue Test::Unit::AssertionFailedError, StandardError, ScriptError => e
|
132
|
+
new_message = %Q{#{e.message} after\n#{Fluid.log.join("\n")}}
|
133
|
+
new_e = e.exception(new_message)
|
134
|
+
new_e.set_backtrace(e.backtrace)
|
135
|
+
raise new_e
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# This class is deprecated. See PictureApplier.
|
142
|
+
#
|
143
|
+
# A GraphicInterpreter takes a list of _graphics_, some
|
144
|
+
# instructions about how to interpret them, and a _target_.
|
145
|
+
# It interprets the instructions to create messages that it then
|
146
|
+
# sends to the target.
|
147
|
+
#
|
148
|
+
class GraphicInterpreter
|
149
|
+
|
150
|
+
# The _graphics_ are an array of OmniGraffle Graffle::AbstractGraphic objects. The
|
151
|
+
# _instructions_ map a string to an object that must respond to
|
152
|
+
# message_and_args_from. (See AbstractTextApplier#message_and_args_from
|
153
|
+
# for more.)
|
154
|
+
#
|
155
|
+
# Here are the available instructions (keys in _instructions_):
|
156
|
+
#
|
157
|
+
# * ['line labels' => ...]: each line within the (single)
|
158
|
+
# label on the line is to be interpreted as a message-send by an
|
159
|
+
# instance of the given class.
|
160
|
+
# * ['shaped graphic text' => ...]: each line of content
|
161
|
+
# within the graphic is to be interpreted by the given class. The
|
162
|
+
# content is the text you type in an object after double-clicking
|
163
|
+
# on it.
|
164
|
+
# * ['text...' => ...]: a line ending in ellipses is treated this way,
|
165
|
+
# no matter what other instructions say.
|
166
|
+
#
|
167
|
+
def initialize(graphics, instructions = {})
|
168
|
+
puts "Warning: #{self.class} is deprecated. Use PictureApplier instead."
|
169
|
+
@graphics = graphics
|
170
|
+
@line_label_strategy =
|
171
|
+
instructions['line labels']
|
172
|
+
@shaped_graphic_text_strategy =
|
173
|
+
instructions['shaped graphic text']
|
174
|
+
|
175
|
+
@ellipses_override = instructions['text...']
|
176
|
+
end
|
177
|
+
|
178
|
+
# Apply the _graphics_, according to the _instructions_, against
|
179
|
+
# the _target_.
|
180
|
+
def run_against(target)
|
181
|
+
@target = target
|
182
|
+
@log = []
|
183
|
+
@graphics.each do |g|
|
184
|
+
case g.stereotype.basename
|
185
|
+
when "ShapedGraphic"
|
186
|
+
process_commands(@shaped_graphic_text_strategy,
|
187
|
+
g.content.as_lines)
|
188
|
+
when "LineGraphic"
|
189
|
+
process_commands(@line_label_strategy,
|
190
|
+
g.label_lines)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def process_commands(text_applier, command_lines)
|
198
|
+
return if text_applier.nil?
|
199
|
+
command_lines.each do |line|
|
200
|
+
info = if /\.\.\.\s*$/ =~ line && @ellipses_override
|
201
|
+
@ellipses_override.message_and_args_from(line)
|
202
|
+
else
|
203
|
+
text_applier.message_and_args_from(line)
|
204
|
+
end
|
205
|
+
begin
|
206
|
+
@log << readable(*info)
|
207
|
+
@target.send(*info)
|
208
|
+
rescue Test::Unit::AssertionFailedError, StandardError, ScriptError => e
|
209
|
+
new_message = %Q{#{e.message} after\n#{@log.join("\n")}}
|
210
|
+
new_e = e.exception(new_message)
|
211
|
+
new_e.set_backtrace(e.backtrace)
|
212
|
+
raise new_e
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def readable(message, *args)
|
218
|
+
inspected = args.collect { |a| a.inspect }
|
219
|
+
"+ #{message}(#{inspected.join(', ')})"
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
|
@@ -0,0 +1,135 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-21.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
module GraphicalTestsForRails
|
7
|
+
|
8
|
+
# Just a wrapper module so that classes are grouped nicely in rdoc.
|
9
|
+
# You don't have to include this - that happens if you require
|
10
|
+
# graphical_tests_for_rails.rb.
|
11
|
+
module TextAppliers
|
12
|
+
|
13
|
+
# This is the base class for objects that know how to convert text
|
14
|
+
# into a series of messages and send them to some target.
|
15
|
+
class AbstractTextApplier
|
16
|
+
|
17
|
+
# apply is given an array of text (_lines_). It is to convert
|
18
|
+
# that text into messages and send them to _target_.
|
19
|
+
#
|
20
|
+
def apply(lines, target)
|
21
|
+
message_sends = lines.collect {|line| message_and_args_from(line) }
|
22
|
+
message_sends.each do |m|
|
23
|
+
Fluid.log << readable(*m)
|
24
|
+
target.send(*m)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# message_and_args_from is given a string that is derived from some
|
29
|
+
# text. It must answer an array whose first element is a
|
30
|
+
# message name and whose remaining elements are appropriate message
|
31
|
+
# arguments. Must be defined in any subclass.
|
32
|
+
def message_and_args_from; subclass_responsibility; end
|
33
|
+
|
34
|
+
private
|
35
|
+
def readable(message, *args)
|
36
|
+
inspected = args.collect { |a| a.inspect }
|
37
|
+
"+ #{message}(#{inspected.join(', ')})"
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# The arguments to apply are quoted within the _string_.
|
45
|
+
# The message is the text before the first arg, snake-cased.
|
46
|
+
# For example, this:
|
47
|
+
# paul logs in with "john", password "j"
|
48
|
+
# means this:
|
49
|
+
# target.paul_logs_in_with("john", "j")
|
50
|
+
#
|
51
|
+
class ArgsFromQuotedText < AbstractTextApplier
|
52
|
+
|
53
|
+
def message_and_args_from(string)
|
54
|
+
quote_marker = '%<<<==-'
|
55
|
+
|
56
|
+
string = string.gsub(/(\w)'(\w)/, "#{quote_marker}\\1\\2#{quote_marker}")
|
57
|
+
tokens = string.split(/['"],?/, allow_trailing_null_fields = -1)
|
58
|
+
message = tokens[0].strip.downcase
|
59
|
+
args = tokens[1..-1]
|
60
|
+
|
61
|
+
|
62
|
+
message.gsub!(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1\\2")
|
63
|
+
message.gsub!(/\s+/,'_')
|
64
|
+
message.gsub!(/-/, '_')
|
65
|
+
message.gsub!(/\W/, '')
|
66
|
+
|
67
|
+
|
68
|
+
args = args.enum_slice(2).collect { |e| e[0] }
|
69
|
+
args = args.collect do | arg |
|
70
|
+
arg.gsub(/#{quote_marker}(.)(.)#{quote_marker}/, "\\1'\\2")
|
71
|
+
end
|
72
|
+
|
73
|
+
[message] + args
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Like ArgsFromQuotedText, but the first word (typically a name)
|
78
|
+
# is downcased and used as the first argument.
|
79
|
+
# For example, this:
|
80
|
+
# paul logs in with "john", password "j"
|
81
|
+
# means this:
|
82
|
+
# target.logs_in_with("paul", "john", "j")
|
83
|
+
#
|
84
|
+
|
85
|
+
class PrefixNameAndArgsFromQuotedText < ArgsFromQuotedText
|
86
|
+
def message_and_args_from(string)
|
87
|
+
user_claims(string =~ /^\s*(\w+)\s+(.*)$/) {
|
88
|
+
"'#{string}' cannot be split into a name and an action."
|
89
|
+
}
|
90
|
+
name = $1.downcase
|
91
|
+
rest = super($2)
|
92
|
+
message = rest.shift
|
93
|
+
[message, name, *rest]
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# This AbstractTextApplier is created with the name of a message.
|
100
|
+
# When handed a string to apply, it downcases and strips the string,
|
101
|
+
# then uses it as the single argument to the method. For example, this:
|
102
|
+
# applier = TextAsNameOfPage.new("assert_on")
|
103
|
+
# applier.apply("HOME")
|
104
|
+
# turns into this:
|
105
|
+
# applier.assert_on("home")
|
106
|
+
class TextAsNameOfPage < AbstractTextApplier
|
107
|
+
|
108
|
+
# When producing a method call, objects of this class will
|
109
|
+
# prepend the _message_ to
|
110
|
+
# a single argument it constructs.
|
111
|
+
def initialize(message)
|
112
|
+
@message = message
|
113
|
+
end
|
114
|
+
|
115
|
+
# All of the text in the _string_ is expected to be the name
|
116
|
+
# of a page. It is downcased and treated as the single argument to
|
117
|
+
# the message passed to +new+.
|
118
|
+
def message_and_args_from(string)
|
119
|
+
[@message, string.downcase.strip]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class TextApplierThatDoesNothing # :nodoc:
|
124
|
+
def message_and_args_from(string)
|
125
|
+
[:object_id] # no_op
|
126
|
+
end
|
127
|
+
|
128
|
+
def apply(lines, target)
|
129
|
+
# do nothing, don't even bother sending a no-op
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Created by Brian Marick on 2007-07-21.
|
4
|
+
# Copyright (c) 2007. All rights reserved.
|
5
|
+
|
6
|
+
require 'graffle'
|
7
|
+
require 'graphical_tests_for_rails/graphic-volunteers'
|
8
|
+
|
9
|
+
# TODO: Turn these all into one class.
|
10
|
+
module GraphicalTestsForRails
|
11
|
+
|
12
|
+
# A GraphicVolunteerPool describes the ways a PictureApplier
|
13
|
+
# can by programmed with PictureApplier#given statements.
|
14
|
+
# You only care about this if you want to add new ways to use
|
15
|
+
# Graffle pictures in tests.
|
16
|
+
|
17
|
+
class GraphicVolunteerPool
|
18
|
+
def initialize
|
19
|
+
@graphic_maker = FormFocusedTemplate.new
|
20
|
+
@text_maker = ContentFocusedTemplate.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def mobilize_form_watcher(description)
|
24
|
+
@graphic_maker.make_applier(description)
|
25
|
+
end
|
26
|
+
|
27
|
+
def mobilize_content_watcher(description)
|
28
|
+
@text_maker.make_applier(description)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# There are two distinct kinds of Templates only so they
|
34
|
+
# can produce different error messages.
|
35
|
+
class EmptyTemplate # :nodoc:
|
36
|
+
include Graffle
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@templates = {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def proclaim(name)
|
43
|
+
prog1(GraphicVolunteer.new(name)) { | t | @templates[name] = t }
|
44
|
+
end
|
45
|
+
|
46
|
+
def make_applier(name)
|
47
|
+
t = @templates[name]
|
48
|
+
user_claims(t) {
|
49
|
+
"#{name} must be one of #{@templates.keys.inspect}"
|
50
|
+
}
|
51
|
+
t.dup
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class FormFocusedTemplate < EmptyTemplate # :nodoc:
|
56
|
+
def initialize
|
57
|
+
super
|
58
|
+
|
59
|
+
proclaim('shaped graphic content').handles { | graphic |
|
60
|
+
shaped_graphic_with_content?(graphic)
|
61
|
+
}.and_extracts { | graphic |
|
62
|
+
graphic.content.as_lines
|
63
|
+
}
|
64
|
+
|
65
|
+
proclaim('line label content').handles { | graphic |
|
66
|
+
line_graphic_with_label_content?(graphic)
|
67
|
+
}.and_extracts { | graphic |
|
68
|
+
graphic.label_lines
|
69
|
+
}
|
70
|
+
|
71
|
+
proclaim('note').handles { | graphic |
|
72
|
+
graphic_with_note?(graphic)
|
73
|
+
}.and_extracts { | graphic |
|
74
|
+
graphic_note(graphic)
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def labeled_line?(graphic)
|
80
|
+
return false unless graphic.behaves_like?(LineGraphic)
|
81
|
+
graphic.has_label?
|
82
|
+
end
|
83
|
+
def shaped_graphic_with_content?(graphic)
|
84
|
+
graphic.behaves_like?(ShapedGraphic) && graphic.has_content?
|
85
|
+
end
|
86
|
+
|
87
|
+
def line_graphic_with_label_content?(graphic)
|
88
|
+
labeled_line?(graphic) && graphic.label.has_content?
|
89
|
+
end
|
90
|
+
|
91
|
+
def graphic_with_note?(graphic)
|
92
|
+
return true if graphic.behaves_like?(AbstractGraphic) && graphic.has_note?
|
93
|
+
labeled_line?(graphic) && graphic.label.has_note?
|
94
|
+
end
|
95
|
+
|
96
|
+
def graphic_note(graphic)
|
97
|
+
return graphic.note.as_lines if graphic.has_note?
|
98
|
+
return graphic.label.note.as_lines
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
class ContentFocusedTemplate < EmptyTemplate # :nodoc:
|
103
|
+
def initialize
|
104
|
+
super
|
105
|
+
|
106
|
+
proclaim('text...').handles { | lines |
|
107
|
+
lines.join.strip =~ /\.\.+$/
|
108
|
+
}
|
109
|
+
|
110
|
+
proclaim('ignore').handles do | lines |
|
111
|
+
lines.any? { |l| l.strip.ends_with?("ignore") }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|