stasis 0.1.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.
Files changed (69) hide show
  1. data/.gitignore +10 -0
  2. data/LICENSE +18 -0
  3. data/README.md +287 -0
  4. data/Rakefile +109 -0
  5. data/bin/stasis +30 -0
  6. data/config/gemsets.yml +9 -0
  7. data/config/gemspec.yml +17 -0
  8. data/lib/stasis.rb +291 -0
  9. data/lib/stasis/dev_mode.rb +55 -0
  10. data/lib/stasis/gems.rb +154 -0
  11. data/lib/stasis/plugin.rb +76 -0
  12. data/lib/stasis/plugins/before.rb +50 -0
  13. data/lib/stasis/plugins/helpers.rb +29 -0
  14. data/lib/stasis/plugins/ignore.rb +35 -0
  15. data/lib/stasis/plugins/instead.rb +15 -0
  16. data/lib/stasis/plugins/layout.rb +51 -0
  17. data/lib/stasis/plugins/priority.rb +41 -0
  18. data/lib/stasis/plugins/render.rb +71 -0
  19. data/lib/stasis/scope.rb +54 -0
  20. data/lib/stasis/scope/action.rb +25 -0
  21. data/lib/stasis/scope/controller.rb +62 -0
  22. data/lib/stasis/server.rb +90 -0
  23. data/site/arrow.png +0 -0
  24. data/site/controller.rb +72 -0
  25. data/site/github.png +0 -0
  26. data/site/index.html.haml +24 -0
  27. data/site/jquery-1.6.2.js +8982 -0
  28. data/site/stasis.css.scss +226 -0
  29. data/site/stasis.js.coffee +42 -0
  30. data/site/stasis.png +0 -0
  31. data/spec/fixtures/gemsets.yml +9 -0
  32. data/spec/fixtures/gemspec.yml +15 -0
  33. data/spec/fixtures/project/_partial.html.haml +1 -0
  34. data/spec/fixtures/project/before_render_partial.html.haml +1 -0
  35. data/spec/fixtures/project/before_render_text.html.haml +1 -0
  36. data/spec/fixtures/project/controller.rb +83 -0
  37. data/spec/fixtures/project/index.html.haml +16 -0
  38. data/spec/fixtures/project/layout.html.haml +3 -0
  39. data/spec/fixtures/project/layout_action.html.haml +1 -0
  40. data/spec/fixtures/project/layout_action_from_subdirectory.html.haml +1 -0
  41. data/spec/fixtures/project/layout_controller.html.haml +1 -0
  42. data/spec/fixtures/project/layout_controller_from_subdirectory.html.haml +1 -0
  43. data/spec/fixtures/project/no_controller/index.html.haml +12 -0
  44. data/spec/fixtures/project/not_dynamic.html +1 -0
  45. data/spec/fixtures/project/plugin.rb +16 -0
  46. data/spec/fixtures/project/subdirectory/_partial.html.haml +1 -0
  47. data/spec/fixtures/project/subdirectory/before_render_partial.html.haml +1 -0
  48. data/spec/fixtures/project/subdirectory/before_render_text.html.haml +1 -0
  49. data/spec/fixtures/project/subdirectory/controller.rb +66 -0
  50. data/spec/fixtures/project/subdirectory/ignore.html.haml +0 -0
  51. data/spec/fixtures/project/subdirectory/index.html.haml +14 -0
  52. data/spec/fixtures/project/subdirectory/layout.html.haml +3 -0
  53. data/spec/fixtures/project/subdirectory/layout_action.html.haml +1 -0
  54. data/spec/fixtures/project/subdirectory/layout_action_from_root.html.haml +1 -0
  55. data/spec/fixtures/project/subdirectory/layout_controller.html.haml +1 -0
  56. data/spec/fixtures/project/subdirectory/layout_controller_from_root.html.haml +1 -0
  57. data/spec/fixtures/project/time.html.haml +2 -0
  58. data/spec/spec_helper.rb +28 -0
  59. data/spec/stasis/gems_spec.rb +249 -0
  60. data/spec/stasis/plugins/before_spec.rb +53 -0
  61. data/spec/stasis/plugins/helpers_spec.rb +16 -0
  62. data/spec/stasis/plugins/ignore_spec.rb +17 -0
  63. data/spec/stasis/plugins/layout_spec.rb +22 -0
  64. data/spec/stasis/plugins/priority_spec.rb +22 -0
  65. data/spec/stasis/plugins/render_spec.rb +23 -0
  66. data/spec/stasis/server_spec.rb +29 -0
  67. data/spec/stasis_spec.rb +46 -0
  68. data/stasis.gemspec +32 -0
  69. metadata +227 -0
