pith 0.2.0 → 0.2.1

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