graffle 0.1.8 → 0.1.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|