pith 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/pith CHANGED
@@ -48,8 +48,6 @@ class PithCommand < Clamp::Command
48
48
 
49
49
  subcommand "serve", "Serve the generated website" do
50
50
  def execute
51
- build
52
- Thread.new { watch }
53
51
  serve
54
52
  end
55
53
  end
@@ -65,16 +63,17 @@ class PithCommand < Clamp::Command
65
63
  end
66
64
 
67
65
  def project
68
- unless @project
66
+ @project ||= begin
69
67
  pith_dir = input_dir + "_pith"
70
68
  unless pith_dir.directory?
71
69
  signal_usage_error %(No "#{pith_dir}" directory ... this doesn't look right!)
72
70
  end
73
71
  puts %{Generating to "#{output_dir}"}
74
- @project = Pith::Project.new(:input_dir => input_dir, :output_dir => output_dir)
75
- @project.logger = Pith::ConsoleLogger.new
72
+ @project = Pith::Project.new(
73
+ :input_dir => input_dir, :output_dir => output_dir,
74
+ :logger => Pith::ConsoleLogger.new
75
+ )
76
76
  end
77
- @project
78
77
  end
79
78
 
80
79
  def build
@@ -10,12 +10,12 @@ Background:
10
10
 
11
11
  Scenario: link from one top-level page to another
12
12
 
13
- Given input file "index.html.haml" contains
13
+ Given input file "index.html.haml" contains
14
14
  """
15
15
  = link("page.html", "Page")
16
16
  """
17
17
  And input file "page.html" exists
18
-
18
+
19
19
  When I build the site
20
20
  Then output file "index.html" should contain
21
21
  """
@@ -93,7 +93,7 @@ Scenario: links included from a partial
93
93
 
94
94
  Scenario: use "title" meta-data attribute in link
95
95
 
96
- Given input file "index.html.haml" contains
96
+ Given input file "index.html.haml" contains
97
97
  """
98
98
  = link("page.html")
99
99
  """
@@ -117,7 +117,7 @@ Scenario: link to an Input object
117
117
 
118
118
  Given input file "subdir/page.html.haml" contains
119
119
  """
120
- = link(project.input("help.html"))
120
+ = link(project.input("help.html.haml"))
121
121
  """
122
122
 
123
123
  And input file "help.html.haml" contains
@@ -135,11 +135,11 @@ Scenario: link to an Input object
135
135
 
136
136
  Scenario: link to a missing resource
137
137
 
138
- Given input file "index.html.haml" contains
138
+ Given input file "index.html.haml" contains
139
139
  """
140
140
  = link("missing_page.html")
141
141
  """
142
-
142
+
143
143
  When I build the site
144
144
  Then output file "index.html" should contain
145
145
  """
@@ -149,11 +149,11 @@ Scenario: link to a missing resource
149
149
  Scenario: assume content negotiation
150
150
 
151
151
  Given the "assume_content_negotiation" flag is enabled
152
- And input file "index.html.haml" contains
152
+ And input file "index.html.haml" contains
153
153
  """
154
154
  = link("page.html", "Page")
155
155
  """
156
-
156
+
157
157
  When I build the site
158
158
  Then output file "index.html" should contain
159
159
  """
@@ -164,11 +164,11 @@ Scenario: link to an index page
164
164
 
165
165
  Given the "assume_directory_index" flag is enabled
166
166
 
167
- And input file "page.html.haml" contains
167
+ And input file "page.html.haml" contains
168
168
  """
169
169
  = link("stuff/index.html", "Stuff")
170
170
  """
171
-
171
+
172
172
  When I build the site
173
173
  Then output file "page.html" should contain
174
174
  """
@@ -179,11 +179,11 @@ Scenario: link to an index page in the same directory
179
179
 
180
180
  Given the "assume_directory_index" flag is enabled
181
181
 
182
- And input file "page.html.haml" contains
182
+ And input file "page.html.haml" contains
183
183
  """
184
184
  = link("index.html", "Index")
185
185
  """
186
-
186
+
187
187
  When I build the site
188
188
  Then output file "page.html" should contain
189
189
  """
@@ -1,27 +1,34 @@
1
1
  module Pith
2
-
2
+
3
3
  class ConsoleLogger
4
-
4
+
5
5
  def initialize(out = STDOUT, err = STDERR)
6
6
  @out = out
7
7
  @err = err
8
8
  end
9
9
 
10
- def info(message, &block)
10
+ def debug(message = nil, &block)
11
+ if ENV["PITH_DEBUG"]
12
+ message ||= block.call
13
+ @out.puts("DEBUG: " + message)
14
+ end
15
+ end
16
+
17
+ def info(message = nil, &block)
11
18
  message ||= block.call
12
19
  @out.puts(message)
13
20
  end
14
21
 
15
- def warn(message, &block)
22
+ def warn(message = nil, &block)
16
23
  message ||= block.call
17
24
  @err.puts(message)
18
25
  end
19
26
 
20
- def error(message, &block)
27
+ def error(message = nil, &block)
21
28
  message ||= block.call
22
29
  @err.puts("ERROR: " + message)
23
30
  end
24
-
31
+
25
32
  end
26
-
27
- end
33
+
34
+ end
data/lib/pith/input.rb CHANGED
@@ -1,49 +1,34 @@
1
1
  require "fileutils"
2
2
  require "pathname"
3
- require "pith/render_context"
3
+ require "observer"
4
+ require "pith/output"
4
5
  require "tilt"
5
6
  require "yaml"
6
7
 
7
8
  module Pith
9
+
8
10
  class Input
9
11
 
12
+ include Observable
13
+
10
14
  def initialize(project, path)
11
15
  @project = project
12
16
  @path = path
13
- @meta = {}
14
17
  determine_pipeline
15
- load unless resource?
18
+ when_created
16
19
  end
17
20
 
18
21
  attr_reader :project, :path
19
22
 
20
23
  attr_reader :output_path
21
- attr_reader :dependencies
22
24
  attr_reader :pipeline
23
- attr_reader :error
24
25
 
25
26
  # Public: Get the file-system location of this input.
26
27
  #
27
28
  # Returns a fully-qualified Pathname.
28
29
  #
29
30
  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
31
+ @file ||= project.input_dir + path
47
32
  end
48
33
 
49
34
  # Consider whether this input can be ignored.
@@ -51,45 +36,32 @@ module Pith
51
36
  # Returns true if it can.
52
37
  #
53
38
  def ignorable?
54
- path.each_filename do |path_component|
39
+ @ignorable ||= path.each_filename do |path_component|
55
40
  project.ignore_patterns.each do |pattern|
56
41
  return true if File.fnmatch(pattern, path_component)
57
42
  end
58
43
  end
59
44
  end
60
45
 
61
- # Check whether output is up-to-date.
46
+ # Determine whether this input is a template, requiring evaluation.
62
47
  #
63
- # Return true unless output needs to be re-generated.
48
+ # Returns true if it is.
64
49
  #
65
- def uptodate?
66
- dependencies && FileUtils.uptodate?(output_file, dependencies)
50
+ def template?
51
+ !pipeline.empty?
67
52
  end
68
53
 
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
54
+ def output
55
+ unless ignorable?
56
+ @output ||= Output.for(self, @output_path)
85
57
  end
86
- @dependencies = render_context.dependencies
87
58
  end
88
59
 
89
60
  # Render this input using Tilt
90
61
  #
91
62
  def render(context, locals = {}, &block)
92
- return file.read if resource?
63
+ return file.read if !template?
64
+ ensure_loaded
93
65
  @pipeline.inject(@template_text) do |text, processor|
94
66
  template = processor.new(file.to_s, @template_start_line) { text }
95
67
  template.render(context, locals, &block)
@@ -116,6 +88,7 @@ module Pith
116
88
  # Returns a Hash.
117
89
  #
118
90
  def meta
91
+ ensure_loaded
119
92
  @meta
120
93
  end
121
94
 
@@ -153,6 +126,39 @@ module Pith
153
126
  end
154
127
  end
155
128
 
129
+ # Synchronise the state of the Input with the filesystem
130
+ #
131
+ # Returns true if the file still exists.
132
+ #
133
+ def sync
134
+ mtime = file.mtime
135
+ if mtime.to_i > @last_mtime.to_i
136
+ @last_mtime = mtime
137
+ when_changed
138
+ end
139
+ true
140
+ rescue Errno::ENOENT => e
141
+ when_deleted
142
+ nil
143
+ end
144
+
145
+ def when_created
146
+ log_lifecycle "+"
147
+ @last_mtime = file.mtime
148
+ end
149
+
150
+ def when_changed
151
+ log_lifecycle "~"
152
+ unload if loaded?
153
+ changed(true)
154
+ notify_observers
155
+ end
156
+
157
+ def when_deleted
158
+ log_lifecycle "X"
159
+ output.delete if output
160
+ end
161
+
156
162
  private
157
163
 
158
164
  def default_title
@@ -163,36 +169,45 @@ module Pith
163
169
  @pipeline = []
164
170
  remaining_path = path.to_s
165
171
  while remaining_path =~ /^(.+)\.(.+)$/
166
- if Tilt[$2]
172
+ if handler = Tilt[$2]
167
173
  remaining_path = $1
168
- @pipeline << Tilt[$2]
174
+ @pipeline << handler
169
175
  else
170
176
  break
171
177
  end
172
178
  end
173
179
  @output_path = Pathname(remaining_path)
174
- if resource?
175
- @dependencies = [file]
176
- end
177
180
  end
178
181
 
179
- def resource?
180
- pipeline.empty?
182
+ def loaded?
183
+ @load_time
184
+ end
185
+
186
+ # Make sure we've loaded the input file.
187
+ #
188
+ def ensure_loaded
189
+ load unless loaded?
181
190
  end
182
191
 
183
192
  # Read input file, extracting YAML meta-data header, and template content.
184
193
  #
185
194
  def load
186
- file.open do |input|
187
- load_meta(input)
188
- load_template(input)
195
+ @load_time = Time.now
196
+ @meta = {}
197
+ if template?
198
+ logger.debug "loading #{path}"
199
+ file.open do |io|
200
+ read_meta(io)
201
+ @template_start_line = io.lineno + 1
202
+ @template_text = io.read
203
+ end
189
204
  end
190
205
  end
191
206
 
192
- def load_meta(input)
193
- header = input.gets
207
+ def read_meta(io)
208
+ header = io.gets
194
209
  if header =~ /^---/
195
- while line = input.gets
210
+ while line = io.gets
196
211
  break if line =~ /^(---|\.\.\.)/
197
212
  header << line
198
213
  end
@@ -202,25 +217,25 @@ module Pith
202
217
  logger.warn "#{file}:1: badly-formed YAML header"
203
218
  end
204
219
  else
205
- input.rewind
220
+ io.rewind
206
221
  end
207
222
  end
208
223
 
209
- def load_template(input)
210
- @template_start_line = input.lineno + 1
211
- @template_text = input.read
212
- end
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"
224
+ # Note that the input file has changed, so we'll need to re-load it.
225
+ #
226
+ def unload
227
+ logger.debug "unloading #{path}"
228
+ @load_time = nil
218
229
  end
219
230
 
220
231
  def logger
221
232
  project.logger
222
233
  end
223
234
 
235
+ def log_lifecycle(state)
236
+ logger.info("#{state} #{path}")
237
+ end
238
+
224
239
  end
225
240
 
226
241
  end
@@ -0,0 +1,107 @@
1
+ require "fileutils"
2
+ require "pith/render_context"
3
+ require "set"
4
+
5
+ module Pith
6
+
7
+ class Output
8
+
9
+ def self.for(input, path)
10
+ new(input, path)
11
+ end
12
+
13
+ def initialize(input, path)
14
+ @input = input
15
+ @path = path
16
+ end
17
+
18
+ attr_reader :input
19
+ attr_reader :error
20
+ attr_reader :path
21
+
22
+ def project
23
+ input.project
24
+ end
25
+
26
+ def file
27
+ @file ||= project.output_dir + path
28
+ end
29
+
30
+ # Generate output for this template
31
+ #
32
+ def build
33
+ return false if @generated
34
+ logger.info("--> #{path}")
35
+ @dependencies = Set.new
36
+ file.parent.mkpath
37
+ if input.template?
38
+ evaluate_template
39
+ else
40
+ copy_resource
41
+ end
42
+ @generated = true
43
+ end
44
+
45
+ def record_dependency_on(*inputs)
46
+ inputs.each do |input|
47
+ @dependencies << input
48
+ input.add_observer(self)
49
+ end
50
+ end
51
+
52
+ def delete
53
+ invalidate
54
+ logger.info("--X #{path}")
55
+ FileUtils.rm_f(file)
56
+ end
57
+
58
+ def update # called by dependencies that change
59
+ invalidate
60
+ end
61
+
62
+ private
63
+
64
+ def invalidate
65
+ if @generated
66
+ @dependencies.each do |d|
67
+ d.delete_observer(self)
68
+ end
69
+ @dependencies = nil
70
+ @generated = nil
71
+ end
72
+ end
73
+
74
+ def copy_resource
75
+ FileUtils.copy(input.file, file)
76
+ record_dependency_on(input)
77
+ end
78
+
79
+ def evaluate_template
80
+ render_context = RenderContext.new(self)
81
+ file.open("w") do |out|
82
+ begin
83
+ @error = nil
84
+ out.puts(render_context.render(input))
85
+ rescue StandardError, SyntaxError => e
86
+ @error = e
87
+ logger.warn exception_summary(e, :max_backtrace => 5)
88
+ out.puts "<pre>"
89
+ out.puts exception_summary(e)
90
+ end
91
+ end
92
+ record_dependency_on(*project.config_inputs)
93
+ end
94
+
95
+ def logger
96
+ project.logger
97
+ end
98
+
99
+ def exception_summary(e, options = {})
100
+ max_backtrace = options[:max_backtrace] || 999
101
+ trimmed_backtrace = e.backtrace[0, max_backtrace]
102
+ (["#{e.class}: #{e.message}"] + trimmed_backtrace).join("\n ") + "\n"
103
+ end
104
+
105
+ end
106
+
107
+ end