germinate 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. data/.gitignore +2 -0
  2. data/History.txt +26 -0
  3. data/README.rdoc +152 -0
  4. data/Rakefile +43 -0
  5. data/TODO +140 -0
  6. data/bin/germ +260 -0
  7. data/cucumber.yml +2 -0
  8. data/examples/basic.rb +123 -0
  9. data/examples/short.rb +19 -0
  10. data/features/author-formats-article.feature +111 -0
  11. data/features/author-lists-info.pending_feature +48 -0
  12. data/features/author-publishes-article-source.feature +5 -0
  13. data/features/author-publishes-article.feature +57 -0
  14. data/features/author-republishes-article.feature +5 -0
  15. data/features/author-selects-hunks.feature +26 -0
  16. data/features/author-sets-variables.feature +88 -0
  17. data/features/author-updates-article-source.feature +5 -0
  18. data/features/author-views-stuff.pending_feature +52 -0
  19. data/features/bin/quoter +6 -0
  20. data/features/bin/sorter +4 -0
  21. data/features/example_articles/bracketing.rb +27 -0
  22. data/features/example_articles/escaping.txt +13 -0
  23. data/features/example_articles/excerpt_output.rb +16 -0
  24. data/features/example_articles/hello.rb +9 -0
  25. data/features/example_articles/pipelines.txt +25 -0
  26. data/features/example_articles/regexen.rb +24 -0
  27. data/features/example_articles/sample_offsets.rb +18 -0
  28. data/features/example_articles/specials.rb +19 -0
  29. data/features/example_articles/stderr.rb +10 -0
  30. data/features/example_articles/wrapping.rb +8 -0
  31. data/features/example_output/bracketing.out +23 -0
  32. data/features/example_output/code_samples.txt +186 -0
  33. data/features/example_output/escaping.out +5 -0
  34. data/features/example_output/excerpt_output.out +6 -0
  35. data/features/example_output/hello.txt +1 -0
  36. data/features/example_output/pipelines.out +28 -0
  37. data/features/example_output/regexen.txt +22 -0
  38. data/features/example_output/sample_offsets.txt +15 -0
  39. data/features/example_output/specials.txt +40 -0
  40. data/features/example_output/stderr.out +3 -0
  41. data/features/example_output/wrapping.txt +3 -0
  42. data/features/step_definitions/germinate.rb +42 -0
  43. data/features/support/env.rb +20 -0
  44. data/germinate.gemspec +55 -0
  45. data/lib/germinate.rb +54 -0
  46. data/lib/germinate/application.rb +113 -0
  47. data/lib/germinate/article_editor.rb +20 -0
  48. data/lib/germinate/formatter.rb +119 -0
  49. data/lib/germinate/hunk.rb +183 -0
  50. data/lib/germinate/implicit_insertion.rb +9 -0
  51. data/lib/germinate/insertion.rb +29 -0
  52. data/lib/germinate/librarian.rb +293 -0
  53. data/lib/germinate/origin.rb +5 -0
  54. data/lib/germinate/pipeline.rb +13 -0
  55. data/lib/germinate/publisher.rb +57 -0
  56. data/lib/germinate/reader.rb +266 -0
  57. data/lib/germinate/selector.rb +136 -0
  58. data/lib/germinate/shared_style_attributes.rb +54 -0
  59. data/lib/germinate/shell_process.rb +94 -0
  60. data/lib/germinate/shell_publisher.rb +19 -0
  61. data/lib/germinate/simple_publisher.rb +7 -0
  62. data/lib/germinate/source_file.rb +41 -0
  63. data/lib/germinate/text_transforms.rb +119 -0
  64. data/lib/germinate/transform_process.rb +25 -0
  65. data/lib/germinate/variable.rb +23 -0
  66. data/sample.rb +14 -0
  67. data/spec/germinate/application_spec.rb +31 -0
  68. data/spec/germinate/article_editor_spec.rb +97 -0
  69. data/spec/germinate/code_hunk_spec.rb +73 -0
  70. data/spec/germinate/file_hunk_spec.rb +28 -0
  71. data/spec/germinate/formatter_spec.rb +160 -0
  72. data/spec/germinate/hunk_spec.rb +84 -0
  73. data/spec/germinate/implicit_insertion_spec.rb +33 -0
  74. data/spec/germinate/insertion_spec.rb +19 -0
  75. data/spec/germinate/librarian_spec.rb +555 -0
  76. data/spec/germinate/pipeline_spec.rb +34 -0
  77. data/spec/germinate/process_spec.rb +105 -0
  78. data/spec/germinate/publisher_spec.rb +130 -0
  79. data/spec/germinate/reader_spec.rb +385 -0
  80. data/spec/germinate/selector_spec.rb +121 -0
  81. data/spec/germinate/shell_publisher_spec.rb +61 -0
  82. data/spec/germinate/source_file_spec.rb +99 -0
  83. data/spec/germinate/text_hunk_spec.rb +98 -0
  84. data/spec/germinate/text_transforms_spec.rb +242 -0
  85. data/spec/germinate/transform_process_spec.rb +50 -0
  86. data/spec/germinate/variable_spec.rb +14 -0
  87. data/spec/germinate_spec.rb +8 -0
  88. data/spec/spec.opts +1 -0
  89. data/spec/spec_helper.rb +16 -0
  90. data/tasks/ann.rake +80 -0
  91. data/tasks/bones.rake +20 -0
  92. data/tasks/cucumber.rake +5 -0
  93. data/tasks/gem.rake +201 -0
  94. data/tasks/git.rake +40 -0
  95. data/tasks/notes.rake +27 -0
  96. data/tasks/post_load.rake +34 -0
  97. data/tasks/rdoc.rake +51 -0
  98. data/tasks/rubyforge.rake +55 -0
  99. data/tasks/setup.rb +292 -0
  100. data/tasks/spec.rake +54 -0
  101. data/tasks/svn.rake +47 -0
  102. data/tasks/test.rake +40 -0
  103. data/tasks/zentest.rake +36 -0
  104. data/test/test_germinate.rb +0 -0
  105. metadata +228 -0
