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,54 @@
1
+ require File.expand_path("text_transforms", File.dirname(__FILE__))
2
+ require File.expand_path("pipeline", File.dirname(__FILE__))
3
+ require File.expand_path("origin", File.dirname(__FILE__))
4
+ require 'fattr'
5
+
6
+ module Germinate::SharedStyleAttributes
7
+ fattr :comment_prefix
8
+ fattr :code_open_bracket => nil
9
+ fattr :code_close_bracket => nil
10
+ fattr :pipeline => Germinate::Pipeline.new([])
11
+ fattr :source_path => nil
12
+ fattr :origin => Germinate::Origin.new
13
+
14
+
15
+ TEXT_TRANSFORMS = Germinate::TextTransforms.singleton_methods
16
+
17
+ (TEXT_TRANSFORMS - ['pipeline']).each do |transform|
18
+ fattr(transform, false)
19
+ end
20
+
21
+ def disable_all_transforms!
22
+ TEXT_TRANSFORMS.each do |transform|
23
+ self.send("#{transform}=", false)
24
+ end
25
+ end
26
+
27
+ def shared_style_attributes
28
+ Germinate::SharedStyleAttributes.fattrs.inject({}) {
29
+ |attributes, key|
30
+
31
+ attributes[key] = send(key)
32
+ attributes
33
+ }
34
+ end
35
+
36
+ def copy_shared_style_attributes_from(other, override=true)
37
+ case other
38
+ when Germinate::SharedStyleAttributes
39
+ copy_attributes!(other.shared_style_attributes)
40
+ when Hash
41
+ copy_attributes!(other, override)
42
+ else
43
+ raise "Don't know how to copy attributes from #{other.inspect}"
44
+ end
45
+ end
46
+
47
+ def copy_attributes!(attributes, override=true)
48
+ attributes.each_pair do |key, value|
49
+ if !value.nil? && (override || !send(key))
50
+ self.send(key, value)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,94 @@
1
+ require 'tempfile'
2
+ require 'fattr'
3
+ require 'English'
4
+ require 'main'
5
+
6
+ # A ShellProcess represents an external command which can be used to process a Hunk
7
+ # of text.
8
+ class Germinate::ShellProcess
9
+ attr_reader :name
10
+ attr_reader :command
11
+ attr_reader :variables
12
+
13
+ fattr(:log) { Germinate.logger }
14
+
15
+ def initialize(name, command, variables={})
16
+ @name = name
17
+ @command = command
18
+ @variables = variables
19
+ end
20
+
21
+ def call(input)
22
+ if pipe?
23
+ call_command_in_pipe(input)
24
+ elsif input.whole_file?
25
+ call_command_on_source_file(input)
26
+ else
27
+ call_command_on_temp_file(input)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def call_command_in_pipe(input)
34
+ log_popen(command, 'r+') do |process|
35
+ input.each do |line|
36
+ process << line
37
+ end
38
+ process.close_write
39
+ return input.class.new(process.readlines, input)
40
+ end
41
+ end
42
+
43
+ def call_command_on_temp_file(input)
44
+ Tempfile.open("germinate_hunk") do |file|
45
+ input.each do |line|
46
+ file << line
47
+ end
48
+ file.close
49
+ log_popen(substitute_filename(command, file.path), 'r') do |process|
50
+ return input.class.new(process.readlines, input)
51
+ end
52
+ end
53
+ end
54
+
55
+ def call_command_on_source_file(input)
56
+ log_popen(substitute_filename(command, input.source_path), 'r') do |process|
57
+ return Germinate::CodeHunk.new(process.readlines, input)
58
+ end
59
+ end
60
+
61
+ def pipe?
62
+ !command.include?("%f")
63
+ end
64
+
65
+ def substitute_filename(command, filename)
66
+ command.gsub("%f", "'#{filename}'")
67
+ end
68
+
69
+ def log_popen(command, mode, &block)
70
+ log.debug "Running command `#{command}`"
71
+ with_environment_variables(@variables) do
72
+ IO.popen(command, mode, &block)
73
+ end
74
+ status = $CHILD_STATUS
75
+ unless status.nil? ||status.success?
76
+ log.warn "Command '#{command}' exited with status #{status}"
77
+ end
78
+ end
79
+
80
+ def with_environment_variables(variables)
81
+ old_values = variables.inject({}) do |original, (name, variable)|
82
+ original[name.to_s] = ENV[name.to_s]
83
+ ENV[name.to_s] = variable.to_s
84
+ original
85
+ end
86
+ yield
87
+ ensure
88
+ (old_values || {}).each_pair do |name, value|
89
+ if value.nil? then ENV.delete(name)
90
+ else ENV[name] = value
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,19 @@
1
+ class Germinate::ShellPublisher < Germinate::Publisher
2
+ identifier 'shell'
3
+
4
+ def initialize(name, librarian, options)
5
+ @command = options.delete(:command) do
6
+ raise ArgumentError,
7
+ "A 'command' option must be supplied for publisher type 'shell'"
8
+ end
9
+ super
10
+ end
11
+
12
+ def publish!(output, extra_options={})
13
+ process = Germinate::ShellProcess.new(name, @command, librarian.variables)
14
+ processed = process.call(input)
15
+ processed.each do |line|
16
+ output.print(line)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ class Germinate::SimplePublisher < Germinate::Publisher
2
+ def publish!(output, extra_options={})
3
+ input.each do |line|
4
+ output.puts(line)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ require 'pathname'
2
+ require 'fattr'
3
+ require 'fileutils'
4
+
5
+ # SourcePath represents an article source file on disk.
6
+ class Germinate::SourceFile
7
+ fattr :path
8
+ fattr(:backup_path) { Pathname(path.to_s + ".germ.bak") }
9
+ fattr(:log) { Germinate.logger }
10
+
11
+ def initialize(path)
12
+ @path = Pathname(path)
13
+ end
14
+
15
+ def write!(lines)
16
+ log.debug "Writing #{lines.size} lines to #{path}"
17
+ file = File.new(path)
18
+ flock_result = file.flock(File::LOCK_EX)
19
+ if flock_result != 0
20
+ raise "Unable to lock file #{path}"
21
+ end
22
+ FileUtils.cp(path, backup_path)
23
+ unless path.read == backup_path.read
24
+ raise "Error backup up #{path} to #{backup_path}"
25
+ end
26
+ begin
27
+ path.open('w+') do |output|
28
+ lines.each do |line|
29
+ output.write(line)
30
+ end
31
+ end
32
+ rescue Exception => error
33
+ FileUtils.cp(backup_path, path)
34
+ raise
35
+ end
36
+ log.info "Changes saved to #{path}."
37
+ log.info "Previous state saved as #{backup_path}."
38
+ ensure
39
+ file.flock(File::LOCK_UN)
40
+ end
41
+ end
@@ -0,0 +1,119 @@
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
+ when String
13
+ result << [line]
14
+ result << []
15
+ else
16
+ result << line
17
+ end
18
+ result
19
+ }
20
+ paragraphs.delete_if{|p| p.empty?}
21
+ hunk.dup.replace(paragraphs.map {|paragraph|
22
+ case paragraph
23
+ when Germinate::Hunk then paragraph
24
+ else paragraph.join(" ")
25
+ end
26
+ })
27
+ }
28
+ end
29
+
30
+ def self.strip_blanks
31
+ lambda { |hunk|
32
+ result = hunk.dup
33
+ result.shift while result.first =~ /^\s*$/ && !result.empty?
34
+ result.pop while result.last =~ /^\s*$/ && !result.empty?
35
+ result
36
+ }
37
+ end
38
+
39
+ def self.erase_comments(comment_prefix="")
40
+ lambda { |hunk|
41
+ hunk.dup.map! do |line|
42
+ if comment_prefix && String === line
43
+ if match_data = /^\s*(#{comment_prefix})+\s*/.match(line)
44
+ offset = match_data.begin(0)
45
+ length = match_data[0].length
46
+ line_copy = line.dup
47
+ line_copy[offset, length] = (" " * length)
48
+ line_copy
49
+ else
50
+ line
51
+ end
52
+ else
53
+ line
54
+ end
55
+ end
56
+ }
57
+ end
58
+
59
+ def self.uncomment(comment_prefix=nil)
60
+ lambda { |hunk|
61
+ comment_prefix ||= hunk.comment_prefix
62
+ hunk.dup.map! do |line|
63
+ if comment_prefix && line.respond_to?(:sub)
64
+ line.sub(/^#{Regexp.escape(comment_prefix.rstrip)}\s*/,"")
65
+ else
66
+ line
67
+ end
68
+ end
69
+ }
70
+ end
71
+
72
+ def self.rstrip_lines
73
+ lambda { |hunk|
74
+ hunk.dup.map!{|line| String === line ? line.to_s.rstrip : line}
75
+ }
76
+ end
77
+
78
+ def self.bracket(open_bracket=nil, close_bracket=nil)
79
+ lambda { |hunk|
80
+ result = hunk.dup
81
+ result.clear
82
+ result << (open_bracket || hunk.code_open_bracket)
83
+ result.push(*Array(hunk))
84
+ result << (close_bracket || hunk.code_close_bracket)
85
+ result.compact!
86
+ result
87
+ }
88
+ end
89
+
90
+ def self.pipeline(pipeline=nil)
91
+ lambda do |hunk|
92
+ pipeline ||= hunk.pipeline
93
+ pipeline.call(hunk)
94
+ end
95
+ end
96
+
97
+ def self.expand_insertions
98
+ lambda do |hunk|
99
+ hunk.resolve_insertions
100
+ end
101
+ end
102
+
103
+ def self.flatten_nested
104
+ lambda do |hunk|
105
+ result = hunk.flatten
106
+ result.copy_shared_style_attributes_from(hunk)
107
+ result
108
+ end
109
+ end
110
+
111
+ eigenclss = class << self; self; end
112
+
113
+ eigenclss.instance_eval do
114
+ private
115
+ def log
116
+ @log ||= Germinate.logger
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,25 @@
1
+ require 'fattr'
2
+ require File.expand_path("shared_style_attributes", File.dirname(__FILE__))
3
+
4
+ class Germinate::TransformProcess
5
+ fattr(:log) { Germinate.logger }
6
+
7
+ # We can't just use TextTransforms.singleton_methods because order is important
8
+ def ordered_transforms
9
+ %w[expand_insertions strip_blanks erase_comments
10
+ uncomment join_lines rstrip_lines
11
+ pipeline bracket flatten_nested]
12
+ end
13
+
14
+ def call(hunk)
15
+ ordered_transforms.inject(hunk) { |input, transform|
16
+ if hunk.send("#{transform}?")
17
+ log.debug "Performing text transform #{transform} on #{hunk}"
18
+ Germinate::TextTransforms.send(transform).call(input)
19
+ else
20
+ log.debug "Skipping text transform #{transform} on #{hunk} lines"
21
+ input
22
+ end
23
+ }
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ class Germinate::Variable < String
2
+ attr_reader :name
3
+ attr_reader :origin
4
+ attr_reader :line
5
+
6
+ def initialize(name, value, line, source_path, line_number)
7
+ super(value.to_s)
8
+ @name = name
9
+ @line = line
10
+ @origin = Germinate::Origin.new(source_path, line_number)
11
+ end
12
+
13
+ def update_source_line!(comment_prefix)
14
+ line.replace(make_control_line(comment_prefix))
15
+ end
16
+
17
+ private
18
+
19
+ def make_control_line(comment_prefix)
20
+ "#{comment_prefix}:SET: '#{name}', '#{self}'\n"
21
+ end
22
+
23
+ end
@@ -0,0 +1,14 @@
1
+ # :BRACKET_CODE: "<pre>", "</pre>"
2
+
3
+ # :SAMPLE: sample1
4
+ code sample 1
5
+
6
+ # :SAMPLE: sample2
7
+ code sample 2
8
+
9
+ # :TEXT:
10
+ # Here is example 2:
11
+ # :INSERT: @sample2
12
+ #
13
+ # And here is example 1:
14
+ # :INSERT: @sample1
@@ -0,0 +1,31 @@
1
+ require File.expand_path(
2
+ File.join(File.dirname(__FILE__), %w[.. .. lib germinate]))
3
+
4
+ module Germinate
5
+
6
+ describe Application do
7
+ before :each do
8
+ @it = Application.new(nil, nil)
9
+ end
10
+
11
+ context "loading plugins" do
12
+ before :each do
13
+ @files = ['a', 'b']
14
+ Gem.stub!(:find_files).and_return(@files)
15
+ Kernel.stub!(:load)
16
+ end
17
+
18
+ it "should look for files called germinate_plugin_v0_init.rb" do
19
+ Gem.should_receive(:find_files).with('germinate_plugin_v0_init')
20
+ @it.load_plugins!
21
+ end
22
+
23
+ it "should load the found files" do
24
+ Kernel.should_receive(:load).with('a')
25
+ Kernel.should_receive(:load).with('b')
26
+ @it.load_plugins!
27
+ end
28
+ end
29
+ end
30
+
31
+ 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\n", "text line 2\n"],
28
+ ["this is the code\n", "code line 2\n"]
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\n", "text line 2\n"]
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