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.
@@ -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