devver-germinate 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. data/History.txt +12 -0
  2. data/README.rdoc +26 -4
  3. data/TODO +80 -8
  4. data/bin/germ +162 -38
  5. data/examples/basic.rb +14 -9
  6. data/examples/short.rb +2 -0
  7. data/features/author-formats-article.feature +3 -3
  8. data/features/{author-lists-info.feature → author-lists-info.pending_feature} +3 -0
  9. data/features/author-publishes-article.feature +52 -0
  10. data/features/author-selects-hunks.feature +1 -1
  11. data/features/author-sets-variables.feature +88 -0
  12. data/features/{author-views-stuff.feature → author-views-stuff.pending_feature} +4 -0
  13. data/features/example_articles/escaping.txt +1 -0
  14. data/features/example_articles/specials.rb +3 -3
  15. data/features/example_output/specials.txt +9 -5
  16. data/features/step_definitions/germinate.rb +9 -0
  17. data/germinate.gemspec +3 -3
  18. data/lib/germinate.rb +1 -1
  19. data/lib/germinate/application.rb +82 -31
  20. data/lib/germinate/hunk.rb +20 -0
  21. data/lib/germinate/insertion.rb +10 -2
  22. data/lib/germinate/librarian.rb +129 -31
  23. data/lib/germinate/origin.rb +5 -0
  24. data/lib/germinate/pipeline.rb +2 -0
  25. data/lib/germinate/publisher.rb +57 -0
  26. data/lib/germinate/reader.rb +51 -8
  27. data/lib/germinate/selector.rb +18 -6
  28. data/lib/germinate/shared_style_attributes.rb +18 -1
  29. data/lib/germinate/{process.rb → shell_process.rb} +27 -8
  30. data/lib/germinate/shell_publisher.rb +19 -0
  31. data/lib/germinate/simple_publisher.rb +7 -0
  32. data/lib/germinate/source_file.rb +41 -0
  33. data/lib/germinate/text_transforms.rb +38 -9
  34. data/lib/germinate/transform_process.rb +25 -0
  35. data/lib/germinate/variable.rb +23 -0
  36. data/sample.rb +14 -0
  37. data/spec/germinate/application_spec.rb +18 -1
  38. data/spec/germinate/article_editor_spec.rb +3 -3
  39. data/spec/germinate/code_hunk_spec.rb +28 -0
  40. data/spec/germinate/file_hunk_spec.rb +1 -0
  41. data/spec/germinate/hunk_spec.rb +1 -0
  42. data/spec/germinate/insertion_spec.rb +2 -1
  43. data/spec/germinate/librarian_spec.rb +280 -85
  44. data/spec/germinate/pipeline_spec.rb +10 -0
  45. data/spec/germinate/process_spec.rb +31 -6
  46. data/spec/germinate/publisher_spec.rb +130 -0
  47. data/spec/germinate/reader_spec.rb +58 -2
  48. data/spec/germinate/selector_spec.rb +34 -14
  49. data/spec/germinate/shell_publisher_spec.rb +61 -0
  50. data/spec/germinate/source_file_spec.rb +99 -0
  51. data/spec/germinate/text_hunk_spec.rb +45 -0
  52. data/spec/germinate/text_transforms_spec.rb +90 -2
  53. data/spec/germinate/transform_process_spec.rb +50 -0
  54. data/spec/germinate/variable_spec.rb +14 -0
  55. metadata +19 -7
  56. data/lib/germinate/article_formatter.rb +0 -75
  57. data/spec/germinate/article_formatter_spec.rb +0 -153
@@ -16,6 +16,10 @@ class Germinate::Hunk < ::Array
16
16
  copy_shared_style_attributes_from(template)
17
17
  end
18
18
 
19
+ def to_s
20
+ "#{self.class.name}[#{origin}](#{self.size} lines)"
21
+ end
22
+
19
23
  # return a copy with leading and trailing whitespace lines removed
20
24
  def strip
