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 +5 -6
- data/features/relative_linking.feature +12 -12
- data/lib/pith/console_logger.rb +15 -8
- data/lib/pith/input.rb +83 -68
- data/lib/pith/output.rb +107 -0
- data/lib/pith/pathname_ext.rb +6 -1
- data/lib/pith/project.rb +73 -48
- data/lib/pith/render_context.rb +35 -32
- data/lib/pith/server.rb +30 -10
- data/lib/pith/version.rb +1 -1
- data/spec/pith/input_spec.rb +57 -20
- data/spec/pith/pathname_ext_spec.rb +29 -0
- data/spec/pith/project_spec.rb +58 -56
- data/spec/pith/server_spec.rb +101 -0
- data/spec/spec_helper.rb +1 -0
- metadata +34 -30
- data/features/reloading.feature +0 -48
- data/spec/pith/metadata_spec.rb~ +0 -46
- data/spec/pith/plugins/publication_spec.rb~ +0 -39
- data/spec/pith/project_spec.rb~ +0 -137
- data/spec/spec_helper.rb~ +0 -18
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
|
-
|
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(
|
75
|
-
|
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
|
"""
|
data/lib/pith/console_logger.rb
CHANGED
@@ -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
|
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 "
|
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
|
-
|
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
|
-
#
|
46
|
+
# Determine whether this input is a template, requiring evaluation.
|
62
47
|
#
|
63
|
-
#
|
48
|
+
# Returns true if it is.
|
64
49
|
#
|
65
|
-
def
|
66
|
-
|
50
|
+
def template?
|
51
|
+
!pipeline.empty?
|
67
52
|
end
|
68
53
|
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
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 <<
|
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
|
180
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
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
|
193
|
-
header =
|
207
|
+
def read_meta(io)
|
208
|
+
header = io.gets
|
194
209
|
if header =~ /^---/
|
195
|
-
while line =
|
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
|
-
|
220
|
+
io.rewind
|
206
221
|
end
|
207
222
|
end
|
208
223
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
data/lib/pith/output.rb
ADDED
@@ -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
|