pith 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/pith CHANGED
@@ -16,11 +16,11 @@ class PithCommand < Clamp::Command
16
16
  option ["-i", "--input"], "INPUT_DIR", "Input directory", :attribute_name => :input_dir, :default => "." do |dir|
17
17
  Pathname(dir)
18
18
  end
19
-
19
+
20
20
  option ["-o", "--output"], "OUTPUT_DIR", "Output directory", :attribute_name => :output_dir, :default => "INPUT_DIR/_out" do |dir|
21
21
  Pathname(dir)
22
22
  end
23
-
23
+
24
24
  option ["-n", "--interval"], "INTERVAL", "Rebuild interval", :default => 2 do |n|
25
25
  Integer(n)
26
26
  end
@@ -45,7 +45,7 @@ class PithCommand < Clamp::Command
45
45
  watch
46
46
  end
47
47
  end
48
-
48
+
49
49
  subcommand "serve", "Serve the generated website" do
50
50
  def execute
51
51
  build
@@ -63,7 +63,7 @@ class PithCommand < Clamp::Command
63
63
  def default_output_dir
64
64
  input_dir + "_out"
65
65
  end
66
-
66
+
67
67
  def project
68
68
  unless @project
69
69
  pith_dir = input_dir + "_pith"
@@ -79,6 +79,7 @@ class PithCommand < Clamp::Command
79
79
 
80
80
  def build
81
81
  project.build
82
+ exit(1) if project.has_errors?
82
83
  end
83
84
 
84
85
  def watch
@@ -92,7 +93,7 @@ class PithCommand < Clamp::Command
92
93
  puts %{>>> Now taking the Pith at "http://localhost:#{port}"}
93
94
  Rack::Handler.get("thin").run(server, :Port => port)
94
95
  end
95
-
96
+
96
97
  end
97
98
 
98
99
  PithCommand.run
@@ -1,16 +1,17 @@
1
1
  Feature: error handling
2
2
 
3
3
  I want to be told when something goes wrong
4
-
4
+
5
5
  Scenario: bad haml
6
6
 
7
7
  Given input file "index.html.haml" contains
8
8
  """
9
9
  %h2{class="this is not valid ruby"} Heading
10
-
10
+
11
11
  %p Content
12
12
  """
13
-
13
+
14
14
  When I build the site
15
-
15
+
16
16
  Then output file "index.html" should contain /syntax error/
17
+ And the project should have errors
@@ -72,3 +72,7 @@ end
72
72
  Then /^output file "([^\"]*)" should contain an error$/ do |path|
73
73
  @outputs[path].clean.should == "foo"
74
74
  end
75
+
76
+ Then /^the project should have errors$/ do
77
+ @project.should have_errors
78
+ end
@@ -1,22 +1,226 @@
1
- require "pith/input/template"
2
- require "pith/input/resource"
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "pith/render_context"
4
+ require "tilt"
5
+ require "yaml"
3
6
 
4
7
  module Pith
5
- module Input
8
+ class Input
6
9
 
7
- class << self
10
+ def initialize(project, path)
11
+ @project = project
12
+ @path = path
13
+ @meta = {}
14
+ determine_pipeline
15
+ load unless resource?
16
+ end
17
+
18
+ attr_reader :project, :path
19
+
20
+ attr_reader :output_path
21
+ attr_reader :dependencies
22
+ attr_reader :pipeline
23
+ attr_reader :error
24
+
25
+ # Public: Get the file-system location of this input.
26
+ #
27
+ # Returns a fully-qualified Pathname.
28
+ #
29
+ def file
30
+ project.input_dir + path
31
+ end
32
+
33
+ # Public: Get the file-system location of the corresponding output file.
34
+ #
35
+ # Returns a fully-qualified Pathname.
36
+ #
37
+ def output_file
38
+ project.output_dir + output_path
39
+ end
40
+
41
+ # Public: Generate an output file.
42
+ #
43
+ def build
44
+ return false if ignorable? || uptodate?
45
+ logger.info("--> #{output_path}")
46
+ generate_output
47
+ end
48
+
49
+ # Consider whether this input can be ignored.
50
+ #
51
+ # Returns true if it can.
52
+ #
53
+ def ignorable?
54
+ path.each_filename do |path_component|
55
+ project.ignore_patterns.each do |pattern|
56
+ return true if File.fnmatch(pattern, path_component)
57
+ end
58
+ end
59
+ end
8
60
 
