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.
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