devver-germinate 1.1.0 → 1.2.0

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