@@ -0,0 +1,9 @@
1
+ stasis:
2
+ directory_watcher: '~>1.4.0'
3
+ rake: '>=0.8.7'
4
+ redis: '~>2.2.1'
5
+ rocco: '~>0.8'
6
+ rspec: '~>1.0'
7
+ slop: '~>2.1.0'
8
+ tilt: '~>1.3'
9
+ yajl-ruby: '~>0.8.2'
@@ -0,0 +1,17 @@
1
+ name: stasis
2
+ version: 0.1.0
3
+ authors:
4
+ - Winton Welsh
5
+ email: mail@wintoni.us
6
+ homepage: http://wintoni.us
7
+ summary: Markup-agnostic static site generator
8
+ description: A markup-agnostic static site generator.
9
+ dependencies:
10
+ - directory_watcher
11
+ - redis
12
+ - slop
13
+ - tilt
14
+ - yajl-ruby
15
+ development_dependencies:
16
+ - rake
17
+ - rspec
data/lib/stasis.rb ADDED
@@ -0,0 +1,291 @@
1
+ # **Stasis** is a dynamic framework for static sites.
2
+
3
+ ### Prerequisites
4
+
5
+ require 'fileutils'
6
+
7
+ # `Stasis::Gems` handles loading of rubygems and gems listed in [config/gemsets.yml][ge].
8
+ #
9
+ # [ge]: https://github.com/winton/stasis/blob/master/config/gemsets.yml
10
+
11
+ require File.dirname(__FILE__) + '/stasis/gems'
12
+
13
+ # [Slim][sl] ships with its own [Tilt][ti] integration. If the user has [Slim][sl]
14
+ # installed, require it, otherwise don't worry about it.
15
+ #
16
+ # [sl]: http://slim-lang.com/
17
+ # [ti]: https://github.com/rtomayko/tilt
18
+
19
+ begin
20
+ require 'slim'
21
+ rescue Exception => e
22
+ end
23
+
24
+ # Activate the [Tilt][ti] gem.
25
+
26
+ Stasis::Gems.activate %w(tilt)
27
+
28
+ # Add the project directory to the load paths.
29
+
30
+ $:.unshift File.dirname(__FILE__)
31
+
32
+ # Require all Stasis library files.
33
+
34
+ require 'stasis/dev_mode'
35
+ require 'stasis/plugin'
36
+ require 'stasis/server'
37
+
38
+ require 'stasis/scope'
39
+ require 'stasis/scope/action'
40
+ require 'stasis/scope/controller'
41
+
42
+ require 'stasis/plugins/before'
43
+ require 'stasis/plugins/helpers'
44
+ require 'stasis/plugins/ignore'
45
+ require 'stasis/plugins/instead'
46
+ require 'stasis/plugins/layout'
47
+ require 'stasis/plugins/priority'
48
+ require 'stasis/plugins/render'
49
+
50
+ ### Public Interface
51
+
52
+ class Stasis
53
+
54
+ # `Action` -- changes with each iteration of the main loop within `Stasis#render`.
55
+ attr_accessor :action
56
+
57
+ # `Controller` -- set to the same instance for the lifetime of the `Stasis` instance.
58
+ attr_accessor :controller
59
+
60
+ # `String` -- the destination path passed to `Stasis.new`.
61
+ attr_accessor :destination
62
+
63
+ # `String` -- changes with each iteration of the main loop within `Stasis#render`.
64
+ attr_accessor :path
65
+
66
+ # `Array` -- all paths in the project that Stasis will act upon.
67
+ attr_accessor :paths
68
+
69
+ # `Options` -- options passed to `Stasis.new`.
70
+ attr_accessor :options
71
+
72
+ # `Array` -- `Plugin` instances.
73
+ attr_accessor :plugins
74
+
75
+ # `String` -- the root path passed to `Stasis.new`.
76
+ attr_accessor :root
77
+
78
+ def initialize(root, *args)
79
+ @options = {}
80
+ @options = args.pop if args.last.is_a?(::Hash)
81
+
82
+ @root = File.expand_path(root)
83
+ @destination = args[0] || @root + '/public'
84
+ @destination = File.expand_path(@destination, @root)
85
+
86
+ # Create an `Array` of paths that Stasis will act upon.
87
+ @paths = Dir["#{@root}/**/*"]
88
+
89
+ # Reject paths that are directories or within the destination directory.
90
+ @paths.reject! do |path|
91
+ !File.file?(path) || path[0..@destination.length-1] == @destination
92
+ end
93
+
94
+ # Create plugin instances.
95
+ @plugins = find_plugins.collect { |klass| klass.new(self) }
96
+
97
+ # Create a controller instance.
98
+ @controller = Controller.new(self)
99
+ end
100
+
101
+ def render(*only)
102
+ collect = {}
103
+ render_options = {}
104
+
105
+ if only.last.is_a?(::Hash)
106
+ render_options = only.pop
107
+ end
108
+
109
+ # Resolve paths given via the `only` parameter.
110
+ only = only.inject([]) do |array, path|
111
+ # If `path` is a regular expression...
112
+ if path.is_a?(::Regexp)
113
+ array << path
114
+ # If `root + path` exists...
115
+ elsif (path = File.expand_path(path, root)) && File.exists?(path)
116
+ array << path
117
+ # If `path` exists...
118
+ elsif File.exists?(path)
119
+ array << path
120
+ end
121
+ array
122
+ end
123
+
124
+ if only.empty?
125
+ # Remove old generated files.
126
+ FileUtils.rm_rf(destination)
127
+ end
128
+
129
+ # Reject paths that are controllers.
130
+ @paths.reject! do |path|
131
+ if File.basename(path) == 'controller.rb'
132
+ # Add controller to `Controller` instance.
133
+ @controller._add(path)
134
+ true
135
+ else
136
+ false
137
+ end
138
+ end
139
+
140
+ # Trigger all plugin `before_all` events.
141
+ trigger(:before_all)
142
+
143
+ @paths.uniq.each do |path|
144
+ @path = path
145
+
146
+ # If `only` parameters given...
147
+ unless only.empty?
148
+ # Skip iteration unless there is a match.
149
+ next unless only.any? do |only|
150
+ # Regular expression match.
151
+ (only.is_a?(::Regexp) && @path =~ only) ||
152
+ (
153
+ only.is_a?(::String) && (
154
+ # File match.
155
+ @path == only ||
156
+ # Directory match.
157
+ @path[0..only.length-1] == only
158
+ )
159
+ )
160
+ end
161
+ end
162
+
163
+ # Create an `Action` instance, the scope for rendering the view.
164
+ @action = Action.new(self)
165
+
166
+ # Set the extension if the `@path` extension is supported by [Tilt][ti].
167
+ ext =
168
+ Tilt.mappings.keys.detect do |ext|
169
+ File.extname(@path)[1..-1] == ext
170
+ end
171
+
172
+ # Trigger all plugin `before_render` events.
173
+ trigger(:before_render)
174
+
175
+ # Skip if `@path` set to `nil`.
176
+ next unless @path
177
+
178
+ # Render the view.
179
+ view =
180
+ # If the path has an extension supported by [Tilt][ti]...
181
+ if ext
182
+ # If the controller calls `render` within the `before` block for this
183
+ # path, receive output from `@action._render`.
184
+ #
185
+ # Otherwise, render the file located at `@path`.
186
+ output = @action._render || @action.render(@path, :callback => false)
187
+
188
+ # If a layout was specified via the `layout` method...
189
+ if @action._layout
190
+ # Render the layout with a block for the layout to `yield` to.
191
+ @action.render(@action._layout) { output }
192
+ # If a layout was not specified...
193
+ else
194
+ output
195
+ end
196
+ # If the path does not have an extension supported by [Tilt][ti] and `render` was
197
+ # called within the `before` block for this path...
198
+ elsif @action._render
199
+ @action._render
200
+ end
201
+
202
+ # Trigger all plugin `after_render` events.
203
+ trigger(:after_render)
204
+
205
+ # Cut the `root` out of the `path` to get the relative destination.
206
+ relative = @path[root.length..-1]
207
+
208
+ # Add `destination` (as specified from `Stasis.new`) to front of relative
209
+ # destination.
210
+ dest = "#{destination}#{relative}"
211
+
212
+ # Cut off the extension if the extension is supported by [Tilt][ti].
213
+ dest =
214
+ if ext && File.extname(dest) == ".#{ext}"
215
+ dest[0..-1*ext.length-2]
216
+ else
217
+ dest
218
+ end
219
+
220
+ # Create the directories leading up to the destination.
221
+ FileUtils.mkdir_p(File.dirname(dest))
222
+
223
+ # If markup was rendered...
224
+ if view
225
+ # Write the rendered markup to the destination.
226
+ if render_options[:write] != false
227
+ File.open(dest, 'w') do |f|
228
+ f.write(view)
229
+ end
230
+ end
231
+ # Collect render output.
232
+ if render_options[:collect]
233
+ collect[relative[1..-1]] = view
234
+ end
235
+ # If markup was not rendered and the path exists...
236
+ elsif File.exists?(@path)
237
+ # Copy the file located at the path to the destination path.
238
+ if render_options[:write] != false
239
+ FileUtils.cp(@path, dest)
240
+ end
241
+ end
242
+ end
243
+
244
+ # Trigger all plugin `after_all` events, passing the `Stasis` instance.
245
+ trigger(:after_all)
246
+
247
+ # Unset class-level instance variables.
248
+ @action, @path = nil, nil
249
+
250
+ # Respond with collected render output if `collect` option given.
251
+ collect if render_options[:collect]
252
+ end
253
+
254
+ # Add a plugin to all existing controller instances. This method should be called by
255
+ # all external plugins.
256
+ def self.register(plugin)
257
+ ObjectSpace.each_object(::Stasis) do |stasis|
258
+ plugin = plugin.new(stasis)
259
+ stasis.plugins << plugin
260
+ stasis.controller._bind_plugin(plugin, :controller_method)
261
+ end
262
+ end
263
+
264
+ # Trigger an event on every plugin in the controller.
265
+ def trigger(type)
266
+ each_priority do |priority|
267
+ @controller._send_to_plugin(priority, type)
268
+ end
269
+ end
270
+
271
+ private
272
+
273
+ # Iterate through plugin priority integers (sorted) and yield each to a block.
274
+ def each_priority(&block)
275
+ priorities = @plugins.collect do |plugin|
276
+ plugin.class._priority
277
+ end
278
+ priorities.uniq.sort.each(&block)
279
+ end
280
+
281
+ # Returns an `Array` of `Stasis::Plugin` classes.
282
+ def find_plugins
283
+ plugins = []
284
+ ObjectSpace.each_object(Class) do |klass|
285
+ if klass < ::Stasis::Plugin
286
+ plugins << klass
287
+ end
288
+ end
289
+ plugins
290
+ end
291
+ end
@@ -0,0 +1,55 @@
1
+ Stasis::Gems.activate %w(directory_watcher)
2
+ require 'directory_watcher'
3
+
4
+ class Stasis
5
+ class DevMode
6
+
7
+ def initialize(dir, options={})
8
+ trap("INT") { exit }
9
+
10
+ puts "\nDevelopment mode enabled: #{dir}"
11
+
12
+ @dir = dir
13
+ @options = options
14
+
15
+ render
16
+
17
+ dw = DirectoryWatcher.new(@stasis.root)
18
+ dw.interval = 1
19
+
20
+ Dir.chdir(@stasis.root) do
21
+ within_public = @stasis.destination[0..@stasis.root.length-1] == @stasis.root
22
+ rel_public = @stasis.destination[@stasis.root.length+1..-1] rescue nil
23
+ dw.glob = Dir["*"].inject(["*"]) do |array, path|
24
+ if File.directory?(path) && (!within_public || path != rel_public)
25
+ array << "#{path}/**/*"
26
+ end
27
+ array
28
+ end
29
+ end
30
+
31
+ dw.add_observer do |*events|
32
+ modified = events.detect { |e| e[:type] == :modified }
33
+ render if modified
34
+ end
35
+
36
+ dw.start
37
+ loop { sleep 1000 }
38
+ end
39
+
40
+ private
41
+
42
+ def render
43
+ puts "\n[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] Regenerating #{@options[:only] ? @options[:only].join(', ') : 'project'}..."
44
+ begin
45
+ @stasis = Stasis.new(*[ @dir, @options[:public], @options ].compact)
46
+ @stasis.render(*@options[:only])
47
+ rescue Exception => e
48
+ puts "\n[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] Error: #{e.message}`"
49
+ puts "\t#{e.backtrace.join("\n\t")}"
50
+ else
51
+ puts "\n[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] Complete"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,154 @@
1
+ unless defined?(Stasis::Gems)
2
+
3
+ require 'yaml'
4
+
5
+ class Stasis
6
+ module Gems
7
+ class <<self
8
+
9
+ attr_accessor :config
10
+ attr_reader :gemset, :gemsets, :versions
11
+
12
+ class SimpleStruct
13
+ attr_reader :hash
14
+
15
+ def initialize(hash)
16
+ @hash = hash
17
+ @hash.each do |key, value|
18
+ self.class.send(:define_method, key) { @hash[key] }
19
+ self.class.send(:define_method, "#{key}=") { |v| @hash[key] = v }
20
+ end
21
+ end
22
+ end
23
+
24
+ Gems.config = SimpleStruct.new(
25
+ :gemsets => [ "#{File.expand_path('../../../', __FILE__)}/config/gemsets.yml" ],
26
+ :gemspec => "#{File.expand_path('../../../', __FILE__)}/config/gemspec.yml",
27
+ :warn => true
28
+ )
29
+
30
+ def activate(*gems)
31
+ begin
32
+ require 'rubygems' unless defined?(::Gem)
33
+ rescue LoadError
34
+ puts "rubygems library could not be required" if @config.warn
35
+ end
36
+
37
+ self.gemset ||= gemset_from_loaded_specs
38
+
39
+ gems.flatten.collect { |g| g.to_sym }.each do |name|
40
+ version = @versions[name]
41
+ vendor = File.expand_path("../../../vendor/#{name}/lib", __FILE__)
42
+ if File.exists?(vendor)
43
+ $:.unshift vendor
44
+ elsif defined?(gem)
45
+ gem name.to_s, version
46
+ else
47
+ puts "#{name} #{"(#{version})" if version} failed to activate" if @config.warn
48
+ end
49
+ end
50
+ end
51
+
52
+ def dependencies
53
+ dependency_filter(@gemspec.dependencies, @gemset)
54
+ end
55
+
56
+ def development_dependencies
57
+ dependency_filter(@gemspec.development_dependencies, @gemset)
58
+ end
59
+
60
+ def gemset=(gemset)
61
+ if gemset
62
+ @gemset = gemset.to_sym
63
+
64
+ @gemsets = @config.gemsets.reverse.collect { |config|
65
+ if config.is_a?(::String)
66
+ YAML::load(File.read(config)) rescue {}
67
+ elsif config.is_a?(::Hash)
68
+ config
69
+ end
70
+ }.inject({}) do |hash, config|
71
+ deep_merge(hash, symbolize_keys(config))
72
+ end
73
+
74
+ @versions = (@gemsets[gemspec.name.to_sym] || {}).inject({}) do |hash, (key, value)|
75
+ if !value.is_a?(::Hash) && value
76
+ hash[key] = value
77
+ elsif key == @gemset
78
+ (value || {}).each { |k, v| hash[k] = v }
79
+ end
80
+ hash
81
+ end
82
+ else
83
+ @gemset = nil
84
+ @gemsets = nil
85
+ @versions = nil
86
+ end
87
+ end
88
+
89
+ def gemset_names
90
+ (
91
+ [ :default ] +
92
+ @gemsets[gemspec.name.to_sym].inject([]) { |array, (key, value)|
93
+ array.push(key) if value.is_a?(::Hash) || value.nil?
94
+ array
95
+ }
96
+ ).uniq
97
+ end
98
+
99
+ def gemspec(reload=false)
100
+ if @gemspec && !reload
101
+ @gemspec
102
+ else
103
+ data = YAML::load(File.read(@config.gemspec)) rescue {}
104
+ @gemspec = SimpleStruct.new(data)
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def deep_merge(first, second)
111
+ merger = lambda do |key, v1, v2|
112
+ Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2
113
+ end
114
+ first.merge(second, &merger)
115
+ end
116
+
117
+ def dependency_filter(dependencies, match)
118
+ (dependencies || []).inject([]) { |array, value|
119
+ if value.is_a?(::Hash)
120
+ array += value[match.to_s] if value[match.to_s]
121
+ else
122
+ array << value
123
+ end
124
+ array
125
+ }.uniq.collect(&:to_sym)
126
+ end
127
+
128
+ def gemset_from_loaded_specs
129
+ if defined?(Gem)
130
+ Gem.loaded_specs.each do |name, spec|
131
+ if name == gemspec.name
132
+ return :default
133
+ elsif name[0..gemspec.name.length] == "#{gemspec.name}-"
134
+ return name[gemspec.name.length+1..-1].to_sym
135
+ end
136
+ end
137
+ :default
138
+ else
139
+ :none
140
+ end
141
+ end
142
+
143
+ def symbolize_keys(hash)
144
+ return {} unless hash.is_a?(::Hash)
145
+ hash.inject({}) do |options, (key, value)|
146
+ value = symbolize_keys(value) if value.is_a?(::Hash)
147
+ options[(key.to_sym rescue key) || key] = value
148
+ options
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end