@@ -0,0 +1,119 @@
1
+ require 'alter_ego'
2
+
3
+ # Obsolete
4
+ class Germinate::Formatter
5
+ include AlterEgo
6
+
7
+ attr_accessor :comment_prefix
8
+
9
+ def initialize(output=$stdio)
10
+ @output = output
11
+ end
12
+
13
+ state :initial, :default => true do
14
+ transition :to => :code, :on => :start!
15
+ end
16
+
17
+ state :code do
18
+ handle :add_line!, :add_code_line!
19
+
20
+ transition :to => :paragraph, :on => :paragraph!
21
+ transition :to => :finished, :on => :finish!
22
+ end
23
+
24
+ state :paragraph do
25
+ handle :add_line!, :add_paragraph_line!
26
+
27
+ transition :to => :linebreak, :on => :linebreak!
28
+ transition :to => :code, :on => :code!
29
+ transition :to => :finished, :on => :finish!
30
+
31
+ on_exit do
32
+ flush_paragraph!
33
+ end
34
+ end
35
+
36
+ state :linebreak do
37
+ handle :add_line!, :add_linebreak_line!
38
+
39
+ transition :to => :paragraph, :on => :paragraph! do
40
+ emit!("\n")
41
+ end
42
+
43
+ transition :to => :code, :on => :code!
44
+ transition :to => :finished, :on => :finish!
45
+
46
+ end
47
+
48
+ state :finished do
49
+
50
+ end
51
+
52
+ protected
53
+
54
+ def add_code_line!(line)
55
+ case line
56
+ when /\s*(\S+)?\s*:TEXT:/ then
57
+ self.comment_prefix = $1
58
+ paragraph!
59
+ end
60
+ end
61
+
62
+ def add_paragraph_line!(line)
63
+ case line
64
+ when /:CUT:/
65
+ code!
66
+ when text_pattern
67
+ paragraph_buffer << $1.chomp
68
+ when whitespace_pattern
69
+ linebreak!
70
+ end
71
+ end
72
+
73
+ def add_linebreak_line!(line)
74
+ case line
75
+ when /:CUT:/
76
+ code!
77
+ when text_pattern
78
+ paragraph_buffer << $1.chomp
79
+ paragraph!
80
+ else
81
+ # NOOP
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ attr_reader :output
88
+
89
+ def text_pattern
90
+ if comment_prefix
91
+ /^\s*#{comment_prefix}+\s*(\S+.*)$/
92
+ else
93
+ /^\s*(\S+.*)\s*$/
94
+ end
95
+ end
96
+
97
+ def whitespace_pattern
98
+ if comment_prefix
99
+ /^\s*#{comment_prefix}*\s*$/
100
+ else
101
+ /^\s*$/
102
+ end
103
+ end
104
+
105
+ def paragraph_buffer
106
+ @paragraph_buffer ||= []
107
+ end
108
+
109
+ def flush_paragraph!
110
+ emit!(paragraph_buffer.join(" "))
111
+ paragraph_buffer.clear
112
+ end
113
+
114
+ def emit!(text)
115
+ unless text.empty?
116
+ output.puts(text.chomp)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,183 @@
1
+ require 'pathname'
2
+ require 'ick'
3
+ require 'fattr'
4
+ require File.expand_path("shared_style_attributes", File.dirname(__FILE__))
5
+
6
+ # A Hunk represents a chunk of content. There are different types of Hunk, like
7
+ # Code or Text, which may be formatted differently by the Formatter. At its
8
+ # most basic a Hunk is just a list of Strings, each one representing a single
9
+ # line.
10
+ class Germinate::Hunk < ::Array
11
+ include Germinate::SharedStyleAttributes
12
+ Ick::Returning.belongs_to(self)
13
+
14
+ def initialize(contents=[], template = {})
15
+ super(contents)
16
+ copy_shared_style_attributes_from(template)
17
+ end
18
+
19
+ def to_s
20
+ "#{self.class.name}[#{origin}](#{self.size} lines)"
21
+ end
22
+
23
+ # return a copy with leading and trailing whitespace lines removed
24
+ def strip
25
+ Germinate::TextTransforms.strip_blanks.call(self)
26
+ end
27
+
28
+ def resolve_insertions
29
+ dup.map!{ |line_or_insertion|
30
+ if line_or_insertion.respond_to?(:resolve)
31
+ line_or_insertion.resolve
32
+ else
33
+ line_or_insertion
34
+ end
35
+ }
36
+ end
37
+
38
+ def format_with(formatter)
39
+ raise "Unresolved hunk cannot be formatted!" unless resolved?
40
+ if nested_hunks?
41
+ group_hunks.each do |hunk|
42
+ hunk.format_with(formatter)
43
+ end
44
+ else
45
+ yield formatter
46
+ end
47
+ end
48
+
49
+ def inspect
50
+ attrs = Germinate::SharedStyleAttributes.fattrs.inject({}) {|attrs, key|
51
+ attrs[key] = send(key)
52
+ attrs
53
+ }
54
+ "#{self.class}:#{super}:#{attrs.inspect}:#{object_id}"
55
+ end
56
+
57
+ def [](*args)
58
+ returning(super) do |slice|
59
+ if slice.kind_of?(Germinate::Hunk)
60
+ slice.copy_shared_style_attributes_from(self)
61
+ end
62
+ end
63
+ end
64
+
65
+ def slice(*args)
66
+ returning(super) do |slice|
67
+ if slice.kind_of?(Germinate::Hunk)
68
+ slice.copy_shared_style_attributes_from(self)
69
+ end
70
+ end
71
+ end
72
+
73
+ def index_matching(pattern, start_index=0)
74
+ (start_index...(size)).each { |i|
75
+ return i if pattern === self[i]
76
+ }
77
+ nil
78
+ end
79
+
80
+ def whole_file?
81
+ false
82
+ end
83
+
84
+ private
85
+
86
+ def resolved?
87
+ !unresolved_hunks?
88
+ end
89
+
90
+ def unresolved_hunks?
91
+ any?{|line| line.respond_to?(:resolve)}
92
+ end
93
+
94
+ def nested?
95
+ unresolved_hunks? || nested_hunks?
96
+ end
97
+
98
+ def nested_hunks?
99
+ any?{|line| line.respond_to?(:format_with)}
100
+ end
101
+
102
+ def group_hunks
103
+ return self unless nested?
104
+ groups = inject([empty_dup]) { |hunks, line_or_hunk|
105
+ if line_or_hunk.respond_to?(:format_with)
106
+ hunks << line_or_hunk
107
+ hunks << empty_dup
108
+ else
109
+ hunks.last << line_or_hunk
110
+ end
111
+ hunks
112
+ }
113
+ groups.delete_if{|g| g.empty?}
114
+ groups
115
+ end
116
+
117
+ # An empty duplicate retains metadata but has no lines
118
+ def empty_dup
119
+ returning(dup) do |duplicate|
120
+ duplicate.clear
121
+ end
122
+ end
123
+
124
+ end
125
+
126
+ # Represents a hunk of article text
127
+ class Germinate::TextHunk < Germinate::Hunk
128
+ def initialize(contents = [], template = {})
129
+ self.join_lines = true
130
+ self.strip_blanks = true
131
+ self.rstrip_lines = true
132
+ self.uncomment = true
133
+ self.expand_insertions = true
134
+ self.flatten_nested = true
135
+ super
136
+ end
137
+
138
+ def format_with(formatter)
139
+ super(formatter) do |formatter|
140
+ formatter.format_text!(self, comment_prefix)
141
+ end
142
+ end
143
+ end
144
+
145
+ # Represents a hunk of source code
146
+ class Germinate::CodeHunk < Germinate::Hunk
147
+ def initialize(contents = [], template = {})
148
+ self.strip_blanks = true
149
+ self.bracket = true
150
+ super
151
+ end
152
+
153
+ def code_open_bracket=(new_value)
154
+ super
155
+ end
156
+
157
+ def code_close_bracket=(new_value)
158
+ super
159
+ end
160
+
161
+
162
+ def format_with(formatter)
163
+ super(formatter) do |formatter|
164
+ formatter.format_code!(self, comment_prefix)
165
+ end
166
+ end
167
+ end
168
+
169
+ class Germinate::NullHunk < Germinate::Hunk
170
+ end
171
+
172
+ # Represents a the entire text of a file on disk
173
+ class Germinate::FileHunk < Germinate::CodeHunk
174
+ def initialize(lines, template)
175
+ super(lines, template)
176
+ raise ArgumentError, "Path required" if source_path.nil?
177
+ end
178
+
179
+ def whole_file?
180
+ true
181
+ end
182
+ end
183
+
@@ -0,0 +1,9 @@
1
+ require File.expand_path("insertion", File.dirname(__FILE__))
2
+
3
+ class Germinate::ImplicitInsertion < Germinate::Insertion
4
+ def resolve
5
+ super
6
+ rescue IndexError
7
+ Germinate::NullHunk.new
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ require 'fattr'
2
+ require 'ick'
3
+ require File.expand_path("shared_style_attributes", File.dirname(__FILE__))
4
+
5
+ class Germinate::Insertion
6
+ include Germinate::SharedStyleAttributes
7
+ Ick::Returning.belongs_to(self)
8
+
9
+ attr_reader :library
10
+ attr_reader :selector
11
+
12
+ fattr(:log) { Germinate.logger }
13
+
14
+ def initialize(selector, library, template={})
15
+ copy_shared_style_attributes_from(template)
16
+ @selector = selector
17
+ @library = library
18
+ end
19
+
20
+ def to_s
21
+ "Insertion[#{selector}]"
22
+ end
23
+
24
+ def resolve
25
+ returning(library[selector, self, self]) do |hunk|
26
+ log.debug "Resolved #{self} to #{hunk}"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,293 @@
1
+ require 'orderedhash'
2
+ require 'fattr'
3
+ require 'ick'
4
+ require File.expand_path("shared_style_attributes", File.dirname(__FILE__))
5
+
6
+ # The Librarian is responsible for organizing all the chunks of content derived
7
+ # from reading a source file and making them available for later re-assembly and
8
+ # formatting.
9
+ class Germinate::Librarian
10
+ include Germinate::SharedStyleAttributes
11
+ Ick::Returning.belongs_to(self)
12
+
13
+ class VariableStore < OrderedHash
14
+ def initialize(librarian)
15
+ super()
16
+ @librarian = librarian
17
+ end
18
+
19
+ def []=(key, value)
20
+ if key?(key)
21
+ variable = fetch(key)
22
+ variable.replace(value.to_s)
23
+ variable.update_source_line!(@librarian.comment_prefix)
24
+ else
25
+ variable =
26
+ case value
27
+ when Germinate::Variable
28
+ value
29
+ else
30
+ line_number = @librarian.lines.length + 1
31
+ line = ""
32
+ Germinate::Variable.new(
33
+ key, value, line, @librarian.source_path, line_number)
34
+ end
35
+ variable.update_source_line!(@librarian.comment_prefix)
36
+ store(key, variable)
37
+ @librarian.log.debug "Appending #{variable.line.inspect} to lines"
38
+ @librarian.lines << variable.line
39
+ end
40
+ @librarian.updated = true
41
+ end
42
+
43
+ end
44
+
45
+ attr_reader :lines
46
+ attr_reader :text_lines
47
+ attr_reader :code_lines
48
+ attr_reader :front_matter_lines
49
+
50
+ fattr :source_path => nil
51
+
52
+ fattr(:log) { Germinate.logger }
53
+ fattr(:variables) { VariableStore.new(self) }
54
+ fattr(:updated) { false }
55
+ fattr(:source_file) { Germinate::SourceFile.new(source_path) }
56
+
57
+ def initialize
58
+ @lines = []
59
+ @text_lines = []
60
+ @code_lines = []
61
+ @front_matter_lines = []
62
+ @sections = OrderedHash.new do |hash, key|
63
+ hash[key] = Germinate::TextHunk.new([], shared_style_attributes)
64
+ end
65
+ @samples = OrderedHash.new do |hash, key|
66
+ hash[key] = Germinate::CodeHunk.new([], shared_style_attributes)
67
+ end
68
+ @processes = {'_transform' => Germinate::TransformProcess.new}
69
+ @publishers = OrderedHash.new
70
+ end
71
+
72
+ def add_front_matter!(line)
73
+ add_line!(line)
74
+ @front_matter_lines << line
75
+ end
76
+
77
+ def add_control!(line)
78
+ add_line!(line)
79
+ end
80
+
81
+ def add_text!(section, line)
82
+ add_line!(line)
83
+ @text_lines << line
84
+ @sections[section] << line
85
+ end
86
+
87
+ def add_code!(sample, line)
88
+ add_line!(line)
89
+ @code_lines << line
90
+ @samples[sample] << line
91
+ end
92
+
93
+ def add_insertion!(section, selector, attributes)
94
+ insertion = Germinate::Insertion.new(selector, self, attributes)
95
+ @sections[section] << insertion
96
+ @text_lines << insertion
97
+ end
98
+
99
+ def set_code_attributes!(sample, attributes)
100
+ attributes.each_pair do |key, value|
101
+ @samples[sample].send(key, value) unless value.nil?
102
+ end
103
+ end
104
+
105
+ def add_process!(process_name, command)
106
+ @processes[process_name] =
107
+ Germinate::ShellProcess.new(process_name, command, variables)
108
+ end
109
+
110
+ def add_publisher!(name, identifier, options)
111
+ @publishers[name] = Germinate::Publisher.make(name, identifier, self, options)
112
+ end
113
+
114
+ def store_changes!
115
+ source_file.write!(lines)
116
+ end
117
+
118
+ def set_variable!(line, line_number, name, value)
119
+ variables.store(name,Germinate::Variable.new(name, value, line, source_path, line_number))
120
+ end
121
+
122
+ def comment_prefix_known?
123
+ !comment_prefix.nil?
124
+ end
125
+
126
+ def section(section_name)
127
+ unless has_section?(section_name)
128
+ raise IndexError,
129
+ "No text section named '#{section_name}'. "\
130
+ "Known sections: #{@sections.keys.join(', ')}"
131
+ end
132
+ Array(@sections[section_name])
133
+ end
134
+
135
+ def has_section?(section_name)
136
+ @sections.key?(section_name)
137
+ end
138
+
139
+ def sample(sample_name)
140
+ unless has_sample?(sample_name)
141
+ raise IndexError,
142
+ "No code sample named '#{sample_name}'. "\
143
+ "Known samples: #{@samples.keys.join(', ')}"
144
+ end
145
+ Array(@samples[sample_name])
146
+ end
147
+
148
+ # Fetch a process by name
149
+ def process(process_name)
150
+ @processes.fetch(process_name)
151
+ rescue IndexError => error
152
+ raise error.exception("Unknown process #{process_name.inspect}")
153
+ end
154
+
155
+ def process_names
156
+ @processes.keys
157
+ end
158
+
159
+ def publisher_names
160
+ @publishers.keys
161
+ end
162
+
163
+ # fetch a publisher by name
164
+ def publisher(publisher_name)
165
+ @publishers.fetch(publisher_name)
166
+ rescue IndexError => error
167
+ raise error.exception("Unknown publisher #{publisher_name.inspect}")
168
+ end
169
+
170
+ def has_sample?(sample_name)
171
+ @samples.key?(sample_name)
172
+ end
173
+
174
+ # TODO Too big, refactor.
175
+ def [](selector, origin="<Unknown>", template={})
176
+ log.debug "Selecting #{selector}, from #{origin}"
177
+ selector = case selector
178
+ when Germinate::Selector then selector
179
+ else Germinate::Selector.new(selector, "SECTION0", origin)
180
+ end
181
+ sample =
182
+ case selector.selector_type
183
+ when :code then
184
+ sample(selector.key)
185
+ when :special then
186
+ case selector.key
187
+ when "SOURCE"
188
+ source_hunk =
189
+ if selector.whole?
190
+ Germinate::FileHunk.new(lines, self)
191
+ else
192
+ Germinate::CodeHunk.new(lines, self)
193
+ end
194
+ source_hunk.disable_all_transforms!
195
+ source_hunk
196
+ when "CODE" then Germinate::CodeHunk.new(code_lines, self)
197
+ when "TEXT" then Germinate::TextHunk.new(text_lines, self)
198
+ else raise "Unknown special section '$#{selector.key}'"
199
+ end
200
+ else
201
+ raise Exception,
202
+ "Unknown selector type #{selector.selector_type.inspect}"
203
+ end
204
+
205
+ sample.copy_shared_style_attributes_from(template)
206
+ sample.origin.source_path ||= source_path
207
+ sample.origin.selector ||= selector
208
+
209
+ sample = if selector.excerpt_output?
210
+ excerpt(execute_pipeline(sample, selector.pipeline), selector)
211
+ else
212
+ execute_pipeline(excerpt(sample, selector), selector.pipeline)
213
+ end
214
+ sample
215
+ end
216
+
217
+ def section_names
218
+ @sections.keys
219
+ end
220
+
221
+ def sample_names
222
+ @samples.keys
223
+ end
224
+
225
+ # Given a list of process names or a '|'-delimited string, return a Pipeline
226
+ # object representing a super-process of all the named processes chained
227
+ # together.
228
+ def make_pipeline(process_names_or_string)
229
+ names =
230
+ if process_names_or_string.kind_of?(String)
231
+ process_names_or_string.split("|")
232
+ else
233
+ process_names_or_string
234
+ end
235
+ processes = names.map{|n| process(n)}
236
+ Germinate::Pipeline.new(processes)
237
+ end
238
+
239
+ private
240
+
241
+ def excerpt(sample, selector)
242
+ # TODO make excerpting just another TextTransform
243
+ start_offset = start_offset(sample, selector)
244
+ end_offset = end_offset(sample, selector, start_offset)
245
+ case selector.delimiter
246
+ when '..' then sample[start_offset..end_offset]
247
+ when '...' then sample[start_offset...end_offset]
248
+ when ',' then sample[start_offset, selector.length]
249
+ when nil then sample.dup.replace([sample[start_offset]])
250
+ else raise "Don't understand delimiter #{selector.delimiter.inspect}"
251
+ end
252
+ end
253
+
254
+ def add_line!(line)
255
+ line.chomp!
256
+ line << "\n"
257
+ @lines << line
258
+ end
259
+
260
+ def start_offset(hunk, selector)
261
+ offset = selector.start_offset_for_slice
262
+ case offset
263
+ when Integer then offset
264
+ when Regexp then
265
+ returning(hunk.index_matching(offset)) do |index|
266
+ if index.nil?
267
+ raise "Cannot find line matching #{offset.inspect} in #{selector}"
268
+ end
269
+ end
270
+ else
271
+ raise "Don't know how to use #{offset.inspect} as an offset"
272
+ end
273
+ end
274
+
275
+ def end_offset(hunk, selector, start_offset)
276
+ offset = selector.end_offset_for_slice
277
+ case offset
278
+ when Integer, nil then offset
279
+ when Regexp then
280
+ returning(hunk.index_matching(offset, start_offset)) do |index|
281
+ if index.nil?
282
+ raise "Cannot find line matching #{offset.inspect} in #{selector}"
283
+ end
284
+ end
285
+ else
286
+ raise "Don't know how to use #{offset.inspect} as an offset"
287
+ end
288
+ end
289
+
290
+ def execute_pipeline(hunk, names)
291
+ make_pipeline(names).call(hunk)
292
+ end
293
+ end