21
25
  Germinate::TextTransforms.strip_blanks.call(self)
@@ -121,6 +125,16 @@ end
121
125
 
122
126
  # Represents a hunk of article text
123
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
+
124
138
  def format_with(formatter)
125
139
  super(formatter) do |formatter|
126
140
  formatter.format_text!(self, comment_prefix)
@@ -130,6 +144,12 @@ end
130
144
 
131
145
  # Represents a hunk of source code
132
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
+
133
153
  def code_open_bracket=(new_value)
134
154
  super
135
155
  end
@@ -1,21 +1,29 @@
1
1
  require 'fattr'
2
+ require 'ick'
2
3
  require File.expand_path("shared_style_attributes", File.dirname(__FILE__))
3
4
 
4
5
  class Germinate::Insertion
5
6
  include Germinate::SharedStyleAttributes
7
+ Ick::Returning.belongs_to(self)
6
8
 
7
9
  attr_reader :library
8
10
  attr_reader :selector
9
11
 
12
+ fattr(:log) { Germinate.logger }
13
+
10
14
  def initialize(selector, library, template={})
11
15
  copy_shared_style_attributes_from(template)
12
16
  @selector = selector
13
17
  @library = library
14
18
  end
15
19
 
20
+ def to_s
21
+ "Insertion[#{selector}]"
22
+ end
23
+
16
24
  def resolve
17
- returning(library[selector]) do |hunk|
18
- hunk.copy_shared_style_attributes_from(self, false)
25
+ returning(library[selector, self, self]) do |hunk|
26
+ log.debug "Resolved #{self} to #{hunk}"
19
27
  end
20
28
  end
21
29
  end
@@ -1,5 +1,6 @@
1
1
  require 'orderedhash'
2
2
  require 'fattr'
3
+ require 'ick'
3
4
  require File.expand_path("shared_style_attributes", File.dirname(__FILE__))
4
5
 
5
6
  # The Librarian is responsible for organizing all the chunks of content derived
@@ -7,7 +8,40 @@ require File.expand_path("shared_style_attributes", File.dirname(__FILE__))
7
8
  # formatting.
8
9
  class Germinate::Librarian
9
10
  include Germinate::SharedStyleAttributes
11
+ Ick::Returning.belongs_to(self)
10
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
+
11
45
  attr_reader :lines
12
46
  attr_reader :text_lines
13
47
  attr_reader :code_lines
@@ -16,6 +50,9 @@ class Germinate::Librarian
16
50
  fattr :source_path => nil
17
51
 
18
52
  fattr(:log) { Germinate.logger }
53
+ fattr(:variables) { VariableStore.new(self) }
54
+ fattr(:updated) { false }
55
+ fattr(:source_file) { Germinate::SourceFile.new(source_path) }
19
56
 
20
57
  def initialize
21
58
  @lines = []
@@ -28,7 +65,8 @@ class Germinate::Librarian
28
65
  @samples = OrderedHash.new do |hash, key|
29
66
  hash[key] = Germinate::CodeHunk.new([], shared_style_attributes)
30
67
  end
31
- @processes = {}
68
+ @processes = {'_transform' => Germinate::TransformProcess.new}
69
+ @publishers = OrderedHash.new
32
70
  end
33
71
 
34
72
  def add_front_matter!(line)
@@ -55,6 +93,7 @@ class Germinate::Librarian
55
93
  def add_insertion!(section, selector, attributes)
56
94
  insertion = Germinate::Insertion.new(selector, self, attributes)
57
95
  @sections[section] << insertion
96
+ @text_lines << insertion
58
97
  end
59
98
 
60
99
  def set_code_attributes!(sample, attributes)
@@ -64,7 +103,20 @@ class Germinate::Librarian
64
103
  end
65
104
 
66
105
  def add_process!(process_name, command)
67
- @processes[process_name] = Germinate::Process.new(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))
68
120
  end
69
121
 
70
122
  def comment_prefix_known?
