devver-germinate 1.0.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 (86) hide show
  1. data/.gitignore +2 -0
  2. data/History.txt +4 -0
  3. data/README.rdoc +132 -0
  4. data/Rakefile +43 -0
  5. data/bin/germ +133 -0
  6. data/cucumber.yml +2 -0
  7. data/examples/basic.rb +118 -0
  8. data/examples/short.rb +17 -0
  9. data/features/author-formats-article.feature +108 -0
  10. data/features/author-lists-info.feature +45 -0
  11. data/features/author-publishes-article-source.feature +5 -0
  12. data/features/author-publishes-article.feature +5 -0
  13. data/features/author-republishes-article.feature +5 -0
  14. data/features/author-selects-hunks.feature +26 -0
  15. data/features/author-updates-article-source.feature +5 -0
  16. data/features/author-views-stuff.feature +48 -0
  17. data/features/bin/quoter +6 -0
  18. data/features/bin/sorter +4 -0
  19. data/features/example_articles/code_samples.rb +89 -0
  20. data/features/example_articles/escaping.txt +12 -0
  21. data/features/example_articles/hello.rb +9 -0
  22. data/features/example_articles/pipelines.txt +25 -0
  23. data/features/example_articles/regexen.rb +24 -0
  24. data/features/example_articles/sample_offsets.rb +18 -0
  25. data/features/example_articles/specials.rb +19 -0
  26. data/features/example_articles/wrapping.rb +8 -0
  27. data/features/example_output/code_samples.txt +186 -0
  28. data/features/example_output/escaping.out +5 -0
  29. data/features/example_output/hello.txt +1 -0
  30. data/features/example_output/pipelines.out +28 -0
  31. data/features/example_output/regexen.txt +22 -0
  32. data/features/example_output/sample_offsets.txt +15 -0
  33. data/features/example_output/specials.txt +36 -0
  34. data/features/example_output/wrapping.txt +3 -0
  35. data/features/step_definitions/germinate.rb +30 -0
  36. data/features/support/env.rb +18 -0
  37. data/germinate.gemspec +55 -0
  38. data/lib/germinate.rb +54 -0
  39. data/lib/germinate/application.rb +62 -0
  40. data/lib/germinate/article_editor.rb +20 -0
  41. data/lib/germinate/article_formatter.rb +75 -0
  42. data/lib/germinate/formatter.rb +119 -0
  43. data/lib/germinate/hunk.rb +149 -0
  44. data/lib/germinate/implicit_insertion.rb +9 -0
  45. data/lib/germinate/insertion.rb +15 -0
  46. data/lib/germinate/librarian.rb +179 -0
  47. data/lib/germinate/pipeline.rb +11 -0
  48. data/lib/germinate/process.rb +67 -0
  49. data/lib/germinate/reader.rb +212 -0
  50. data/lib/germinate/selector.rb +95 -0
  51. data/lib/germinate/shared_style_attributes.rb +23 -0
  52. data/lib/germinate/text_transforms.rb +90 -0
  53. data/spec/germinate/application_spec.rb +14 -0
  54. data/spec/germinate/article_editor_spec.rb +97 -0
  55. data/spec/germinate/article_formatter_spec.rb +153 -0
  56. data/spec/germinate/code_hunk_spec.rb +45 -0
  57. data/spec/germinate/formatter_spec.rb +160 -0
  58. data/spec/germinate/hunk_spec.rb +77 -0
  59. data/spec/germinate/implicit_insertion_spec.rb +33 -0
  60. data/spec/germinate/insertion_spec.rb +18 -0
  61. data/spec/germinate/librarian_spec.rb +336 -0
  62. data/spec/germinate/pipeline_spec.rb +24 -0
  63. data/spec/germinate/process_spec.rb +64 -0
  64. data/spec/germinate/reader_spec.rb +306 -0
  65. data/spec/germinate/selector_spec.rb +65 -0
  66. data/spec/germinate/text_hunk_spec.rb +53 -0
  67. data/spec/germinate/text_transforms_spec.rb +154 -0
  68. data/spec/germinate_spec.rb +8 -0
  69. data/spec/spec.opts +1 -0
  70. data/spec/spec_helper.rb +16 -0
  71. data/tasks/ann.rake +80 -0
  72. data/tasks/bones.rake +20 -0
  73. data/tasks/cucumber.rake +5 -0
  74. data/tasks/gem.rake +201 -0
  75. data/tasks/git.rake +40 -0
  76. data/tasks/notes.rake +27 -0
  77. data/tasks/post_load.rake +34 -0
  78. data/tasks/rdoc.rake +51 -0
  79. data/tasks/rubyforge.rake +55 -0
  80. data/tasks/setup.rb +292 -0
  81. data/tasks/spec.rake +54 -0
  82. data/tasks/svn.rake +47 -0
  83. data/tasks/test.rake +40 -0
  84. data/tasks/zentest.rake +36 -0
  85. data/test/test_germinate.rb +0 -0
  86. metadata +209 -0