9
- # Construct an object representing a project input file.
10
- #
11
- def new(project, path)
12
- if Template.can_handle?(path)
13
- Template.new(project, path)
61
+ # Check whether output is up-to-date.
62
+ #
63
+ # Return true unless output needs to be re-generated.
64
+ #
65
+ def uptodate?
66
+ dependencies && FileUtils.uptodate?(output_file, dependencies)
67
+ end
68
+
69
+ # Generate output for this template
70
+ #
71
+ def generate_output
72
+ output_file.parent.mkpath
73
+ return FileUtils.copy(file, output_file) if resource?
74
+ render_context = RenderContext.new(project)
75
+ output_file.open("w") do |out|
76
+ begin
77
+ @error = nil
78
+ out.puts(render_context.render(self))
79
+ rescue StandardError, SyntaxError => e
80
+ @error = e
81
+ logger.warn exception_summary(e, :max_backtrace => 5)
82
+ out.puts "<pre>"
83
+ out.puts exception_summary(e)
84
+ end
85
+ end
86
+ @dependencies = render_context.dependencies
87
+ end
88
+
89
+ # Render this input using Tilt
90
+ #
91
+ def render(context, locals = {}, &block)
92
+ return file.read if resource?
93
+ @pipeline.inject(@template_text) do |text, processor|
94
+ template = processor.new(file.to_s, @template_start_line) { text }
95
+ template.render(context, locals, &block)
96
+ end
97
+ end
98
+
99
+ # Public: Get YAML metadata declared in the header of of a template.
100
+ #
101
+ # If the first line of the template starts with "---" it is considered to be
102
+ # the start of a YAML 'document', which is loaded and returned.
103
+ #
104
+ # Examples
105
+ #
106
+ # Given input starting with:
107
+ #
108
+ # ---
109
+ # published: 2008-09-15
110
+ # ...
111
+ # OTHER STUFF
112
+ #
113
+ # input.meta
114
+ # #=> { "published" => "2008-09-15" }
115
+ #
116
+ # Returns a Hash.
117
+ #
118
+ def meta
119
+ @meta
120
+ end
121
+
122
+ # Public: Get page title.
123
+ #
124
+ # The default title is based on the input file-name, sans-extension, capitalised,
125
+ # but can be overridden by providing a "title" in the metadata block.
126
+ #
127
+ # Examples
128
+ #
129
+ # input.path.to_s
130
+ # #=> "some_page.html.haml"
131
+ # input.title
132
+ # #=> "Some page"
133
+ #
134
+ def title
135
+ meta["title"] || default_title
136
+ end
137
+
138
+ # Public: Resolve a reference relative to this input.
139
+ #
140
+ # ref - a String referencing another asset
141
+ #
142
+ # A ref starting with "/" is resolved relative to the project root;
143
+ # anything else is resolved relative to this input.
144
+ #
145
+ # Returns a fully-qualified Pathname of the asset.
146
+ #
147
+ def resolve_path(ref)
148
+ ref = ref.to_s
149
+ if ref[0,1] == "/"
150
+ Pathname(ref[1..-1])
151
+ else
152
+ path.parent + ref
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def default_title
159
+ path.basename.to_s.sub(/\..*/, '').tr('_-', ' ').capitalize
160
+ end
161
+
162
+ def determine_pipeline
163
+ @pipeline = []
164
+ remaining_path = path.to_s
165
+ while remaining_path =~ /^(.+)\.(.+)$/
166
+ if Tilt[$2]
167
+ remaining_path = $1
168
+ @pipeline << Tilt[$2]
14
169
  else
15
- Resource.new(project, path)
170
+ break
171
+ end
172
+ end
173
+ @output_path = Pathname(remaining_path)
174
+ if resource?
175
+ @dependencies = [file]
176
+ end
177
+ end
178
+
179
+ def resource?
180
+ pipeline.empty?
181
+ end
182
+
183
+ # Read input file, extracting YAML meta-data header, and template content.
184
+ #
185
+ def load
186
+ file.open do |input|
187
+ load_meta(input)
188
+ load_template(input)
189
+ end
190
+ end
191
+
192
+ def load_meta(input)
193
+ header = input.gets
194
+ if header =~ /^---/
195
+ while line = input.gets
196
+ break if line =~ /^(---|\.\.\.)/
197
+ header << line
198
+ end
199
+ begin
200
+ @meta = YAML.load(header)
201
+ rescue ArgumentError, SyntaxError
202
+ logger.warn "#{file}:1: badly-formed YAML header"
16
203
  end
204
+ else
205
+ input.rewind
17
206
  end
207
+ end
208
+
209
+ def load_template(input)
210
+ @template_start_line = input.lineno + 1
211
+ @template_text = input.read
212
+ end
18
213
 
