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