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.
- data/.gitignore +2 -0
- data/History.txt +26 -0
- data/README.rdoc +152 -0
- data/Rakefile +43 -0
- data/TODO +140 -0
- data/bin/germ +260 -0
- data/cucumber.yml +2 -0
- data/examples/basic.rb +123 -0
- data/examples/short.rb +19 -0
- data/features/author-formats-article.feature +111 -0
- data/features/author-lists-info.pending_feature +48 -0
- data/features/author-publishes-article-source.feature +5 -0
- data/features/author-publishes-article.feature +57 -0
- data/features/author-republishes-article.feature +5 -0
- data/features/author-selects-hunks.feature +26 -0
- data/features/author-sets-variables.feature +88 -0
- data/features/author-updates-article-source.feature +5 -0
- data/features/author-views-stuff.pending_feature +52 -0
- data/features/bin/quoter +6 -0
- data/features/bin/sorter +4 -0
- data/features/example_articles/bracketing.rb +27 -0
- data/features/example_articles/escaping.txt +13 -0
- data/features/example_articles/excerpt_output.rb +16 -0
- data/features/example_articles/hello.rb +9 -0
- data/features/example_articles/pipelines.txt +25 -0
- data/features/example_articles/regexen.rb +24 -0
- data/features/example_articles/sample_offsets.rb +18 -0
- data/features/example_articles/specials.rb +19 -0
- data/features/example_articles/stderr.rb +10 -0
- data/features/example_articles/wrapping.rb +8 -0
- data/features/example_output/bracketing.out +23 -0
- data/features/example_output/code_samples.txt +186 -0
- data/features/example_output/escaping.out +5 -0
- data/features/example_output/excerpt_output.out +6 -0
- data/features/example_output/hello.txt +1 -0
- data/features/example_output/pipelines.out +28 -0
- data/features/example_output/regexen.txt +22 -0
- data/features/example_output/sample_offsets.txt +15 -0
- data/features/example_output/specials.txt +40 -0
- data/features/example_output/stderr.out +3 -0
- data/features/example_output/wrapping.txt +3 -0
- data/features/step_definitions/germinate.rb +42 -0
- data/features/support/env.rb +20 -0
- data/germinate.gemspec +55 -0
- data/lib/germinate.rb +54 -0
- data/lib/germinate/application.rb +113 -0
- data/lib/germinate/article_editor.rb +20 -0
- data/lib/germinate/formatter.rb +119 -0
- data/lib/germinate/hunk.rb +183 -0
- data/lib/germinate/implicit_insertion.rb +9 -0
- data/lib/germinate/insertion.rb +29 -0
- data/lib/germinate/librarian.rb +293 -0
- data/lib/germinate/origin.rb +5 -0
- data/lib/germinate/pipeline.rb +13 -0
- data/lib/germinate/publisher.rb +57 -0
- data/lib/germinate/reader.rb +266 -0
- data/lib/germinate/selector.rb +136 -0
- data/lib/germinate/shared_style_attributes.rb +54 -0
- data/lib/germinate/shell_process.rb +94 -0
- data/lib/germinate/shell_publisher.rb +19 -0
- data/lib/germinate/simple_publisher.rb +7 -0
- data/lib/germinate/source_file.rb +41 -0
- data/lib/germinate/text_transforms.rb +119 -0
- data/lib/germinate/transform_process.rb +25 -0
- data/lib/germinate/variable.rb +23 -0
- data/sample.rb +14 -0
- data/spec/germinate/application_spec.rb +31 -0
- data/spec/germinate/article_editor_spec.rb +97 -0
- data/spec/germinate/code_hunk_spec.rb +73 -0
- data/spec/germinate/file_hunk_spec.rb +28 -0
- data/spec/germinate/formatter_spec.rb +160 -0
- data/spec/germinate/hunk_spec.rb +84 -0
- data/spec/germinate/implicit_insertion_spec.rb +33 -0
- data/spec/germinate/insertion_spec.rb +19 -0
- data/spec/germinate/librarian_spec.rb +555 -0
- data/spec/germinate/pipeline_spec.rb +34 -0
- data/spec/germinate/process_spec.rb +105 -0
- data/spec/germinate/publisher_spec.rb +130 -0
- data/spec/germinate/reader_spec.rb +385 -0
- data/spec/germinate/selector_spec.rb +121 -0
- data/spec/germinate/shell_publisher_spec.rb +61 -0
- data/spec/germinate/source_file_spec.rb +99 -0
- data/spec/germinate/text_hunk_spec.rb +98 -0
- data/spec/germinate/text_transforms_spec.rb +242 -0
- data/spec/germinate/transform_process_spec.rb +50 -0
- data/spec/germinate/variable_spec.rb +14 -0
- data/spec/germinate_spec.rb +8 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- data/tasks/ann.rake +80 -0
- data/tasks/bones.rake +20 -0
- data/tasks/cucumber.rake +5 -0
- data/tasks/gem.rake +201 -0
- data/tasks/git.rake +40 -0
- data/tasks/notes.rake +27 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +292 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/tasks/zentest.rake +36 -0
- data/test/test_germinate.rb +0 -0
- metadata +228 -0
@@ -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
|