germinate 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 (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,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
@@ -0,0 +1,13 @@
1
+ class Germinate::Pipeline
2
+ attr_reader :processes
3
+
4
+ def initialize(processes)
5
+ @processes = processes
6
+ end
7
+
8
+ def call(input)
9
+ @processes.inject(input) { |output, process|
10
+ process.call(output)
11
+ }
12
+ end
13
+ 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
@@ -0,0 +1,266 @@
1
+ require 'alter_ego'
2
+ require 'ick'
3
+ require 'forwardable'
4
+ require 'fattr'
5
+
6
+ # The Reader is responsible for reading a source file, breaking it down into
7
+ # into its constituent chunks of text, code, etc., assigning names to them, and
8
+ # filing them away in a Librarian. It is also responsible for interpreting any
9
+ # special control lines found in the input document.
10
+ class Germinate::Reader
11
+ include AlterEgo
12
+ extend Forwardable
13
+ Ick::Returning.belongs_to(self)
14
+
15
+ CONTROL_PATTERN = /^(\s*([^\s\\]+)?\s*):([A-Z0-9_]+):\s*(.*)?\s*$/
16
+
17
+ attr_reader :librarian
18
+ attr_reader :current_section
19
+ attr_reader :section_count
20
+ fattr(:log) { Germinate.logger }
21
+
22
+ def_delegators :librarian,
23
+ :comment_prefix,
24
+ :comment_prefix=,
25
+ :comment_prefix_known?
26
+
27
+ state :initial, :default => true do
28
+ handle :add_line!, :first_line!
29
+
30
+ transition :to => :text, :on => :text!
31
+ transition :to => :code, :on => :code!
32
+ transition :to => :front_matter, :on => :front_matter!
33
+ end
34
+
35
+ state :front_matter do
36
+ handle :add_line!, :add_front_matter!
37
+
38
+ transition :to => :text, :on => :text!
39
+ transition :to => :code, :on => :code!
40
+ end
41
+
42
+ state :code do
43
+ handle :add_line!, :add_code!
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
+
51
+ transition :to => :text, :on => :text!
52
+ transition :to => :code, :on => :code!
53
+ transition :to => :finished, :on => :finish!
54
+ end
55
+
56
+ state :text do
57
+ handle :add_line!, :add_text!
58
+
59
+ transition :to => :text, :on => :text!
60
+ transition :to => :code, :on => :code!
61
+ transition :to => :finished, :on => :finish!
62
+ end
63
+
64
+ state :finished do
65
+
66
+ end
67
+
68
+ def initialize(librarian, source_path=nil)
69
+ @librarian = librarian
70
+ @section_count = 0
71
+ @current_section = "SECTION0"
72
+ @line_number = 0
73
+ @source_path = source_path ? Pathname(source_path) : nil
74
+ librarian.source_path = @source_path
75
+ end
76
+
77
+ # Read a line
78
+ def <<(line)
79
+ @line_number += 1
80
+ unless handle_control_line!(line)
81
+ add_line!(unescape(line))
82
+ end
83
+ end
84
+
85
+ def increment_section_count!
86
+ self.section_count = section_count.succ
87
+ self.current_section = automatic_section_name
88
+ end
89
+
90
+ private
91
+
92
+ attr_writer :current_section
93
+ attr_writer :section_count
94
+
95
+ def first_line!(line)
96
+ front_matter!
97
+ add_front_matter!(line)
98
+ end
99
+
100
+ def add_front_matter!(line)
101
+ librarian.add_front_matter!(line)
102
+ end
103
+
104
+ def add_text!(line)
105
+ if uncommented?(line) && non_blank?(line)
106
+ code!
107
+ add_code!(line)
108
+ else
109
+ librarian.add_text!(current_section, line)
110
+ end
111
+ end
112
+
113
+ def add_code!(line)
114
+ librarian.add_code!(current_section, line)
115
+ end
116
+
117
+ def handle_control_line!(line)
118
+ if match_data = CONTROL_PATTERN.match(line)
119
+ comment_chars = match_data[1]
120
+ keyword = match_data[3]
121
+ argument_text = match_data[4]
122
+ arguments = YAML.load("[ #{argument_text} ]")
123
+
124
+ if comment_chars && !comment_chars.empty? && !comment_prefix_known?
125
+ self.comment_prefix = comment_chars
126
+ end
127
+ case keyword
128
+ when "TEXT" then text_control_line!(*arguments)
129
+ when "SAMPLE" then sample_control_line!(*arguments)
130
+ when "CUT" then cut_control_line!(*arguments)
131
+ when "END" then end_control_line!(*arguments)
132
+ when "INSERT" then insert_control_line!(*arguments)
133
+ when "BRACKET_CODE" then bracket_code_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)
137
+ else
138
+ log.warn "Ignoring unknown directive #{keyword} at line #{@line_number}"
139
+ end
140
+ librarian.add_control!(line)
141
+ true
142
+ else
143
+ false
144
+ end
145
+ end
146
+
147
+ def text_control_line!(section_name=nil)
148
+ increment_section_count!
149
+ self.current_section = section_name || automatic_section_name
150
+ text!
151
+ end
152
+
153
+ def cut_control_line!
154
+ increment_section_count!
155
+ code!
156
+ end
157
+
158
+ def end_control_line!
159
+ increment_section_count!
160
+ code!
161
+ end
162
+
163
+ def sample_control_line!(sample_name=current_section, options={})
164
+ increment_section_count!
165
+ self.sample_name = sample_name || automatic_section_name
166
+ librarian.set_code_attributes!(
167
+ sample_name,
168
+ options_to_style_attributes(options))
169
+ code!
170
+ end
171
+
172
+ def bracket_code_control_line!(open_bracket=nil, close_bracket=nil)
173
+ librarian.code_open_bracket = open_bracket
174
+ librarian.code_close_bracket = close_bracket
175
+ end
176
+
177
+ def insert_control_line!(selector=nil, options={})
178
+ librarian.add_insertion!(
179
+ current_section,
180
+ Germinate::Selector.new(selector, current_section),
181
+ options_to_style_attributes(options))
182
+ end
183
+
184
+ def process_control_line!(process_name, command)
185
+ librarian.add_process!(process_name, command)
186
+ end
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
+
196
+ def sample_name=(name)
197
+ self.current_section = name
198
+ end
199
+
200
+ def sample_name
201
+ self.current_section
202
+ end
203
+
204
+ def automatic_section_name
205
+ "SECTION#{section_count}"
206
+ end
207
+
208
+ def parse_section_name(text)
209
+ name = YAML.load(text)
210
+ if name && !name.empty?
211
+ name.to_s
212
+ else
213
+ nil
214
+ end
215
+ end
216
+
217
+ def non_blank?(line)
218
+ /^\s*$/ !~ line
219
+ end
220
+
221
+ def uncommented?(line)
222
+ if comment_prefix_known?
223
+ comment_pattern !~ line
224
+ else
225
+ false
226
+ end
227
+ end
228
+
229
+ def comment_pattern
230
+ /^#{Regexp.escape(comment_prefix.rstrip)}/
231
+ end
232
+
233
+ def unescape(line)
234
+ line.sub(/\\(:[A-Z0-9_]+:)/, '\1')
235
+ end
236
+
237
+ def options_to_style_attributes(options)
238
+ returning({}) do |attributes|
239
+ if options.key?("brackets")
240
+ attributes[:code_open_bracket] = options.fetch("brackets").first
241
+ attributes[:code_close_bracket] = options.fetch("brackets").last
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
249
+ end
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
266
+ end
@@ -0,0 +1,136 @@
1
+ class Germinate::Selector
2
+ attr_reader :string
3
+ attr_reader :selector_type
4
+ attr_reader :key
5
+ attr_reader :start_offset
6
+ attr_reader :end_offset
7
+ attr_reader :length
8
+ attr_reader :delimiter
9
+ attr_reader :pipeline
10
+ attr_reader :default_key
11
+ attr_reader :origin
12
+
13
+ PATTERN = /^([@$])?(\w+)?(:([^\|]+))?(\|([\w|]+))?$/
14
+ EXCERPT_OUTPUT_PATTERN = /^([@$])?(\w+)?(\|([\w|]+))?(:([^\|]+))?$/
15
+ EXCERPT_PATTERN = %r{((-?\d+)|(/[^/]*/))(((\.\.\.?)|(,))((-?\d+)|(/[^/]*/)))?}
16
+
17
+ def initialize(string, default_key=:unspecified, origin="<Unknown>")
18
+ @string = string
19
+ @default_key = default_key
20
+ @origin = origin
21
+ match_data = case string
22
+ when "", nil then {}
23
+ else
24
+ if data = PATTERN.match(string)
25
+ @excerpt_output = false
26
+ elsif data = EXCERPT_OUTPUT_PATTERN.match(string)
27
+ @excerpt_output = true
28
+ else
29
+ raise "Could not parse selector '#{string}'"
30
+ end
31
+ data
32
+ end
33
+
34
+ subscript_index = @excerpt_output ? 6 : 3
35
+ pipeline_index = @excerpt_output ? 4 : 6
36
+
37
+ @selector_type =
38
+ case match_data[1]
39
+ when "$" then :special
40
+ when "@" then :code
41
+ when nil then default_key == :unspecified ? :special : :code
42
+ else raise "Unknown selector type '#{match_data[1]}'"
43
+ end
44
+ @key = match_data[2] || (default_key == :unspecified ? "SOURCE" : default_key)
45
+ if match_data[subscript_index]
46
+ @slice = true
47
+ parse_excerpt(match_data[subscript_index])
48
+ else
49
+ @slice = false
50
+ @delimiter = '..'
51
+ @start_offset = 1
52
+ @end_offset = -1
53
+ @length = nil
54
+ end
55
+ @pipeline = String(match_data[pipeline_index]).
56
+ split("|").unshift('_transform').uniq
57
+ end
58
+
59
+ def start_offset_for_slice
60
+ offset_for_slice(start_offset)
61
+ end
62
+
63
+ def end_offset_for_slice
64
+ offset_for_slice(end_offset)
65
+ end
66
+
67
+ # Should excerpting be done on the output of the process?
68
+ def excerpt_output?
69
+ @excerpt_output
70
+ end
71
+
72
+ # Is it just a subset of the source hunk? (opposite of @whole?)
73
+ def slice?
74
+ @slice && !@excerpt_output
75
+ end
76
+
77
+ # Is it the entire hunk? (opposite of #slice?)
78
+ def whole?
79
+ !slice?
80
+ end
81
+
82
+ def to_s
83
+ string + "(#{origin})"
84
+ end
85
+
86
+ def ==(other)
87
+ string == other
88
+ end
89
+
90
+ private
91
+
92
+ def parse_excerpt(excerpt)
93
+ match_data = EXCERPT_PATTERN.match(excerpt)
94
+ integer_start_offset = match_data[2]
95
+ regexp_start_offset = match_data[3]
96
+ @delimiter = match_data[5]
97
+ integer_end_offset = match_data[9]
98
+ regexp_end_offset = match_data[10]
99
+
100
+ if integer_start_offset
101
+ @start_offset = integer_start_offset.to_i
102
+ elsif regexp_start_offset
103
+ @start_offset = Regexp.new(regexp_start_offset[1..-2])
104
+ else
105
+ raise "Could not parse start offset '#{match_data[1]}'"
106
+ end
107
+
108
+ case @delimiter
109
+ when '..', '...'
110
+ if integer_end_offset
111
+ @end_offset = integer_end_offset.to_i
112
+ elsif regexp_end_offset
113
+ @end_offset = Regexp.new(regexp_end_offset[1..-2])
114
+ end
115
+ when nil
116
+ @end_offset = @start_offset
117
+ when ','
118
+ @length = integer_end_offset.to_i
119
+ else raise "Should not get here"
120
+ end
121
+
122
+ end
123
+
124
+ def integer_offsets?
125
+ Integer === @start_offset && Integer === @end_offset
126
+ end
127
+
128
+ def offset_for_slice(offset)
129
+ return offset if offset.nil? || offset.kind_of?(Regexp)
130
+ if offset < 1
131
+ offset
132
+ else
133
+ offset - 1
134
+ end
135
+ end
136
+ end