@@ -96,52 +148,70 @@ class Germinate::Librarian
96
148
  # Fetch a process by name
97
149
  def process(process_name)
98
150
  @processes.fetch(process_name)
151
+ rescue IndexError => error
152
+ raise error.exception("Unknown process #{process_name.inspect}")
99
153
  end
100
154
 
101
155
  def process_names
102
156
  @processes.keys
103
157
  end
104
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
+
105
170
  def has_sample?(sample_name)
106
171
  @samples.key?(sample_name)
107
172
  end
108
173
 
109
- def [](selector)
174
+ # TODO Too big, refactor.
175
+ def [](selector, origin="<Unknown>", template={})
176
+ log.debug "Selecting #{selector}, from #{origin}"
110
177
  selector = case selector
111
178
  when Germinate::Selector then selector
112
- else Germinate::Selector.new(selector, "SECTION0")
179
+ else Germinate::Selector.new(selector, "SECTION0", origin)
113
180
  end
114
181
  sample =
115
182
  case selector.selector_type
116
- when :code then sample(selector.key)
183
+ when :code then
184
+ sample(selector.key)
117
185
  when :special then
118
186
  case selector.key
119
187
  when "SOURCE"
120
- if selector.whole?
121
- Germinate::FileHunk.new(lines, self)
122
- else
123
- Germinate::CodeHunk.new(lines, self)
124
- end
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
125
196
  when "CODE" then Germinate::CodeHunk.new(code_lines, self)
126
- when "TEXT" then Germinate::CodeHunk.new(text_lines, self)
197
+ when "TEXT" then Germinate::TextHunk.new(text_lines, self)
127
198
  else raise "Unknown special section '$#{selector.key}'"
128
199
  end
129
200
  else
130
201
  raise Exception,
131
202
  "Unknown selector type #{selector.selector_type.inspect}"
132
203
  end
133
-
134
- sample = execute_pipeline(sample, selector.pipeline)
135
204
 
136
- start_offset = start_offset(sample, selector)
137
- end_offset = end_offset(sample, selector, start_offset)
138
- case selector.delimiter
139
- when '..' then sample[start_offset..end_offset]
140
- when '...' then sample[start_offset...end_offset]
141
- when ',' then sample[start_offset, selector.length]
142
- when nil then sample.dup.replace([sample[start_offset]])
143
- else raise "Don't understand delimiter #{selector.delimiter.inspect}"
144
- end
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
145
215
  end
146
216
 
147
217
  def section_names
@@ -152,9 +222,38 @@ class Germinate::Librarian
152
222
  @samples.keys
153
223
  end
154
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
+
155
239
  private
156
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
+
157
254
  def add_line!(line)
255
+ line.chomp!
256
+ line << "\n"
158
257
  @lines << line
159
258
  end
160
259
 
@@ -163,9 +262,9 @@ class Germinate::Librarian
163
262
  case offset
164
263
  when Integer then offset
165
264
  when Regexp then
166
- returning(hunk.index_matching(offset)) do |offset|
167
- if offset.nil?
168
- raise "Cannot find line matching #{offset.inspect}"
265
+ returning(hunk.index_matching(offset)) do |index|
266
+ if index.nil?
267
+ raise "Cannot find line matching #{offset.inspect} in #{selector}"
169
268
  end
170
269
  end
171
270
  else
@@ -178,9 +277,9 @@ class Germinate::Librarian
178
277
  case offset
179
278
  when Integer, nil then offset
180
279
  when Regexp then
181
- returning(hunk.index_matching(offset, start_offset)) do |offset|
182
- if offset.nil?
183
- raise "Cannot find line matching #{offset.inspect}"
280
+ returning(hunk.index_matching(offset, start_offset)) do |index|
281
+ if index.nil?
282
+ raise "Cannot find line matching #{offset.inspect} in #{selector}"
184
283
  end
185
284
  end
186
285
  else
@@ -188,8 +287,7 @@ class Germinate::Librarian
188
287
  end
