pith 0.2.3 → 0.3.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.
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