pith 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,5 +12,10 @@ class Pathname
12
12
  path.file?
13
13
  end
14
14
  end
15
-
15
+
16
+ def in?(dir)
17
+ prefix = "#{dir}/"
18
+ self.to_s[0,prefix.length] == prefix
19
+ end
20
+
16
21
  end
data/lib/pith/project.rb CHANGED
@@ -2,13 +2,14 @@ require "logger"
2
2
  require "pith/input"
3
3
  require "pith/pathname_ext"
4
4
  require "pith/reference_error"
5
+ require "set"
5
6
  require "tilt"
6
7
 
7
8
  module Pith
8
9
 
9
10
  class Project
10
11
 
11
- DEFAULT_IGNORE_PATTERNS = ["_*", ".git", ".svn", "*~"].freeze
12
+ DEFAULT_IGNORE_PATTERNS = ["_*", ".git", ".gitignore", ".svn", ".sass-cache", "*~", "*.sw[op]"].to_set.freeze
12
13
 
13
14
  def initialize(attributes = {})
14
15
  @ignore_patterns = DEFAULT_IGNORE_PATTERNS.dup
@@ -28,63 +29,89 @@ module Pith
28
29
 
29
30
  def output_dir=(dir)
30
31
  @output_dir = Pathname(dir)
32
+ FileUtils.rm_rf(@output_dir)
33
+ @output_dir.mkpath
31
34
  end
32
35
 
33
36
  attr_accessor :assume_content_negotiation
34
37
  attr_accessor :assume_directory_index
35
38
 
39
+ def ignore(pattern)
40
+ ignore_patterns << pattern
41
+ end
42
+
36
43
  # Public: get inputs
37
44
  #
38
45
  # Returns Pith::Input objects representing the files in the input_dir.
39
46
  #
40
- # The list of inputs is cached after first load;
41
- # call #refresh to discard the cached data.
42
- #
43
47
  def inputs
44
- @inputs ||= input_dir.all_files.map do |input_file|
45
- path = input_file.relative_path_from(input_dir)
46
- find_or_create_input(path)
47
- end.compact
48
+ @input_map.values
49
+ end
50
+
51
+ # Public: get outputs
52
+ #
53
+ # Returns Pith::Output objects representing the files in the output_dir.
54
+ #
55
+ def outputs
56
+ inputs.map(&:output).compact
48
57
  end
49
58
 
50
59
  # Public: find an input.
51
60
  #
52
- # path - an path relative to either input_dir or output_dir
61
+ # path - an path relative to input_dir
53
62
  #
54
- # Returns the first input whose input_path or output_path matches.
63
+ # Returns the first input whose path matches.
55
64
  # Returns nil if no match is found.
56
65
  #
57
66
  def input(path)
58
- path = Pathname(path)
59
- inputs.each do |input|
60
- return input if input.path == path || input.output_path == path
61
- end
62
- nil
67
+ @input_map[Pathname(path)]
68
+ end
69
+
70
+ # Public: find an output.
71
+ #
72
+ # path - an path relative to output_dir
73
+ #
74
+ # Returns the first output whose path matches.
75
+ # Returns nil if no match is found.
76
+ #
77
+ def output(path)
78
+ @output_map[Pathname(path)]
63
79
  end
64
80
 
65
81
  # Public: build the project, generating output files.
66
82
  #
67
83
  def build
68
- refresh
69
- load_config
70
- remove_old_outputs
84
+ sync
71
85
  output_dir.mkpath
72
- generate_outputs
86
+ outputs.each(&:build)
73
87
  output_dir.touch
74
88
  end
75
89
 
76
- # Public: discard cached data that is out-of-sync with the file-system.
90
+ # Public: re-sync with the file-system.
77
91
  #
78
- def refresh
79
- @inputs = nil
80
- @config_files = nil
92
+ def sync
93
+ @input_map ||= {}
94
+ @output_map ||= {}
95
+ @config_inputs = nil
96
+ load_config
97
+ validate_known_inputs
98
+ find_new_inputs
99
+ end
100
+
101
+ def sync_every(period)
102
+ @next_sync ||= 0
103
+ now = Time.now.to_i
104
+ if now >= @next_sync
105
+ sync
106
+ @next_sync = now + period
107
+ end
81
108
  end