189
288
  end
190
289
 
191
- def execute_pipeline(hunk, process_names)
192
- processes = process_names.map{|n| process(n)}
193
- Germinate::Pipeline.new(processes).call(hunk)
290
+ def execute_pipeline(hunk, names)
291
+ make_pipeline(names).call(hunk)
194
292
  end
195
293
  end
@@ -0,0 +1,5 @@
1
+ Germinate::Origin = Struct.new(:source_path, :line_number, :selector) do
2
+ def to_s
3
+ "#{source_path}:#{line_number}:#{selector}"
4
+ end
5
+ end
@@ -1,4 +1,6 @@
1
1
  class Germinate::Pipeline
2
+ attr_reader :processes
3
+
2
4
  def initialize(processes)
3
5
  @processes = processes
4
6
  end
@@ -0,0 +1,57 @@
1
+ require 'fattr'
2
+ require File.expand_path("publisher", File.dirname(__FILE__))
3
+
4
+ class Germinate::Publisher
5
+ Fattr :identifier
6
+ Fattr :registered_publishers => {}
7
+
8
+ fattr :name
9
+ fattr :librarian
10
+ fattr :options
11
+
12
+ fattr(:pipeline)
13
+ fattr(:log) { Germinate.logger }
14
+ fattr(:selector)
15
+
16
+ @registered_publishers = {}
17
+
18
+ def self.make(name, identifier, librarian, options)
19
+ @registered_publishers.fetch(identifier).new(name, librarian, options)
20
+ rescue IndexError => error
21
+ raise error.exception("Unknown publisher type #{identifier.inspect}")
22
+ end
23
+
24
+ def self.register_publisher_type(identifier, klass)
25
+ @registered_publishers[identifier] = klass
26
+ end
27
+
28
+ def self.identifier(*args)
29
+ if args.empty?
30
+ @identifier
31
+ else
32
+ id = args.first
33
+ self.identifier = id
34
+ Germinate::Publisher.register_publisher_type(id, self)
35
+ end
36
+ end
37
+
38
+ def initialize(name, librarian, options={})
39
+ self.name = name
40
+ self.librarian = librarian
41
+ self.options = options
42
+ self.pipeline = librarian.make_pipeline(options.delete(:pipeline){""})
43
+ self.selector = options.delete(:selector) {
44
+ options.delete(:select){"$TEXT|_transform"}
45
+ }
46
+
47
+ # All options should have been removed by this point
48
+ options.keys.each do |key|
49
+ log.warn "Unknown publisher option '#{key}'"
50
+ end
51
+ end
52
+
53
+ def input
54
+ source = librarian[selector, "publish #{name} command"]
55
+ pipeline.call(source)
56
+ end
57
+ end
@@ -1,4 +1,5 @@
1
1
  require 'alter_ego'
2
+ require 'ick'
2
3
  require 'forwardable'
3
4
  require 'fattr'
4
5
 
@@ -9,8 +10,9 @@ require 'fattr'
9
10
  class Germinate::Reader
10
11
  include AlterEgo
11
12
  extend Forwardable
13
+ Ick::Returning.belongs_to(self)
12
14
 
13
- CONTROL_PATTERN = /^\s*([^\s\\]+)?\s*:([A-Z0-9_]+):\s*(.*)?\s*$/
15
+ CONTROL_PATTERN = /^(\s*([^\s\\]+)?\s*):([A-Z0-9_]+):\s*(.*)?\s*$/
14
16
 
15
17
  attr_reader :librarian
16
18
  attr_reader :current_section
@@ -40,6 +42,12 @@ class Germinate::Reader
40
42
  state :code do
41
43
  handle :add_line!, :add_code!
42
44
 
45
+ on_enter do
46
+ if librarian.section_names.include?(sample_name)
47
+ librarian.add_insertion!(sample_name, "@#{sample_name}", {})
48
+ end
49
+ end
50
+
43
51
  transition :to => :text, :on => :text!