214
+ def exception_summary(e, options = {})
215
+ max_backtrace = options[:max_backtrace] || 999
216
+ trimmed_backtrace = e.backtrace[0, max_backtrace]
217
+ (["#{e.class}: #{e.message}"] + trimmed_backtrace).join("\n ") + "\n"
218
+ end
219
+
220
+ def logger
221
+ project.logger
19
222
  end
20
223
 
21
224
  end
22
- end
225
+
226
+ end
@@ -32,19 +32,7 @@ module Pith
32
32
  end
33
33
 
34
34
  module Pith
35
- module Input
36
-
37
- class Abstract
38
-
39
- def published?
40
- false
41
- end
42
-
43
- end
44
-
45
- class Template
46
- include Pith::Plugins::Publication::TemplateMethods
47
- end
48
-
35
+ class Input
36
+ include Pith::Plugins::Publication::TemplateMethods
49
37
  end
50
38
  end
@@ -5,21 +5,21 @@ require "pith/reference_error"
5
5
  require "tilt"
6
6
 
7
7
  module Pith
8
-
8
+
9
9
  class Project
10
-
10
+
11
11
  DEFAULT_IGNORE_PATTERNS = ["_*", ".git", ".svn"].freeze
12
-
12
+
13
13
  def initialize(attributes = {})
14
14
  @ignore_patterns = DEFAULT_IGNORE_PATTERNS.dup
15
15
  attributes.each do |k,v|
16
16
  send("#{k}=", v)
17
17
  end
18
18
  end
19
-
19
+
20
20
  attr_reader :input_dir
21
21
  attr_reader :ignore_patterns
22
-
22
+
23
23
  def input_dir=(dir)
24
24
  @input_dir = Pathname(dir)
25
25
  end
@@ -32,12 +32,12 @@ module Pith
32
32
 
33
33
  attr_accessor :assume_content_negotiation
34
34
  attr_accessor :assume_directory_index
35
-
35
+
36
36
  # Public: get inputs
37
37
  #
38
38
  # Returns Pith::Input objects representing the files in the input_dir.
39
39
  #
40
- # The list of inputs is cached after first load;
40
+ # The list of inputs is cached after first load;
41
41
  # call #refresh to discard the cached data.
42
42
  #
43
43
  def inputs
@@ -71,7 +71,7 @@ module Pith
71
71
  generate_outputs
72
72
  output_dir.touch
73
73
  end
74
-
74
+
75
75
  # Public: discard cached data that is out-of-sync with the file-system.
76
76
  #
77
77
  def refresh
@@ -79,32 +79,39 @@ module Pith
79
79
  @config_files = nil
80
80
  end
81
81
 
82
+ # Public: check for errors.
83
+ #
84
+ # Returns true if any errors were encountered during the last build.
85
+ def has_errors?
86
+ @inputs.any?(&:error)
87
+ end
88
+
82
89
  def last_built_at
83
90
  output_dir.mtime
84
91
  end
85
-
92
+
86
93
  def logger
87
94
  @logger ||= Logger.new(nil)
88
95
  end
89
-
96
+
90
97
  attr_writer :logger
91
98
 
92
99
  def helpers(&block)
93
100
  helper_module.module_eval(&block)
94
101
  end
95
-
102
+
96
103
  def helper_module
97
104
  @helper_module ||= Module.new
98
105
  end
99
106
 
100
107
  def config_files
101
- @config_files ||= begin
108
+ @config_files ||= begin
102
109
  input_dir.all_files("_pith/**")
103
110
  end.to_set
104
111
  end
105
-
112
+
106
113
  private
107
-
114
+
108
115
  def load_config
109
116
  config_file = input_dir + "_pith/config.rb"
110
117
  project = self
@@ -112,7 +119,7 @@ module Pith
112
119
  eval(config_file.read, binding, config_file.to_s, 1)
113
120
  end
114
121
  end
115
-
122
+
116
123
  def remove_old_outputs
117
124
  valid_output_paths = inputs.map { |i| i.output_path }
118
125
  output_dir.all_files.each do |output_file|
@@ -123,9 +130,9 @@ module Pith
123
130
  end
124
131
  end
125
132
  end
126
-
133
+
127
134
  def generate_outputs
128
- inputs.each do |input|
135
+ inputs.each do |input|
129
136
  input.build
130
137
  end
131
138
  end
@@ -141,7 +148,7 @@ module Pith
141
148
  cache_key = [path, file.mtime]
142
149
  input_cache[cache_key]
143
150
  end
144
-
151
+
145
152
  end
146
-
153
+
147
154
  end