82
109
 
83
110
  # Public: check for errors.
84
111
  #
85
112
  # Returns true if any errors were encountered during the last build.
86
113
  def has_errors?
87
- @inputs.any?(&:error)
114
+ inputs.map(&:output).compact.any?(&:error)
88
115
  end
89
116
 
90
117
  def last_built_at
@@ -105,10 +132,10 @@ module Pith
105
132
  @helper_module ||= Module.new
106
133
  end
107
134
 
108
- def config_files
109
- @config_files ||= begin
110
- input_dir.all_files("_pith/**")
111
- end.to_set
135
+ def config_inputs
136
+ @config_inputs ||= inputs.select do |input|
137
+ input.path.to_s[0,6] == "_pith/"
138
+ end
112
139
  end
113
140
 
114
141
  private
@@ -121,35 +148,33 @@ module Pith
121
148
  end
122
149
  end
123
150
 
124
- def remove_old_outputs
125
- valid_output_paths = inputs.map { |i| i.output_path }
126
- output_dir.all_files.each do |output_file|
127
- output_path = output_file.relative_path_from(output_dir)
128
- unless valid_output_paths.member?(output_path)
129
- logger.info("removing #{output_path}")
130
- FileUtils.rm(output_file)
131
- end
151
+ def load_input(path)
152
+ i = Input.new(self, path)
153
+ @input_map[path] = i
154
+ if o = i.output
155
+ @output_map[o.path] = o
132
156
  end
157
+ i
133
158
  end
134
159
 
135
- def generate_outputs
136
- inputs.each do |input|
137
- input.build
160
+ def validate_known_inputs
161
+ invalid_inputs = inputs.select { |i| !i.sync }
162
+ invalid_inputs.each do |i|
163
+ @input_map.delete(i.path)
164
+ if o = i.output
165
+ @output_map.delete(o.path)
166
+ end
138
167
  end
139
168
  end
140
169
 
141
- def input_cache
142
- @input_cache ||= Hash.new do |h, cache_key|
143
- h[cache_key] = Input.new(self, cache_key.first)
170
+ def find_new_inputs
171
+ input_dir.all_files.map do |input_file|
172
+ next if input_file.in?(output_dir)
173
+ path = input_file.relative_path_from(input_dir)
174
+ input(path) || load_input(path)
144
175
  end
145
176
  end
146
177
 
147
- def find_or_create_input(path)
148
- file = input_dir + path
149
- cache_key = [path, file.mtime]
150
- input_cache[cache_key]
151
- end
152
-
153
178
  end
154
179
 
155
180
  end
@@ -5,20 +5,23 @@ require "set"
5
5
  require "tilt"
6
6
 
7
7
  module Pith
8
-
8
+
9
9
  class RenderContext
10
-
10
+
11
11
  include Tilt::CompileSite
12
-
13
- def initialize(project)
14
- @project = project
12
+
13
+ def initialize(output)
14
+ @output = output
15
+ @page = @output.input
16
+ @project = @page.project
15
17
  @input_stack = []
16
- @dependencies = project.config_files.dup
17
18
  self.extend(project.helper_module)
18
19
  end
19
20
 
21
+ attr_reader :output
22
+ attr_reader :page
20
23
  attr_reader :project
21
-
24
+
22
25
  def page
23
26
  @input_stack.first
24
27
  end
@@ -26,7 +29,7 @@ module Pith
26
29
  def current_input
27
30
  @input_stack.last
28
31
  end
29
-
32
+
30
33
  def render(input, locals = {}, &block)
31
34
  with_input(input) do
32
35
  result = input.render(self, locals, &block)
@@ -36,8 +39,6 @@ module Pith
36
39
  end
37
40
  end
38
41
 
39
- attr_reader :dependencies
40
-
41
42
  def include(template_ref, locals = {}, &block)
42
43
  content_block = if block_given?
43
44
  content = capture_haml(&block)
@@ -45,13 +46,13 @@ module Pith
45
46
  end
46
47
  render_ref(template_ref, locals, &content_block)
47
48
  end
48
-
49
+
49
50
  alias :inside :include
50
-
51
+
51
52
  def content_for
