pith 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/pith CHANGED
@@ -69,10 +69,7 @@ class PithCommand < Clamp::Command
69
69
  signal_usage_error %(No "#{pith_dir}" directory ... this doesn't look right!)
70
70
  end
71
71
  puts %{Generating to "#{output_dir}"}
72
- @project = Pith::Project.new(
73
- :input_dir => input_dir, :output_dir => output_dir,
74
- :logger => Pith::ConsoleLogger.new
75
- )
72
+ @project = Pith::Project.new(input_dir, output_dir, :logger => Pith::ConsoleLogger.new)
76
73
  end
77
74
  end
78
75
 
@@ -22,3 +22,51 @@ Scenario: don't alter an input, and the output is untouched
22
22
  When I rebuild the site
23
23
 
24
24
  Then output file "page.html" should not be re-generated
25
+
26
+ Scenario: alter a dependency
27
+
28
+ Given input file "page.html.haml" contains "= include('_partials/foo.haml')"
29
+ And input file "_partials/foo.haml" contains "bananas"
30
+ And the site is up-to-date
31
+
32
+ When I change input file "_partials/foo.haml" to contain "New content"
33
+ And I rebuild the site
34
+
35
+ Then output file "page.html" should be re-generated
36
+ And output file "page.html" should contain "New content"
37
+
38
+ Scenario: delete a dependency
39
+
40
+ Given input file "fruits.html.haml" contains
41
+ """
42
+ - project.inputs.map(&:path).map(&:to_s).grep(/^_fruit/).sort.each do |input_name|
43
+ %p= include(input_name)
44
+ """
45
+
46
+ And input file "_fruit/a.html.haml" contains
47
+ """
48
+ apple
49
+ """
50
+
51
+ And input file "_fruit/b.html.haml" contains
52
+ """
53
+ banana
54
+ """
55
+
56
+ And the site is up-to-date
57
+
58
+ Then output file "fruits.html" should contain
59
+ """
60
+ <p>apple</p>
61
+ <p>banana</p>
62
+ """
63
+
64
+ When I remove input file "_fruit/a.html.haml"
65
+ And I rebuild the site
66
+
67
+ Then output file "fruits.html" should be re-generated
68
+ And output file "fruits.html" should contain
69
+ """
70
+ <p>banana</p>
71
+ """
72
+
@@ -3,11 +3,6 @@ Feature: linking between files
3
3
  I want to be able to generate relative reference to other pages
4
4
  So that the generated site is re-locateable
5
5
 
6
- Background:
7
-
8
- Given the "assume_content_negotiation" flag is disabled
9
- And the "assume_directory_index" flag is disabled
10
-
11
6
  Scenario: link from one top-level page to another
12
7
 
13
8
  Given input file "index.html.haml" contains
@@ -148,7 +143,11 @@ Scenario: link to a missing resource
148
143
 
149
144
  Scenario: assume content negotiation
150
145
 
151
- Given the "assume_content_negotiation" flag is enabled
146
+ Given the config file contains
147
+ """
148
+ project.assume_content_negotiation = true
149
+ """
150
+
152
151
  And input file "index.html.haml" contains
153
152
  """
154
153
  = link("page.html", "Page")
@@ -162,7 +161,10 @@ Scenario: assume content negotiation
162
161
 
163
162
  Scenario: link to an index page
164
163
 
165
- Given the "assume_directory_index" flag is enabled
164
+ Given the config file contains
165
+ """
166
+ project.assume_directory_index = true
167
+ """
166
168
 
167
169
  And input file "page.html.haml" contains
168
170
  """
@@ -177,7 +179,10 @@ Scenario: link to an index page
177
179
 
178
180
  Scenario: link to an index page in the same directory
179
181
 
180
- Given the "assume_directory_index" flag is enabled
182
+ Given the config file contains
183
+ """
184
+ project.assume_directory_index = true
185
+ """
181
186
 
182
187
  And input file "page.html.haml" contains
183
188
  """
@@ -14,12 +14,8 @@ Given "the site is up-to-date" do
14
14
  When "I build the site"
15
15
  end
16
16
 
17
- Given /^the "([^\"]*)" flag is enabled$/ do |flag|
18
- @project.send("#{flag}=", true)
19
- end
20
-
21
- Given /^the "([^\"]*)" flag is disabled$/ do |flag|
22
- @project.send("#{flag}=", false)
17
+ Given /^the config file contains$/ do |content|
18
+ @inputs.write("_pith/config.rb", content, :mtime => (Time.now - 5))
23
19
  end
24
20
 
25
21
  When /^I change input file "([^\"]*)" to contain "([^\"]*)"$/ do |path, content|
@@ -1,7 +1,7 @@
1
1
  require "pathname"
2
2
 
3
3
  class DirHash
4
-
4
+
5
5
  def initialize(dir)
6
6
  @dir = Pathname(dir)
7
7
  end
@@ -34,7 +34,7 @@ class DirHash
34
34
  file_path.utime(timestamp, timestamp)
35
35
  end
36
36
  end
37
-
37
+
38
38
  end
39
39
 
40
40
  class InternalLogger
@@ -42,13 +42,13 @@ class InternalLogger
42
42
  def initialize
43
43
  @messages = []
44
44
  end
45
-
45
+
46
46
  attr_reader :messages
47
-
47
+
48
48
  def clear
49
49
  @messages.clear
50
50
  end
51
-
51
+
52
52
  def info(message, &block)
53
53
  message ||= block.call
54
54
  write(message)
@@ -56,11 +56,11 @@ class InternalLogger
56
56
 
57
57
  alias :warn :info
58
58
  alias :debug :info
59
-
59
+
60
60
  def write(message)
61
61
  @messages << message
62
62
  end
63
-
63
+
64
64
  end
65
65
 
66
66
  $project_dir = Pathname(__FILE__).expand_path.parent.parent.parent
@@ -79,8 +79,7 @@ Before do
79
79
  dir.rmtree if dir.exist?
80
80
  dir.mkpath
81
81
  end
82
- @project = Pith::Project.new(:input_dir => $input_dir, :output_dir => $output_dir)
83
- @project.logger = InternalLogger.new
82
+ @project = Pith::Project.new($input_dir, $output_dir, :logger => InternalLogger.new)
84
83
  @inputs = DirHash.new($input_dir)
85
84
  @outputs = DirHash.new($output_dir)
86
85
  end
@@ -0,0 +1,42 @@
1
+ module Pith
2
+
3
+ class Config
4
+
5
+ DEFAULT_IGNORE_PATTERNS = ["_*", ".git", ".gitignore", ".svn", ".sass-cache", "*~", "*.sw[op]"].to_set.freeze
6
+
7
+ def initialize
8
+ @ignore_patterns = DEFAULT_IGNORE_PATTERNS.dup
9
+ @helper_module = Module.new
10
+ end
11
+
12
+ attr_accessor :assume_content_negotiation
13
+ attr_accessor :assume_directory_index
14
+
15
+ attr_reader :ignore_patterns
16
+
17
+ def ignore(pattern)
18
+ ignore_patterns << pattern
19
+ end
20
+
21
+ attr_reader :helper_module
22
+
23
+ def helpers(&block)
24
+ helper_module.module_eval(&block)
25
+ end
26
+
27
+ class << self
28
+
29
+ def load(config_file)
30
+ config = self.new
31
+ if config_file.exist?
32
+ project = config # for backward compatibility
33
+ eval(config_file.read, binding, config_file.to_s, 1)
34
+ end
35
+ config
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,36 @@
1
+ require "pith/config"
2
+ require "pith/observable"
3
+
4
+ module Pith
5
+
6
+ class ConfigProvider
7
+
8
+ include Pith::Observable
9
+
10
+ def initialize(project)
11
+ @project = project
12
+ @last_load_mtime = :never
13
+ sync
14
+ end
15
+
16
+ attr_reader :config
17
+
18
+ def sync
19
+ config_mtime = config_file.mtime rescue nil
20
+ unless config_mtime == @last_load_mtime
21
+ @last_load_mtime = config_mtime
22
+ @project.logger.debug "loading config"
23
+ @config = Pith::Config.load(config_file)
24
+ notify_observers
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def config_file
31
+ @project.input_dir + "_pith/config.rb"
32
+ end
33
+
34
+ end
35
+
36
+ end
data/lib/pith/input.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "fileutils"
2
2
  require "pathname"
3
- require "observer"
3
+ require "pith/observable"
4
4
  require "pith/output"
5
5
  require "tilt"
6
6
  require "yaml"
@@ -9,13 +9,12 @@ module Pith
9
9
 
10
10
  class Input
11
11
 
12
- include Observable
12
+ include Pith::Observable
13
13
 
14
14
  def initialize(project, path)
15
15
  @project = project
16
16
  @path = path
17
17
  determine_pipeline
18
- when_created
19
18
  end
20
19
 
21
20
  attr_reader :project, :path
@@ -37,7 +36,7 @@ module Pith
37
36
  #
38
37
  def ignorable?
39
38
  @ignorable ||= path.each_filename do |path_component|
40
- project.ignore_patterns.each do |pattern|
39
+ project.config.ignore_patterns.each do |pattern|
41
40
  return true if File.fnmatch(pattern, path_component)
42
41
  end
43
42
  end
@@ -126,37 +125,20 @@ module Pith
126
125
  end
127
126
  end
128
127
 
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
128
+ def when_added
146
129
  log_lifecycle "+"
147
- @last_mtime = file.mtime
148
130
  end
149
131
 
150
- def when_changed
132
+ def when_modified
151
133
  log_lifecycle "~"
152
134
  unload if loaded?
153
- changed(true)
154
135
  notify_observers
155
136
  end
156
137
 
157
- def when_deleted
138
+ def when_removed
158
139
  log_lifecycle "X"
159
140
  output.delete if output
141
+ notify_observers
160
142
  end
161
143
 
162
144
  private
@@ -0,0 +1,27 @@
1
+ module Pith
2
+
3
+ module Observable
4
+
5
+ def add_observer(observer, signal = :update)
6
+ observer_map[observer] = signal
7
+ end
8
+
9
+ def remove_observer(observer)
10
+ observer_map.delete(observer)
11
+ end
12
+
13
+ def notify_observers
14
+ observer_map.each do |observer, signal|
15
+ observer.send(signal)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def observer_map
22
+ @observer_map ||= {}
23
+ end
24
+
25
+ end
26
+
27
+ end
data/lib/pith/output.rb CHANGED
@@ -45,32 +45,30 @@ module Pith
45
45
  def record_dependency_on(*inputs)
46
46
  inputs.each do |input|
47
47
  @dependencies << input
48
- input.add_observer(self)
48
+ input.add_observer(self, :invalidate)
49
49
  end
50
50
  end
51
51
 
52
52
  def delete
53
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
54
+ if file.exist?
55
+ logger.info("--X #{path}")
56
+ FileUtils.rm_f(file)
57
+ end
60
58
  end
61
59
 
62
- private
63
-
64
60
  def invalidate
65
61
  if @generated
66
62
  @dependencies.each do |d|
67
- d.delete_observer(self)
63
+ d.remove_observer(self)
68
64
  end
69
65
  @dependencies = nil
70
66
  @generated = nil
71
67
  end
72
68
  end
73
69
 
70
+ private
71
+
74
72
  def copy_resource
75
73
  FileUtils.copy(input.file, file)
76
74
  record_dependency_on(input)
@@ -89,7 +87,7 @@ module Pith
89
87
  out.puts exception_summary(e)
90
88
  end
91
89
  end
92
- record_dependency_on(*project.config_inputs)
90
+ record_dependency_on(project.config_provider)
93
91
  end
94
92
 
95
93
  def logger
@@ -7,12 +7,6 @@ class Pathname
7
7
  utime(mtime, mtime) if mtime
8
8
  end
9
9
 
10
- def all_files(pattern = "**/*")
11
- Pathname.glob(self + pattern, File::FNM_DOTMATCH).select do |path|
12
- path.file?
13
- end
14
- end
15
-
16
10
  def in?(dir)
17
11
  prefix = "#{dir}/"
18
12
  self.to_s[0,prefix.length] == prefix
data/lib/pith/project.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "logger"
2
+ require "pith/config_provider"
2
3
  require "pith/input"
3
4
  require "pith/pathname_ext"
4
5
  require "pith/reference_error"
@@ -9,37 +10,20 @@ module Pith
9
10
 
10
11
  class Project
11
12
 
12
- DEFAULT_IGNORE_PATTERNS = ["_*", ".git", ".gitignore", ".svn", ".sass-cache", "*~", "*.sw[op]"].to_set.freeze
13
-
14
- def initialize(attributes = {})
15
- @ignore_patterns = DEFAULT_IGNORE_PATTERNS.dup
13
+ def initialize(input_dir, output_dir = nil, attributes = {})
14
+ @input_dir = Pathname(input_dir)
15
+ @output_dir = output_dir ? Pathname(output_dir) : (@input_dir + "_out")
16
+ @input_map = {}
17
+ @output_map = {}
16
18
  attributes.each do |k,v|
17
19
  send("#{k}=", v)
18
20
  end
21
+ FileUtils.rm_rf(output_dir.to_s)
19
22
  end
20
23
 
21
24
  attr_reader :input_dir
22
- attr_reader :ignore_patterns
23
-
24
- def input_dir=(dir)
25
- @input_dir = Pathname(dir)
26
- end
27
-
28
25
  attr_reader :output_dir
29
26
 
30
- def output_dir=(dir)
31
- @output_dir = Pathname(dir)
32
- FileUtils.rm_rf(@output_dir)
33
- @output_dir.mkpath
34
- end
35
-
36
- attr_accessor :assume_content_negotiation
37
- attr_accessor :assume_directory_index
38
-
39
- def ignore(pattern)
40
- ignore_patterns << pattern
41
- end
42
-
43
27
  # Public: get inputs
44
28
  #
45
29
  # Returns Pith::Input objects representing the files in the input_dir.
@@ -90,12 +74,8 @@ module Pith
90
74
  # Public: re-sync with the file-system.
91
75
  #
92
76
  def sync
93
- @input_map ||= {}
94
- @output_map ||= {}
95
- @config_inputs = nil
96
- load_config
97
- validate_known_inputs
98
- find_new_inputs
77
+ config_provider.sync
78
+ sync_input_files
99
79
  end
100
80
 
101
81
  def sync_every(period)
@@ -111,7 +91,7 @@ module Pith
111
91
  #
112
92
  # Returns true if any errors were encountered during the last build.
113
93
  def has_errors?
114
- inputs.map(&:output).compact.any?(&:error)
94
+ outputs.any?(&:error)
115
95
  end
116
96
 
117
97
  def last_built_at
@@ -122,56 +102,59 @@ module Pith
122
102
  @logger ||= Logger.new(nil)
123
103
  end
124
104
 
125
- attr_writer :logger
126
-
127
- def helpers(&block)
128
- helper_module.module_eval(&block)
105
+ def config_provider
106
+ @config_provider ||= Pith::ConfigProvider.new(self)
129
107
  end
130
108
 
131
- def helper_module
132
- @helper_module ||= Module.new
133
- end
134
-
135
- def config_inputs
136
- @config_inputs ||= inputs.select do |input|
137
- input.path.to_s[0,6] == "_pith/"
138
- end
109
+ def config
110
+ config_provider.config
139
111
  end
140
112
 
141
113
  private
142
114
 
143
- def load_config
144
- config_file = input_dir + "_pith/config.rb"
145
- project = self
146
- if config_file.exist?
147
- eval(config_file.read, binding, config_file.to_s, 1)
115
+ attr_writer :logger
116
+
117
+ def sync_input_files
118
+ @mtimes ||= {}
119
+ removed_file_paths = @mtimes.keys
120
+ Pathname.glob(input_dir + "**/*", File::FNM_DOTMATCH) do |file|
121
+ next unless file.file?
122
+ next if file.in?(output_dir)
123
+ mtime = file.mtime
124
+ path = file.relative_path_from(input_dir)
125
+ if @mtimes.has_key?(path)
126
+ if @mtimes[path] < mtime
127
+ when_file_modified(path)
128
+ end
129
+ else
130
+ when_file_added(path)
131
+ end
132
+ @mtimes[path] = mtime
133
+ removed_file_paths.delete(path)
134
+ end
135
+ removed_file_paths.each do |path|
136
+ when_file_removed(path)
148
137
  end
149
138
  end
150
139
 
151
- def load_input(path)
140
+ def when_file_added(path)
152
141
  i = Input.new(self, path)
142
+ i.when_added
153
143
  @input_map[path] = i
154
144
  if o = i.output
155
145
  @output_map[o.path] = o
156
146
  end
157
- i
158
147
  end
159
148
 
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
167
- end
149
+ def when_file_modified(path)
150
+ @input_map[path].when_modified
168
151
  end
169
152
 
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)
153
+ def when_file_removed(path)
154
+ i = @input_map.delete(path)
155
+ i.when_removed
156
+ if o = i.output
157
+ @output_map.delete(o.path)
175
158
  end
176
159
  end
177
160
 
@@ -15,7 +15,7 @@ module Pith
15
15
  @page = @output.input
16
16
  @project = @page.project
17
17
  @input_stack = []
18
- self.extend(project.helper_module)
18
+ self.extend(project.config.helper_module)
19
19
  end
20
20
 
21
21
  attr_reader :output
@@ -55,8 +55,8 @@ module Pith
55
55
 
56
56
  def relative_url_to(target_path)
57
57
  url = target_path.relative_path_from(page.path.parent).to_s
58
- url = url.sub(/index\.html$/, "") if project.assume_directory_index
59
- url = url.sub(/\.html$/, "") if project.assume_content_negotiation
58
+ url = url.sub(/index\.html$/, "") if project.config.assume_directory_index
59
+ url = url.sub(/\.html$/, "") if project.config.assume_content_negotiation
60
60
  url = "./" if url.empty?
61
61
  Pathname(url)
62
62
  end
data/lib/pith/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pith
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.3.1".freeze
3
3
  end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'pith/config'
3
+
4
+ describe Pith::Config do
5
+
6
+ let(:config) { Pith::Config.new }
7
+
8
+ describe "#ignore_patterns" do
9
+
10
+ it "is a set" do
11
+ config.ignore_patterns.should be_kind_of(Set)
12
+ end
13
+
14
+ it "includes some sensible defaults" do
15
+ config.ignore_patterns.should include("_*")
16
+ config.ignore_patterns.should include(".git")
17
+ config.ignore_patterns.should include(".svn")
18
+ end
19
+
20
+ end
21
+
22
+ describe "#ignore" do
23
+
24
+ it "adds to ignore_patterns" do
25
+ config.ignore("foo")
26
+ config.ignore_patterns
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -5,7 +5,7 @@ require "pith/project"
5
5
  describe Pith::Input do
6
6
 
7
7
  before do
8
- @project = Pith::Project.new(:input_dir => $input_dir)
8
+ @project = Pith::Project.new($input_dir)
9
9
  end
10
10
 
11
11
  def make_input(path)
@@ -4,7 +4,7 @@ require 'pith/project'
4
4
  describe Pith::Project do
5
5
 
6
6
  before do
7
- @project = Pith::Project.new(:input_dir => $input_dir, :output_dir => $output_dir)
7
+ @project = Pith::Project.new($input_dir, $output_dir)
8
8
  end
9
9
 
10
10
  describe "#build" do
@@ -110,27 +110,4 @@ describe Pith::Project do
110
110
 
111
111
  end
112
112
 
113
- describe "#ignore_patterns" do
114
-
115
- it "is a set" do
116
- @project.ignore_patterns.should be_kind_of(Set)
117
- end
118
-
119
- it "includes some sensible defaults" do
120
- @project.ignore_patterns.should include("_*")
121
- @project.ignore_patterns.should include(".git")
122
- @project.ignore_patterns.should include(".svn")
123
- end
124
-
125
- end
126
-
127
- describe "#ignore" do
128
-
129
- it "adds to ignore_patterns" do
130
- @project.ignore("foo")
131
- @project.ignore_patterns
132
- end
133
-
134
- end
135
-
136
113
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pith
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-04-29 00:00:00.000000000 Z
12
+ date: 2012-05-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: tilt
@@ -85,11 +85,14 @@ executables:
85
85
  extensions: []
86
86
  extra_rdoc_files: []
87
87
  files:
88
+ - lib/pith/config.rb
89
+ - lib/pith/config_provider.rb
88
90
  - lib/pith/console_logger.rb
89
91
  - lib/pith/console_logger.rb~
90
92
  - lib/pith/input.rb
91
93
  - lib/pith/input.rb~
92
94
  - lib/pith/metadata.rb~
95
+ - lib/pith/observable.rb
93
96
  - lib/pith/output.rb
94
97
  - lib/pith/pathname_ext.rb
95
98
  - lib/pith/pathname_ext.rb~
@@ -120,6 +123,7 @@ files:
120
123
  - sample/stylesheets/app.css.sass
121
124
  - README.markdown
122
125
  - Rakefile
126
+ - spec/pith/config_spec.rb
123
127
  - spec/pith/input_spec.rb
124
128
  - spec/pith/pathname_ext_spec.rb
125
129
  - spec/pith/plugins/publication_spec.rb
@@ -176,7 +180,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
176
180
  version: '0'
177
181
  segments:
178
182
  - 0
179
- hash: -22568693852038165
183
+ hash: 1629984080237938528
180
184
  required_rubygems_version: !ruby/object:Gem::Requirement
181
185
  none: false
182
186
  requirements:
@@ -185,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
189
  version: '0'
186
190
  segments:
187
191
  - 0
188
- hash: -22568693852038165
192
+ hash: 1629984080237938528
189
193
  requirements: []
190
194
  rubyforge_project:
191
195
  rubygems_version: 1.8.21
@@ -194,6 +198,7 @@ specification_version: 3
194
198
  summary: A static website generator
195
199
  test_files:
196
200
  - Rakefile
201
+ - spec/pith/config_spec.rb
197
202
  - spec/pith/input_spec.rb
198
203
  - spec/pith/pathname_ext_spec.rb
199
204
  - spec/pith/plugins/publication_spec.rb