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,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