52
53
  @content_for_hash ||= Hash.new { "" }
53
54
  end
54
-
55
+
55
56
  def relative_url_to(target_path)
56
57
  url = target_path.relative_path_from(page.path.parent).to_s
57
58
  url = url.sub(/index\.html$/, "") if project.assume_directory_index
@@ -59,46 +60,48 @@ module Pith
59
60
  url = "./" if url.empty?
60
61
  Pathname(url)
61
62
  end
62
-
63
+
63
64
  def href(target_ref)
64
65
  relative_url_to(resolve_reference(target_ref))
65
66
  end
66
67
 
67
68
  def link(target_ref, label = nil)
68
69
  target_path = resolve_reference(target_ref)
69
- label ||= begin
70
+ label ||= begin
70
71
  target_input = input(target_path)
71
- record_dependency_on(target_input.file)
72
+ output.record_dependency_on(target_input)
72
73
  target_input.title
73
74
  rescue ReferenceError
74
- "???"
75
+ "???"
75
76
  end
76
77
  url = relative_url_to(target_path)
77
78
  %{<a href="#{url}">#{label}</a>}
78
79
  end
79
80
 
80
- def record_dependency_on(file)
81
- @dependencies << file
82
- end
83
-
84
81
  private
85
-
82
+
86
83
  def resolve_reference(ref)
87
- if ref.respond_to?(:output_path)
88
- ref.output_path
84
+ if ref.kind_of?(Pith::Input)
85
+ raise(ReferenceError, %{No output for "#{ref.path}"}) if ref.output.nil?
86
+ ref.output.path
89
87
  else
90
88
  current_input.resolve_path(ref)
91
89
  end
92
90
  end
93
-
91
+
94
92
  def input(path)
95
- input = project.input(path)
96
- raise(ReferenceError, %{Can't find "#{path}"}) if input.nil?
97
- input
93
+ project.input(path) ||
94
+ input_with_output_path(path) ||
95
+ raise(ReferenceError, %{Can't find "#{path}"})
96
+ end
97
+
98
+ def input_with_output_path(path)
99
+ o = project.output(path)
100
+ o ? o.input : nil
98
101
  end
99
-
102
+
100
103
  def with_input(input)
101
- record_dependency_on(input.file)
104
+ output.record_dependency_on(input)
102
105
  @input_stack.push(input)
103
106
  begin
104
107
  yield
@@ -106,12 +109,12 @@ module Pith
106
109
  @input_stack.pop
107
110
  end
108
111
  end
109
-
112
+
110
113
  def render_ref(template_ref, locals = {}, &block)
111
114
  template_input = input(resolve_reference(template_ref))
112
115
  render(template_input, locals, &block)
113
116
  end
114
117
 
115
118
  end
116
-
119
+
117
120
  end
data/lib/pith/server.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "rack"
2
- require "adsf/rack"
2
+ require "pathname"
3
+ require "thread"
3
4
 
4
5
  module Pith
5
6
 
@@ -10,29 +11,48 @@ module Pith
10
11
  use Rack::CommonLogger
11
12
  use Rack::ShowExceptions
12
13
  use Rack::Lint
13
- use Adsf::Rack::IndexFileFinder, :root => project.output_dir
14
- use Pith::Server::DefaultToHtml, project.output_dir
14
+ use Pith::Server::OutputFinder, project
15
15
  run Rack::Directory.new(project.output_dir)
16
16
  end
17
17
  end
18
18
 
19
19
  extend self
20
20
 
21
- class DefaultToHtml
21
+ class OutputFinder
22
22
 
23
- def initialize(app, root)
23
+ def initialize(app, project)
24
24
  @app = app
25
- @root = root
25
+ @project = project
26
26
  end
27
27
 
28
28
  def call(env)
29
29
 
30
+ @project.sync_every(1)
31
+
30
32
  path_info = ::Rack::Utils.unescape(env["PATH_INFO"])
31
- file = "#{@root}#{path_info}"
32
- unless File.exist?(file)
33
- if File.exist?("#{file}.html")
34
- env["PATH_INFO"] += ".html"
33
+ ends_with_slash = (path_info[-1] == '/')
34
+
35
+ outputs = @project.outputs.sort_by { |output| output.path }
36
+ outputs.each do |output|
37
+
38
+ output_path = "/" + output.path.to_s
39
+
40
+ if !ends_with_slash && output_path =~ %r{^#{path_info}/}
41
+ return [
42
+ 302,
43
+ { "Location" => path_info + "/" },
44
+ []
45
+ ]
35
46
  end
47
+
48
+ ["", ".html", "index.html"].map do |ext|
49
+ if output_path == (path_info + ext)
50
+ output.build
51
+ env["PATH_INFO"] += ext
52
+ return @app.call(env)
53
+ end
54
+ end
55
+
36
56
  end
37
57
 
38
58
  @app.call(env)
data/lib/pith/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pith
2
- VERSION = "0.2.3".freeze
2
+ VERSION = "0.3.0".freeze
3
3
  end
@@ -5,53 +5,90 @@ require "pith/project"
5
5
  describe Pith::Input do
6
6
 
7
7
  before do
8
- $input_dir.mkpath
9
8
  @project = Pith::Project.new(:input_dir => $input_dir)
10
9
  end
11
10
 
12
11
  def make_input(path)
12
+ path = Pathname(path)
13
13
  input_file = $input_dir + path
14
14
  input_file.parent.mkpath
15
15
  input_file.open("w") do |io|
16
16
  yield io if block_given?
17
17
  end
18
- @project.input(path)
18
+ Pith::Input.new(@project, path)
19
19
  end
20
20
 
21
- describe "#title" do
21
+ context "for a template" do
22
22
 
23
- it "is based on last component of filename" do
24
- @input = make_input("dir/some_page.html.haml")
25
- @input.title.should == "Some page"
23
+ subject do
24
+ make_input("dir/some_page.html.md.erb")
26
25
  end
27
26
 
28
- it "can be over-ridden in metadata" do
29
- @input = make_input("dir/some_page.html.haml") do |i|
30
- i.puts "---"
31
- i.puts "title: Blah blah"
32
- i.puts "..."
27
+ it { should be_template }
28
+
29
+ describe "#title" do
30
+
31
+ it "is based on last component of filename" do
32
+ subject.title.should == "Some page"
33
+ end
34
+
35
+ it "can be over-ridden in metadata" do
36
+ input = make_input("dir/some_page.html.haml") do |i|
37
+ i.puts "---"
38
+ i.puts "title: Blah blah"
39
+ i.puts "..."
40
+ end
41
+ input.title.should == "Blah blah"
42
+ end
43
+
44
+ end
45
+
46
+ describe "#output" do
47
+
48
+ it "returns an Output" do
49
+ subject.output.should_not be_nil
33
50
  end
34
- @input.title.should == "Blah blah"
51
+
52
+ end
53
+
54
+ describe "#output_path" do
55
+
56
+ it "excludes the template-type extensions" do
57
+ subject.output_path.should == Pathname("dir/some_page.html")
58
+ end
59
+
60
+ end
61
+
62
+ describe "#pipeline" do
63
+
64
+ it "is a list of Tilt processors" do
65
+ subject.pipeline.should == [Tilt["erb"], Tilt["md"]]
66
+ end
67
+
35
68
  end
36
69
 
37
70
  end
38
71
 
39
- describe "#output_path" do
72
+ context "for a resource" do
40
73
 
41
- it "excludes the template-type extension" do
42
- @input = make_input("dir/some_page.html.haml")
43
- @input.output_path.should == Pathname("dir/some_page.html")
74
+ subject do
75
+ make_input("dir/some_image.gif")
44
76
  end
45
77
 
78
+ it { should_not be_template }
79
+
80
+ its(:pipeline) { should be_empty }
81
+
46
82
  end
47
83
 
48
- describe "#pipeline" do
84
+ context "for an ignored file" do
49
85
 
50
- it "is a list of Tilt processors" do
51
- @input = make_input("dir/some_page.html.haml")
52
- @input.pipeline.should == [Tilt["haml"]]
86
+ subject do
87
+ make_input("_blah/blah.de")
53
88
  end
54
89
 
90
+ its(:output) { should be_nil }
91
+
55
92
  end
56
93
 
57
94
  end