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.
- data/.gitignore +2 -0
- data/History.txt +4 -0
- data/README.rdoc +132 -0
- data/Rakefile +43 -0
- data/bin/germ +133 -0
- data/cucumber.yml +2 -0
- data/examples/basic.rb +118 -0
- data/examples/short.rb +17 -0
- data/features/author-formats-article.feature +108 -0
- data/features/author-lists-info.feature +45 -0
- data/features/author-publishes-article-source.feature +5 -0
- data/features/author-publishes-article.feature +5 -0
- data/features/author-republishes-article.feature +5 -0
- data/features/author-selects-hunks.feature +26 -0
- data/features/author-updates-article-source.feature +5 -0
- data/features/author-views-stuff.feature +48 -0
- data/features/bin/quoter +6 -0
- data/features/bin/sorter +4 -0
- data/features/example_articles/code_samples.rb +89 -0
- data/features/example_articles/escaping.txt +12 -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/wrapping.rb +8 -0
- data/features/example_output/code_samples.txt +186 -0
- data/features/example_output/escaping.out +5 -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 +36 -0
- data/features/example_output/wrapping.txt +3 -0
- data/features/step_definitions/germinate.rb +30 -0
- data/features/support/env.rb +18 -0
- data/germinate.gemspec +55 -0
- data/lib/germinate.rb +54 -0
- data/lib/germinate/application.rb +62 -0
- data/lib/germinate/article_editor.rb +20 -0
- data/lib/germinate/article_formatter.rb +75 -0
- data/lib/germinate/formatter.rb +119 -0
- data/lib/germinate/hunk.rb +149 -0
- data/lib/germinate/implicit_insertion.rb +9 -0
- data/lib/germinate/insertion.rb +15 -0
- data/lib/germinate/librarian.rb +179 -0
- data/lib/germinate/pipeline.rb +11 -0
- data/lib/germinate/process.rb +67 -0
- data/lib/germinate/reader.rb +212 -0
- data/lib/germinate/selector.rb +95 -0
- data/lib/germinate/shared_style_attributes.rb +23 -0
- data/lib/germinate/text_transforms.rb +90 -0
- data/spec/germinate/application_spec.rb +14 -0
- data/spec/germinate/article_editor_spec.rb +97 -0
- data/spec/germinate/article_formatter_spec.rb +153 -0
- data/spec/germinate/code_hunk_spec.rb +45 -0
- data/spec/germinate/formatter_spec.rb +160 -0
- data/spec/germinate/hunk_spec.rb +77 -0
- data/spec/germinate/implicit_insertion_spec.rb +33 -0
- data/spec/germinate/insertion_spec.rb +18 -0
- data/spec/germinate/librarian_spec.rb +336 -0
- data/spec/germinate/pipeline_spec.rb +24 -0
- data/spec/germinate/process_spec.rb +64 -0
- data/spec/germinate/reader_spec.rb +306 -0
- data/spec/germinate/selector_spec.rb +65 -0
- data/spec/germinate/text_hunk_spec.rb +53 -0
- data/spec/germinate/text_transforms_spec.rb +154 -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 +209 -0
@@ -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,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
|