44
52
  transition :to => :code, :on => :code!
45
53
  transition :to => :finished, :on => :finish!
@@ -61,7 +69,7 @@ class Germinate::Reader
61
69
  @librarian = librarian
62
70
  @section_count = 0
63
71
  @current_section = "SECTION0"
64
- @line_number = 1
72
+ @line_number = 0
65
73
  @source_path = source_path ? Pathname(source_path) : nil
66
74
  librarian.source_path = @source_path
67
75
  end
@@ -109,11 +117,11 @@ class Germinate::Reader
109
117
  def handle_control_line!(line)
110
118
  if match_data = CONTROL_PATTERN.match(line)
111
119
  comment_chars = match_data[1]
112
- keyword = match_data[2]
113
- argument_text = match_data[3]
120
+ keyword = match_data[3]
121
+ argument_text = match_data[4]
114
122
  arguments = YAML.load("[ #{argument_text} ]")
115
123
 
116
- if comment_chars && !comment_prefix_known?
124
+ if comment_chars && !comment_chars.empty? && !comment_prefix_known?
117
125
  self.comment_prefix = comment_chars
118
126
  end
119
127
  case keyword
@@ -123,9 +131,11 @@ class Germinate::Reader
123
131
  when "END" then end_control_line!(*arguments)
124
132
  when "INSERT" then insert_control_line!(*arguments)
125
133
  when "BRACKET_CODE" then bracket_code_control_line!(*arguments)
126
- when "PROCESS" then process_control_line!(*arguments)
134
+ when "PROCESS" then process_control_line!(*arguments)
135
+ when "PUBLISHER" then publisher_control_line!(*arguments)
136
+ when "SET" then set_control_line!(line, *arguments)
127
137
  else
128
- @log.warn "Ignoring unknown directive #{keyword} at line #{@line_number}"
138
+ log.warn "Ignoring unknown directive #{keyword} at line #{@line_number}"
129
139
  end
130
140
  librarian.add_control!(line)
131
141
  true
@@ -175,10 +185,22 @@ class Germinate::Reader
175
185
  librarian.add_process!(process_name, command)
176
186
  end
177
187
 
188
+ def publisher_control_line!(name, type, options={})
189
+ librarian.add_publisher!(name, type, symbolize_keys(options))
190
+ end
191
+
192
+ def set_control_line!(line, name, value)
193
+ librarian.set_variable!(line, @line_number, name, value)
194
+ end
195
+
178
196
  def sample_name=(name)
179
197
  self.current_section = name
180
198
  end
181
199
 
200
+ def sample_name
201
+ self.current_section
202
+ end
203
+
182
204
  def automatic_section_name
183
205
  "SECTION#{section_count}"
184
206
  end
@@ -205,7 +227,7 @@ class Germinate::Reader
205
227
  end
206
228
 
207
229
  def comment_pattern
208
- /^\s*(#{comment_prefix})/
230
+ /^#{Regexp.escape(comment_prefix.rstrip)}/
209
231
  end
210
232
 
211
233
  def unescape(line)
@@ -218,6 +240,27 @@ class Germinate::Reader
218
240
  attributes[:code_open_bracket] = options.fetch("brackets").first
219
241
  attributes[:code_close_bracket] = options.fetch("brackets").last
220
242
  end
243
+ if options["disable_transforms"]
244
+ Germinate::TextTransforms.singleton_methods.each do |transform|
245
+ attributes[transform.to_sym] = false
246
+ end
247
+ end
248
+ attributes
221
249
  end
222
250
  end
251
+
252
+ def symbolize_keys(hash)
253
+ hash.inject({}){|result, (key, value)|
254
+ new_key = case key
255
+ when String then key.to_sym
256
+ else key
257
+ end
258
+ new_value = case value
259
+ when Hash then symbolize_keys(value)
260
+ else value
261
+ end
262
+ result[new_key] = new_value
263
+ result
264
+ }
265
+ end
223
266
  end