@@ -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,15 @@
1
+ require 'fattr'
2
+
3
+ class Germinate::Insertion
4
+ attr_reader :library
5
+ attr_reader :selector
6
+
7
+ def initialize(selector, library)
8
+ @selector = selector
9
+ @library = library
10
+ end
11
+
12
+ def resolve
13
+ library[selector]
14
+ end
15
+ end
@@ -0,0 +1,179 @@
1
+ require 'orderedhash'
2
+ require 'fattr'
3
+ require File.expand_path("shared_style_attributes", File.dirname(__FILE__))
4
+
5
+ # The Librarian is responsible for organizing all the chunks of content derived
6
+ # from reading a source file and making them available for later re-assembly and
7
+ # formatting.
8
+ class Germinate::Librarian
9
+ include Germinate::SharedStyleAttributes
10
+
11
+ attr_reader :lines
12
+ attr_reader :text_lines
13
+ attr_reader :code_lines
14
+ attr_reader :front_matter_lines
15
+
16
+ fattr(:log) { Germinate.logger }
17
+
18
+ def initialize
19
+ @lines = []
20
+ @text_lines = []
21
+ @code_lines = []
22
+ @front_matter_lines = []
23
+ @sections = OrderedHash.new do |hash, key|
24
+ hash[key] = Germinate::TextHunk.new([], shared_style_attributes)
25
+ end
26
+ @samples = OrderedHash.new do |hash, key|
27
+ hash[key] = Germinate::CodeHunk.new([], shared_style_attributes)
28
+ end
29
+ @processes = {}
30
+ end
31
+
32
+ def add_front_matter!(line)
33
+ add_line!(line)
34
+ @front_matter_lines << line
35
+ end
36
+
37
+ def add_control!(line)
38
+ add_line!(line)
39
+ end
40
+
41
+ def add_text!(section, line)
42
+ add_line!(line)
43
+ @text_lines << line
44
+ @sections[section] << line
45
+ end
46
+
47
+ def add_code!(sample, line)
48
+ add_line!(line)
49
+ @code_lines << line
50
+ @samples[sample] << line
51
+ end
52
+
53
+ def add_insertion!(section, selector)
54
+ insertion = Germinate::Insertion.new(selector, self)
55
+ @sections[section] << insertion
56
+ end
57
+
58
+ def set_code_attributes!(sample, attributes)
59
+ attributes.each_pair do |key, value|
60
+ @samples[sample].send(key, value) unless value.nil?
61
+ end
62
+ end
63
+
64
+ def add_process!(process_name, command)
65
+ @processes[process_name] = Germinate::Process.new(process_name, command)
66
+ end
67
+
68
+ def comment_prefix_known?
69
+ !comment_prefix.nil?
70
+ end
71
+
72
+ def section(section_name)
73
+ unless has_section?(section_name)
74
+ raise IndexError,
75
+ "No text section named '#{section_name}'. "\
76
+ "Known sections: #{@sections.keys.join(', ')}"
77
+ end
78
+ Array(@sections[section_name])
79
+ end
80
+
81
+ def has_section?(section_name)
82
+ @sections.key?(section_name)
83
+ end
84
+
85
+ def sample(sample_name)
86
+ unless has_sample?(sample_name)
87
+ raise IndexError,
88
+ "No code sample named '#{sample_name}'. "\
89
+ "Known samples: #{@samples.keys.join(', ')}"
90
+ end
91
+ Array(@samples[sample_name])
92
+ end
93
+
94
+ # Fetch a process by name
95
+ def process(process_name)
96
+ @processes.fetch(process_name)
97
+ end
98
+
99
+ def process_names
100
+ @processes.keys
101
+ end
102
+
103
+ def has_sample?(sample_name)
104
+ @samples.key?(sample_name)
105
+ end
106
+
107
+ def [](selector)
108
+ selector = case selector
109
+ when Germinate::Selector then selector
110
+ else Germinate::Selector.new(selector, "SECTION0")
111
+ end
112
+ sample =
113
+ case selector.selector_type
114
+ when :code then sample(selector.key)
115
+ when :special then
116
+ case selector.key
117
+ when "SOURCE" then Germinate::CodeHunk.new(lines, self)
118
+ when "CODE" then Germinate::CodeHunk.new(code_lines, self)
119
+ when "TEXT" then Germinate::CodeHunk.new(text_lines, self)
120
+ else raise "Unknown special section '$#{selector.key}'"
121
+ end
122
+ else
123
+ raise Exception,
124
+ "Unknown selector type #{selector.selector_type.inspect}"
125
+ end
126
+
127
+ sample = execute_pipeline(sample, selector.pipeline)
128
+
129
+ start_offset = start_offset(sample, selector)
130
+ end_offset = end_offset(sample, selector, start_offset)
131
+ case selector.delimiter
132
+ when '..' then sample[start_offset..end_offset]
133
+ when '...' then sample[start_offset...end_offset]
134
+ when ',' then sample[start_offset, selector.length]
135
+ when nil then sample.dup.replace([sample[start_offset]])
136
+ else raise "Don't understand delimiter #{selector.delimiter.inspect}"
137
+ end
138
+ end
139
+
140
+ def section_names
141
+ @sections.keys
142
+ end
143
+
144
+ def sample_names
145
+ @samples.keys
146
+ end
147
+
148
+ private
149
+
150
+ def add_line!(line)
151
+ @lines << line
152
+ end
153
+
154
+ def start_offset(hunk, selector)
155
+ offset = selector.start_offset_for_slice
156
+ case offset
157
+ when Integer then offset
158
+ when Regexp then hunk.index_matching(offset)
159
+ else
160
+ raise "Don't know how to use #{offset.inspect} as an offset"
161
+ end
162
+ end
163
+
164
+ def end_offset(hunk, selector, start_offset)
165
+ offset = selector.end_offset_for_slice
166
+ case offset
167
+ when Integer, nil then offset
168
+ when Regexp then
169
+ hunk.index_matching(offset, start_offset)
170
+ else
171
+ raise "Don't know how to use #{offset.inspect} as an offset"
172
+ end
173
+ end
174
+
175
+ def execute_pipeline(hunk, process_names)
176
+ processes = process_names.map{|n| process(n)}
177
+ Germinate::Pipeline.new(processes).call(hunk)
178
+ end
179
+ end
@@ -0,0 +1,11 @@
1
+ class Germinate::Pipeline
2
+ def initialize(processes)
3
+ @processes = processes
4
+ end
5
+
6
+ def call(input)
7
+ @processes.inject(input) { |output, process|
8
+ process.call(output)
9
+ }
10
+ end
11
+ end
@@ -0,0 +1,67 @@
1
+ require 'tempfile'
2
+ require 'fattr'
3
+ require 'English'
4
+ require 'main'
5
+
6
+ # A Process represents an external command which can be used to process a Hunk
7
+ # of text.
8
+ class Germinate::Process
9
+ attr_reader :name
10
+ attr_reader :command
11
+
12
+ fattr(:log) { Germinate.logger }
13
+
14
+ def initialize(name, command)
15
+ @name = name
16
+ @command = command
17
+ end
18
+
19
+ def call(input)
20
+ if pipe?
21
+ call_command_in_pipe(input)
22
+ else
23
+ call_command_on_temp_file(input)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def call_command_in_pipe(input)
30
+ log_popen(command, 'r+') do |process|
31
+ input.each do |line|
32
+ process << line
33
+ end
34
+ process.close_write
35
+ return input.class.new(process.readlines, input)
36
+ end
37
+ end
38
+
39
+ def call_command_on_temp_file(input)
40
+ Tempfile.open("germinate_hunk") do |file|
41
+ input.each do |line|
42
+ file << line
43
+ end
44
+ file.close
45
+ log_popen(substitute_filename(command, file.path), 'r') do |process|
46
+ return input.class.new(process.readlines, input)
47
+ end
48
+ end
49
+ end
50
+
51
+ def pipe?
52
+ !command.include?("%f")
53
+ end
54
+
55
+ def substitute_filename(command, filename)
56
+ command.gsub("%f", "'#{filename}'")
57
+ end
58
+
59
+ def log_popen(command, mode, &block)
60
+ log.debug "Running command '#{command}'"
61
+ IO.popen(command, mode, &block)
62
+ status = $CHILD_STATUS
63
+ unless status.success?
64
+ log.warn "Command '#{command}' exited with status #{status}"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,212 @@
1
+ require 'alter_ego'
2
+ require 'forwardable'
3
+ require 'fattr'
4
+
5
+ # The Reader is responsible for reading a source file, breaking it down into
6
+ # into its constituent chunks of text, code, etc., assigning names to them, and
7
+ # filing them away in a Librarian. It is also responsible for interpreting any
8
+ # special control lines found in the input document.
9
+ class Germinate::Reader
10
+ include AlterEgo
11
+ extend Forwardable
12
+
13
+ CONTROL_PATTERN = /^\s*([^\s\\]+)?\s*:([A-Z0-9_]+):\s*(.*)?\s*$/
14
+
15
+ attr_reader :librarian
16
+ attr_reader :current_section
17
+ attr_reader :section_count
18
+ fattr(:log) { Germinate.logger }
19
+
20
+ def_delegators :librarian,
21
+ :comment_prefix,
22
+ :comment_prefix=,
23
+ :comment_prefix_known?
24
+
25
+ state :initial, :default => true do
26
+ handle :add_line!, :first_line!
27
+
28
+ transition :to => :text, :on => :text!
29
+ transition :to => :code, :on => :code!
30
+ transition :to => :front_matter, :on => :front_matter!
31
+ end
32
+
33
+ state :front_matter do
34
+ handle :add_line!, :add_front_matter!
35
+
36
+ transition :to => :text, :on => :text!
37
+ transition :to => :code, :on => :code!
38
+ end
39
+
40
+ state :code do
41
+ handle :add_line!, :add_code!
42
+
43
+ transition :to => :text, :on => :text!
44
+ transition :to => :code, :on => :code!
45
+ transition :to => :finished, :on => :finish!
46
+ end
47
+
48
+ state :text do
49
+ handle :add_line!, :add_text!
50
+
51
+ transition :to => :text, :on => :text!
52
+ transition :to => :code, :on => :code!
53
+ transition :to => :finished, :on => :finish!
54
+ end
55
+
56
+ state :finished do
57
+
58
+ end
59
+
60
+ def initialize(librarian)
61
+ @librarian = librarian
62
+ @section_count = 0
63
+ @current_section = "SECTION0"
64
+ @line_number = 1
65
+ end
66
+
67
+ # Read a line
68
+ def <<(line)
69
+ @line_number += 1
70
+ unless handle_control_line!(line)
71
+ add_line!(unescape(line))
72
+ end
73
+ end
74
+
75
+ def increment_section_count!
76
+ self.section_count = section_count.succ
77
+ self.current_section = automatic_section_name
78
+ end
79
+
80
+ private
81
+
82
+ attr_writer :current_section
83
+ attr_writer :section_count
84
+
85
+ def first_line!(line)
86
+ front_matter!
87
+ add_front_matter!(line)
88
+ end
89
+
90
+ def add_front_matter!(line)
91
+ librarian.add_front_matter!(line)
92
+ end
93
+
94
+ def add_text!(line)
95
+ if uncommented?(line) && non_blank?(line)
96
+ code!
97
+ add_code!(line)
98
+ else
99
+ librarian.add_text!(current_section, line)
100
+ end
101
+ end
102
+
103
+ def add_code!(line)
104
+ librarian.add_code!(current_section, line)
105
+ end
106
+
107
+ def handle_control_line!(line)
108
+ if match_data = CONTROL_PATTERN.match(line)
109
+ comment_chars = match_data[1]
110
+ keyword = match_data[2]
111
+ argument_text = match_data[3]
112
+ arguments = YAML.load("[ #{argument_text} ]")
113
+
114
+ if comment_chars && !comment_prefix_known?
115
+ self.comment_prefix = comment_chars
116
+ end
117
+ case keyword
118
+ when "TEXT" then text_control_line!(*arguments)
119
+ when "SAMPLE" then sample_control_line!(*arguments)
120
+ when "CUT" then cut_control_line!(*arguments)
121
+ when "END" then end_control_line!(*arguments)
122
+ when "INSERT" then insert_control_line!(*arguments)
123
+ when "BRACKET_CODE" then bracket_code_control_line!(*arguments)
124
+ when "PROCESS" then process_control_line!(*arguments)
125
+ else
126
+ @log.warn "Ignoring unknown directive #{keyword} at line #{@line_number}"
127
+ end
128
+ librarian.add_control!(line)
129
+ true
130
+ else
131
+ false
132
+ end
133
+ end
134
+
135
+ def text_control_line!(section_name=nil)
136
+ increment_section_count!
137
+ self.current_section = section_name || automatic_section_name
138
+ text!
139
+ end
140
+
141
+ def cut_control_line!
142
+ increment_section_count!
143
+ code!
144
+ end
145
+
146
+ def end_control_line!
147
+ increment_section_count!
148
+ code!
149
+ end
150
+
151
+ def sample_control_line!(sample_name=current_section, options={})
152
+ increment_section_count!
153
+ self.sample_name = sample_name || automatic_section_name
154
+ librarian.set_code_attributes!(
155
+ sample_name,
156
+ :code_open_bracket => options.fetch("brackets", []).first,
157
+ :code_close_bracket => options.fetch("brackets", []).last)
158
+ code!
159
+ end
160
+
161
+ def bracket_code_control_line!(open_bracket=nil, close_bracket=nil)
162
+ librarian.code_open_bracket = open_bracket
163
+ librarian.code_close_bracket = close_bracket
164
+ end
165
+
166
+ def insert_control_line!(selector=nil)
167
+ librarian.add_insertion!(
168
+ current_section,
169
+ Germinate::Selector.new(selector, current_section))
170
+ end
171
+
172
+ def process_control_line!(process_name, command)
173
+ librarian.add_process!(process_name, command)
174
+ end
175
+
176
+ def sample_name=(name)
177
+ self.current_section = name
178
+ end
179
+
180
+ def automatic_section_name
181
+ "SECTION#{section_count}"
182
+ end
183
+
184
+ def parse_section_name(text)
185
+ name = YAML.load(text)
186
+ if name && !name.empty?
187
+ name.to_s
188
+ else
189
+ nil
190
+ end
191
+ end
192
+
193
+ def non_blank?(line)
194
+ /^\s*$/ !~ line
195
+ end
196
+
197
+ def uncommented?(line)
198
+ if comment_prefix_known?
199
+ comment_pattern !~ line
200
+ else
201
+ false
202
+ end
203
+ end
204
+
205
+ def comment_pattern
206
+ /^\s*(#{comment_prefix})/
207
+ end
208
+
209
+ def unescape(line)
210
+ line.sub(/\\(:[A-Z0-9_]+:)/, '\1')
211
+ end
212
+ end