devver-germinate 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,95 @@
|
|
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
|
+
|
12
|
+
PATTERN = /([@$])?(\w+)?(:([^\s\|]+))?(\|([\w|]+))?/
|
13
|
+
EXCERPT_PATTERN = %r{((-?\d+)|(/[^/]*/))(((\.\.\.?)|(,))((-?\d+)|(/[^/]*/)))?}
|
14
|
+
|
15
|
+
def initialize(string, default_key)
|
16
|
+
@string = string
|
17
|
+
@default_key = default_key
|
18
|
+
match_data = case string
|
19
|
+
when "", nil then {}
|
20
|
+
else PATTERN.match(string)
|
21
|
+
end
|
22
|
+
|
23
|
+
@selector_type =
|
24
|
+
case match_data[1]
|
25
|
+
when "$" then :special
|
26
|
+
when "@", nil then :code
|
27
|
+
else raise "Unknown selector type '#{match_data[1]}'"
|
28
|
+
end
|
29
|
+
@key = match_data[2] || default_key
|
30
|
+
if match_data[3]
|
31
|
+
parse_excerpt(match_data[3])
|
32
|
+
else
|
33
|
+
@delimiter = '..'
|
34
|
+
@start_offset = 1
|
35
|
+
@end_offset = -1
|
36
|
+
@length = nil
|
37
|
+
end
|
38
|
+
@pipeline = String(match_data[6]).split("|")
|
39
|
+
end
|
40
|
+
|
41
|
+
def start_offset_for_slice
|
42
|
+
offset_for_slice(start_offset)
|
43
|
+
end
|
44
|
+
|
45
|
+
def end_offset_for_slice
|
46
|
+
offset_for_slice(end_offset)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def parse_excerpt(excerpt)
|
52
|
+
match_data = EXCERPT_PATTERN.match(excerpt)
|
53
|
+
integer_start_offset = match_data[2]
|
54
|
+
regexp_start_offset = match_data[3]
|
55
|
+
@delimiter = match_data[5]
|
56
|
+
integer_end_offset = match_data[9]
|
57
|
+
regexp_end_offset = match_data[10]
|
58
|
+
|
59
|
+
if integer_start_offset
|
60
|
+
@start_offset = integer_start_offset.to_i
|
61
|
+
elsif regexp_start_offset
|
62
|
+
@start_offset = Regexp.new(regexp_start_offset[1..-2])
|
63
|
+
else
|
64
|
+
raise "Could not parse start offset '#{match_data[1]}'"
|
65
|
+
end
|
66
|
+
|
67
|
+
case @delimiter
|
68
|
+
when '..', '...'
|
69
|
+
if integer_end_offset
|
70
|
+
@end_offset = integer_end_offset.to_i
|
71
|
+
elsif regexp_end_offset
|
72
|
+
@end_offset = Regexp.new(regexp_end_offset[1..-2])
|
73
|
+
end
|
74
|
+
when nil
|
75
|
+
@end_offset = @start_offset
|
76
|
+
when ','
|
77
|
+
@length = integer_end_offset.to_i
|
78
|
+
else raise "Should not get here"
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
def integer_offsets?
|
84
|
+
Integer === @start_offset && Integer === @end_offset
|
85
|
+
end
|
86
|
+
|
87
|
+
def offset_for_slice(offset)
|
88
|
+
return offset if offset.nil? || offset.kind_of?(Regexp)
|
89
|
+
if offset < 1
|
90
|
+
offset
|
91
|
+
else
|
92
|
+
offset - 1
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'fattr'
|
2
|
+
|
3
|
+
module Germinate::SharedStyleAttributes
|
4
|
+
fattr :comment_prefix
|
5
|
+
fattr :code_open_bracket => nil
|
6
|
+
fattr :code_close_bracket => nil
|
7
|
+
fattr :pipeline => []
|
8
|
+
|
9
|
+
def shared_style_attributes
|
10
|
+
Germinate::SharedStyleAttributes.fattrs.inject({}) {
|
11
|
+
|attributes, key|
|
12
|
+
|
13
|
+
attributes[key] = send(key)
|
14
|
+
attributes
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def copy_shared_style_attrubutes_from(other)
|
19
|
+
other.shared_style_attributes.each_pair do |key, value|
|
20
|
+
self.send(key, value) unless other.send(key).nil?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# A library of text transforms. Each public module method returns a callable
|
2
|
+
# object which can be used to transform an array of lines.
|
3
|
+
module Germinate::TextTransforms
|
4
|
+
|
5
|
+
IDENTITY_TRANSFORM = lambda {|hunk| hunk}
|
6
|
+
def self.join_lines
|
7
|
+
lambda { |hunk|
|
8
|
+
paragraphs = hunk.inject([[]]) { |result, line|
|
9
|
+
case line
|
10
|
+
when /\S/
|
11
|
+
result.last << line.strip
|
12
|
+
else
|
13
|
+
result << [line]
|
14
|
+
result << []
|
15
|
+
end
|
16
|
+
result
|
17
|
+
}
|
18
|
+
paragraphs.delete_if{|p| p.empty?}
|
19
|
+
hunk.dup.replace(paragraphs.map {|paragraph|
|
20
|
+
paragraph.join(" ")
|
21
|
+
})
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.strip_blanks
|
26
|
+
lambda { |hunk|
|
27
|
+
result = hunk.dup
|
28
|
+
result.shift until result.first =~ /\S/ || result.empty?
|
29
|
+
result.pop until result.last =~ /\S/ || result.empty?
|
30
|
+
result
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.erase_comments(comment_prefix="")
|
35
|
+
lambda { |hunk|
|
36
|
+
hunk.dup.map! do |line|
|
37
|
+
if comment_prefix
|
38
|
+
if match_data = /^\s*(#{comment_prefix})+\s*/.match(line)
|
39
|
+
offset = match_data.begin(0)
|
40
|
+
length = match_data[0].length
|
41
|
+
line_copy = line.dup
|
42
|
+
line_copy[offset, length] = (" " * length)
|
43
|
+
line_copy
|
44
|
+
else
|
45
|
+
line
|
46
|
+
end
|
47
|
+
else
|
48
|
+
line
|
49
|
+
end
|
50
|
+
end
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.uncomment(comment_prefix="")
|
55
|
+
lambda { |hunk|
|
56
|
+
hunk.dup.map! do |line|
|
57
|
+
if comment_prefix
|
58
|
+
line.sub(/^#{comment_prefix}/,"")
|
59
|
+
else
|
60
|
+
line
|
61
|
+
end
|
62
|
+
end
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.rstrip_lines
|
67
|
+
lambda { |hunk|
|
68
|
+
hunk.dup.map!{|line| line.to_s.rstrip}
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.bracket(open_bracket=nil, close_bracket=nil)
|
73
|
+
lambda { |hunk|
|
74
|
+
result = hunk.dup
|
75
|
+
result.clear
|
76
|
+
result << (open_bracket || hunk.code_open_bracket)
|
77
|
+
result.push(*Array(hunk))
|
78
|
+
result << (close_bracket || hunk.code_close_bracket)
|
79
|
+
result.compact!
|
80
|
+
result
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.pipeline(pipeline=nil)
|
85
|
+
lambda do |hunk|
|
86
|
+
pipeline ||= hunk.pipeline
|
87
|
+
pipeline.call(hunk)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.expand_path(
|
2
|
+
File.join(File.dirname(__FILE__), %w[.. .. lib germinate]))
|
3
|
+
|
4
|
+
module Germinate
|
5
|
+
describe ArticleEditor do
|
6
|
+
before :each do
|
7
|
+
@librarian = Librarian.new
|
8
|
+
@it = ArticleEditor.new(@librarian)
|
9
|
+
end
|
10
|
+
|
11
|
+
context "with an empty library" do
|
12
|
+
it "should yield no hunks" do
|
13
|
+
collect_hunks.should be_empty
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context "with a text section and matching code section" do
|
18
|
+
before :each do
|
19
|
+
@librarian.add_text!("SECTION1", "this is the text")
|
20
|
+
@librarian.add_text!("SECTION1", "text line 2")
|
21
|
+
@librarian.add_code!("SECTION1", "this is the code")
|
22
|
+
@librarian.add_code!("SECTION1", "code line 2")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should yield text and code in order" do
|
26
|
+
collect_hunks.should == [
|
27
|
+
["this is the text", "text line 2"],
|
28
|
+
["this is the code", "code line 2"]
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should yield text section as TextHunk" do
|
33
|
+
collect_hunks[0].should be_a_kind_of(TextHunk)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should yield code section as CodeHunk" do
|
37
|
+
collect_hunks[1].should be_a_kind_of(CodeHunk)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should set no comment prefix on hunks" do
|
41
|
+
collect_hunks[0].comment_prefix.should be_nil
|
42
|
+
collect_hunks[1].comment_prefix.should be_nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "with a text section and mis-matched code section" do
|
47
|
+
before :each do
|
48
|
+
@librarian.add_text!("SECTION1", "this is the text")
|
49
|
+
@librarian.add_text!("SECTION1", "text line 2")
|
50
|
+
@librarian.add_code!("SECTION2", "this is the code")
|
51
|
+
@librarian.add_code!("SECTION2", "code line 2")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should yield just the text" do
|
55
|
+
collect_hunks.should == [
|
56
|
+
["this is the text", "text line 2"]
|
57
|
+
]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "with a known comment prefix" do
|
62
|
+
before :each do
|
63
|
+
@librarian.comment_prefix = ";;"
|
64
|
+
@librarian.add_text!("SECTION1", "this is the text")
|
65
|
+
@librarian.add_code!("SECTION1", "this is the code")
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
context "when iterating through hunks" do
|
71
|
+
before :each do
|
72
|
+
@resolved = stub("Resolved Section")
|
73
|
+
@section = stub("Unresolved Section",
|
74
|
+
:resolve_insertions => @resolved)
|
75
|
+
@librarian = stub("Librarian",
|
76
|
+
:section_names => ["section1"],
|
77
|
+
:section => @section,
|
78
|
+
:sample => @section,
|
79
|
+
:has_sample? => true)
|
80
|
+
@it = ArticleEditor.new(@librarian)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should resolve hunks before yielding them" do
|
84
|
+
collect_hunks[0].should equal(@resolved)
|
85
|
+
collect_hunks[1].should equal(@resolved)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def collect_hunks
|
90
|
+
hunks = []
|
91
|
+
@it.each_hunk do |hunk|
|
92
|
+
hunks << hunk
|
93
|
+
end
|
94
|
+
hunks
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
require File.expand_path(
|
4
|
+
File.join(File.dirname(__FILE__), %w[.. .. lib germinate]))
|
5
|
+
|
6
|
+
module Germinate
|
7
|
+
describe ArticleFormatter do
|
8
|
+
before :each do
|
9
|
+
@output = StringIO.new
|
10
|
+
@hunk1 = stub("Hunk1")
|
11
|
+
@hunk2 = stub("Hunk2")
|
12
|
+
@it = ArticleFormatter.new(@output)
|
13
|
+
end
|
14
|
+
|
15
|
+
def output
|
16
|
+
@output.rewind
|
17
|
+
@output.string
|
18
|
+
end
|
19
|
+
|
20
|
+
context "given some hunks to format" do
|
21
|
+
before :each do
|
22
|
+
@hunks = [@hunk1, @hunk2]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should visit each hunk in turn" do
|
26
|
+
@hunk1.should_receive(:format_with).with(@it).ordered
|
27
|
+
@hunk2.should_receive(:format_with).with(@it).ordered
|
28
|
+
@hunks.each do |hunk|
|
29
|
+
@it.format!(hunk)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
context "given some text lines to format but no comment prefix" do
|
36
|
+
before :each do
|
37
|
+
@it.join_lines = false
|
38
|
+
@it.format_text!(Hunk.new([" # foo", "bar\n\n"]))
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should just normalise newlines" do
|
42
|
+
output.should == " # foo\nbar\n"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "given some text lines to format and a comment prefix" do
|
47
|
+
before :each do
|
48
|
+
@it.comment_prefix = "# "
|
49
|
+
@it.uncomment = true
|
50
|
+
@it.join_lines = false
|
51
|
+
@it.format_text!(Hunk.new(["# foo", "bar\n\n"]), "# ")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should erase comments" do
|
55
|
+
output.should == " foo\nbar\n"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "given leading and trailing blank lines around text" do
|
60
|
+
before :each do
|
61
|
+
@it.format_text!(Hunk.new(["", "foo", " \n "]), "#")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should erase comments" do
|
65
|
+
output.should == "foo\n"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "given uncommenting is enabled and a comment prefix is set" do
|
70
|
+
it "should supply the comment prefix to the uncomment transform" do
|
71
|
+
@hunk = stub("Hunk").as_null_object
|
72
|
+
@hunk.stub!(:strip).and_return(@hunk)
|
73
|
+
@prefix = "//"
|
74
|
+
@transform = stub("Transform", :call => @transformed)
|
75
|
+
TextTransforms.stub!(:uncomment).and_return(@transform)
|
76
|
+
|
77
|
+
TextTransforms.should_receive(:uncomment).with(@prefix).
|
78
|
+
and_return(lambda{|h| h})
|
79
|
+
|
80
|
+
@it.comment_prefix = @prefix
|
81
|
+
@it.uncomment = true
|
82
|
+
@it.format_text!(@hunk)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
CODE_TRANSFORMS = %w[rstrip_lines strip_blanks]
|
87
|
+
TEXT_TRANSFORMS = %w[join_lines strip_blanks uncomment rstrip_lines]
|
88
|
+
TEXT_TRANSFORMS.each do |transform_name|
|
89
|
+
context "when only #{transform_name} is enabled" do
|
90
|
+
before :each do
|
91
|
+
(TEXT_TRANSFORMS - [transform_name]).each do |disabled_transform|
|
92
|
+
@it.send("#{disabled_transform}=", false)
|
93
|
+
TextTransforms.stub!(disabled_transform) do
|
94
|
+
fail "Transform #{disabled_transform} should not be enabled"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
@it.send("#{transform_name}=", true)
|
98
|
+
|
99
|
+
@transformed = stub("Transformed Hunk").as_null_object
|
100
|
+
@hunk = stub("Hunk").as_null_object
|
101
|
+
@hunk.stub!(:strip).and_return(@hunk)
|
102
|
+
@transform = stub("Transform", :call => @transformed)
|
103
|
+
TextTransforms.stub!(transform_name).and_return(@transform)
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should perform transform on text hunks" do
|
108
|
+
@transform.should_receive(:call).with(@hunk)
|
109
|
+
@it.format_text!(@hunk)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should return the result of the transform" do
|
113
|
+
@it.format_text!(@hunk).should == @transformed
|
114
|
+
end
|
115
|
+
|
116
|
+
unless CODE_TRANSFORMS.include?(transform_name)
|
117
|
+
it "should not perform transform on code hunks" do
|
118
|
+
@transform.should_not_receive(:call).with(@hunk)
|
119
|
+
@it.format_code!(@hunk)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "given some code lines to format" do
|
126
|
+
before :each do
|
127
|
+
@it.format_code!(Hunk.new([" \n ", " # foo", "bar\n\n", ""]))
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should normalise newlines and strip blanks" do
|
131
|
+
output.should == " # foo\nbar\n"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context "given a code hunk with brackets specified" do
|
136
|
+
before :each do
|
137
|
+
@hunk = Hunk.new(["line 1"],
|
138
|
+
:code_open_bracket => "<<<",
|
139
|
+
:code_close_bracket => ">>>")
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should wrap the code in brackets" do
|
143
|
+
@transform = stub("Bracketer")
|
144
|
+
TextTransforms.should_receive(:bracket).
|
145
|
+
with().and_return(@transform)
|
146
|
+
@transform.should_receive(:call).with(["line 1"]).and_return([])
|
147
|
+
@it.format_code!(@hunk)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|