stasis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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