plate 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ ## Plate 0.5.0
2
+
3
+ * Initial build, basic site generation, asset compilation and template handling.
4
+ * Command line interface
5
+ * Rebuilding site with changes on demand with the `--watch` parameter
6
+ * Basic callbacks for manipulating page and site content
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2012 John D. Tornow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the 'Software'), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # Plate
2
+
3
+ Plate is a super simple static site generator and blog engine. It takes a folder full of Markdown files and turns it into a site that you can host anywhere. The output is a plain old static HTML site. In addition to basic formatting with Markdown, Plate also supports generating asset files with CoffeeScript, Sass and others.
4
+
5
+ ## Requirements
6
+
7
+ * Ruby 1.8.7, 1.9.2 or 1.9.3
8
+ * Bundler
9
+
10
+ ## Installation
11
+
12
+ gem install plate
13
+
14
+ Or, create a `Gemfile` and add:
15
+
16
+ gem 'plate'
17
+
18
+ ## Set up
19
+
20
+ To generate a new site with plate, run the following command:
21
+
22
+ plate new site-name/
23
+
24
+ ## Building a site
25
+
26
+ To build your site, run:
27
+
28
+ plate build
29
+
30
+ Or, just run:
31
+
32
+ plate
33
+
34
+ ## Directory Structure
35
+
36
+ Plate observes the following folder structure in your site:
37
+
38
+ * config/ - Put your global configuration settings here.
39
+ * content/ - All custom content for the site, besides blog posts. Everything in this folder will be copied over to the published site.
40
+ * layouts/ - Global layouts available for use on all content pages and posts.
41
+ * lib/ - Extend the basic functionality of Plate with plugins in this directory. All `.rb` files will be loaded automatically.
42
+ * posts/ - All blog post content for the site. Posts can be organized into sub-directories if you like.
43
+ * public/ - This will be generated if it does not exist, contains the produced site. Set this as the web server root to your site for development mode.
44
+
45
+ ## Extending Plate
46
+
47
+ Plate is meant to be extended easily. You might want to extend the basic functionality of Plate to add additional functionality for your site. To get started, create a directory named `lib` in the root of your site. Any Ruby files (ending in `.rb`) will be automatically loaded into the stack when Plate is run.
48
+
49
+ ### Callbacks
50
+
51
+ Callbacks are used to call certain blocks of code when an event happens in the lifecycle of building a site.
52
+
53
+ The callbacks currently available are:
54
+
55
+ * Site - `before_render`, `after_render`
56
+ * Page/Post - `before_render`, `after_render`
57
+
58
+ Example of a callback to be run when a site completes the build:
59
+
60
+ Plate::Site.register_callback :after_render do |site|
61
+ puts "the site finished rendering!"
62
+ end
63
+
64
+ ### Helpers
65
+
66
+ Helpers are modules that are automatically loaded into views. Any methods in the module will be available when you render a page.
67
+
68
+ An example of a helper file located in `lib/sample_helper.rb`
69
+
70
+ module SampleHelper
71
+ def sample_helper_method
72
+ "yes"
73
+ end
74
+ end
75
+
76
+ Then, in your `.erb` view you can call `sample_helper_method`.
77
+
78
+ ## Issues
79
+
80
+ If you have any issues or find bugs running Plate, please [report them on Github](https://github.com/jdtornow/plate/issues). While most functions should be stable, Plate is still in its infancy and certain issues may be present.
81
+
82
+ ## Testing
83
+
84
+ Plate is fully tested using Test Unit, Shoulda and Mocha. To run the test suite, `bundle install` then run:
85
+
86
+ rake test
87
+
88
+ ## License
89
+
90
+ Challah is released under the [MIT license](http://www.opensource.org/licenses/MIT)
91
+
92
+ Contributions and pull-requests are more than welcome.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'yard'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), *%w(lib)))
6
+
7
+ task :default => [ :test ]
8
+
9
+ require 'rake/testtask'
10
+
11
+ Rake::TestTask.new(:test) do |test|
12
+ test.libs << 'lib' << 'test'
13
+ test.pattern = 'test/**/test_*.rb'
14
+ test.verbose = true
15
+ end
16
+
17
+ namespace :test do
18
+ desc "Build the sample site and leave its contents in test/sample/public"
19
+ task :sample do
20
+ sh %q(ruby -I"lib:test" test/test_builder.rb -n /sample/)
21
+ end
22
+ end
23
+
24
+ YARD::Rake::YardocTask.new do |t|
25
+ end
26
+
27
+ task :doc => :yard
data/bin/plate ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'plate'
4
+ Plate::CLI.run!
data/lib/plate.rb ADDED
@@ -0,0 +1,56 @@
1
+ require 'yaml'
2
+ require 'fileutils'
3
+ require 'pathname'
4
+ require 'active_support/core_ext/hash'
5
+ require 'directory_watcher'
6
+ require 'tilt'
7
+
8
+ require 'plate/version'
9
+ require 'plate/errors'
10
+
11
+ module Plate
12
+ autoload :CLI, 'plate/cli'
13
+
14
+ autoload :Builder, 'plate/builder'
15
+ autoload :Callbacks, 'plate/callbacks'
16
+ autoload :Layout, 'plate/layout'
17
+ autoload :Site, 'plate/site'
18
+
19
+ autoload :BloggingHelper, 'plate/helpers/blogging_helper'
20
+ autoload :MetaHelper, 'plate/helpers/meta_helper'
21
+ autoload :URLHelper, 'plate/helpers/url_helper'
22
+
23
+ autoload :View, 'plate/view'
24
+
25
+ autoload :PostCollection, 'plate/post_collection'
26
+
27
+ autoload :Asset, 'plate/asset'
28
+ autoload :DynamicPage, 'plate/dynamic_page'
29
+ autoload :Page, 'plate/page'
30
+ autoload :Post, 'plate/post'
31
+ autoload :StaticPage, 'plate/static_page'
32
+
33
+ autoload :Engine, 'plate/engine'
34
+ autoload :HamlTemplate, 'plate/haml_template'
35
+ autoload :MarkdownTemplate, 'plate/markdown_template'
36
+ autoload :SassTemplate, 'plate/sass_template'
37
+ autoload :ScssTemplate, 'plate/scss_template'
38
+
39
+ extend Engine
40
+ @engines ||= {}
41
+
42
+ # Set up the basic engines that are supported by Plate. Add your own this same way.
43
+ # Thanks to sprockets for the inspiration.
44
+ # https://github.com/sstephenson/sprockets
45
+
46
+ # Assets
47
+ register_asset_engine :coffee, Tilt::CoffeeScriptTemplate
48
+ register_asset_engine :sass, SassTemplate
49
+ register_asset_engine :scss, ScssTemplate
50
+
51
+ # Layouts & Markup
52
+ register_template_engine :erb, Tilt::ERBTemplate
53
+ register_template_engine :haml, HamlTemplate
54
+ register_template_engine :md, MarkdownTemplate
55
+ register_template_engine :markdown, MarkdownTemplate
56
+ end
@@ -0,0 +1,47 @@
1
+ module Plate
2
+ # An asset is a CoffeeScript or Sass file that needs to be compiled before writing
3
+ # to the destination.
4
+ class Asset < Page
5
+ def engines
6
+ @engines ||= self.extensions.reverse.collect { |e| self.site.registered_asset_engines[e.gsub(/\./, '').to_sym] }.reject { |e| !e }
7
+ end
8
+
9
+ def extensions
10
+ @extensions ||= self.basename.scan(/\.[^.]+/)
11
+ end
12
+
13
+ def format_extension
14
+ self.extensions.reverse.detect { |e| !self.site.asset_engine_extensions.include?(e) }
15
+ end
16
+
17
+ def file_path
18
+ "#{directory}/#{file_name}"
19
+ end
20
+
21
+ # Same as page, but no layout applied
22
+ def rendered_content
23
+ return @rendered_content if @rendered_content
24
+
25
+ result = File.read(file)
26
+
27
+ self.engines.each do |engine|
28
+ template = engine.new() { result }
29
+ result = template.render(self, :site => self.site)
30
+ end
31
+
32
+ @rendered_content = result
33
+ end
34
+
35
+ # Write this page to the destination. For static files this just results
36
+ # in copying the file over to the destination
37
+ def write!
38
+ path = File.join(site.build_destination, file_path)
39
+
40
+ FileUtils.mkdir_p(File.dirname(path))
41
+
42
+ File.open(path, 'w') do |f|
43
+ f.write(self.rendered_content)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,375 @@
1
+ require 'yaml'
2
+ require 'digest'
3
+
4
+ module Plate
5
+ # Used by the command line tool to generate a site in the given directory.
6
+ class Builder
7
+ attr_accessor :source, :destination, :options, :site, :enable_logging, :helpers
8
+
9
+ def initialize(source, destination, options = {})
10
+ @source = source
11
+ @destination = destination
12
+ @options = Hash === options ? options.clone : {}
13
+ @options.symbolize_keys!
14
+ end
15
+
16
+ def cache_location
17
+ return @cache_location if @cache_location
18
+
19
+ if self.options.has_key?(:cache_location)
20
+ @cache_location ||= File.expand_path(self.options[:cache_location])
21
+ else
22
+ @cache_location ||= File.expand_path("~/.plate/#{self.id}")
23
+ end
24
+ end
25
+
26
+ # Remove any caches from this site build, also resets any variables for the caching and
27
+ # temporary build folders so they can be reset
28
+ def clear_cache!
29
+ FileUtils.rm_rf(cache_location)
30
+
31
+ @cache_location = nil
32
+ @tmp_destination = nil
33
+ @loaded = false
34
+ end
35
+
36
+ # A unique id for this site, based off of the source directory
37
+ def id
38
+ check_source!
39
+
40
+ @id ||= [ File.basename(source), Digest::MD5.hexdigest(source) ].collect { |s| s.to_s.downcase.parameterize }.join('-')
41
+ end
42
+
43
+ def items?
44
+ self.total_items > 0
45
+ end
46
+
47
+ def load!
48
+ unless @loaded
49
+ log('Site builder initialized.')
50
+
51
+ self.require_plugins!
52
+ self.load_config_file!
53
+ self.setup_site!
54
+ self.setup_tmp_directory!
55
+
56
+ @loaded = true
57
+ end
58
+
59
+ @loaded
60
+ end
61
+
62
+ def relative_path(file_or_directory)
63
+ file_or_directory.gsub(/^#{Regexp.quote(source)}(.*)$/, '\1')
64
+ end
65
+
66
+ def rebuild!
67
+ log('Re-rendering site...')
68
+
69
+ clear_cache!
70
+
71
+ self.site.reload!
72
+ self.render_site!
73
+ self.copy_to_destination!
74
+
75
+ true
76
+ end
77
+
78
+ # When watching a directory for changes, allow reloading of site content based on modifications
79
+ # only in the content, layouts and posts folder. Changes to config or lib files will need to
80
+ # be reloaded manually.
81
+ def reloadable?(relative_file)
82
+ relative_file =~ /^\/?(content|layouts|posts)\/(.*?)/
83
+ end
84
+
85
+ # Called to start the rendering of the site based on the provided, source, destination and config options.
86
+ def render!
87
+ @start_time = Time.now
88
+
89
+ log("Building full site...")
90
+
91
+ self.load!
92
+ self.render_site!
93
+ self.copy_to_destination!
94
+
95
+ @end_time = Time.now
96
+
97
+ log("Site build completed in #{timer} seconds")
98
+
99
+ true
100
+ end
101
+
102
+ def render_file!(relative_file_path)
103
+ self.load!
104
+
105
+ page = self.site.find(relative_file_path)
106
+
107
+ if page and page.file?
108
+ # if the file is a layout, rebuild all pages using it
109
+ if Layout === page
110
+ page.reload!
111
+
112
+ log("Building layout [#{page.relative_file}]")
113
+
114
+ self.site.find_by_layout(page.relative_file).each do |layout_page|
115
+ self.render_file!(layout_page.relative_file)
116
+ end
117
+ else
118
+ log("Building file [#{page.relative_file}]")
119
+
120
+ # Remove tmp file
121
+ existing_tmp = File.join(tmp_destination, page.file_path)
122
+
123
+ if File.exists?(existing_tmp)
124
+ FileUtils.rm_rf(existing_tmp)
125
+ end
126
+
127
+ page.reload!
128
+ page.write!
129
+
130
+ # File should exist again, even though we just removed it since we re-wrote it.
131
+ if File.exists?(existing_tmp)
132
+ existing = File.join(destination, page.file_path)
133
+
134
+ if File.exists?(existing)
135
+ log("Removing existing file [#{existing}]", :indent)
136
+ FileUtils.rm_rf(existing)
137
+ end
138
+
139
+ FileUtils.mkdir_p(File.dirname(existing))
140
+ FileUtils.cp(existing_tmp, existing)
141
+
142
+ log("File build complete.", :indent)
143
+ end
144
+ end
145
+ else
146
+ log("Cannot render file, it doesn't exist. [#{relative_file_path}]")
147
+ end
148
+
149
+ true
150
+ end
151
+
152
+ # Total number of all assets, posts and pages.
153
+ def total_items
154
+ return 0 unless self.site
155
+ @total_items ||= self.site.all_files.size
156
+ end
157
+
158
+ # Returns the time it took to run render! (in milliseconds)
159
+ def timer
160
+ return 0 unless @end_time and @start_time
161
+ ((@end_time - @start_time)).round(2)
162
+ end
163
+
164
+ # The directory path of where to put the files while the site is being built.
165
+ #
166
+ # If this value is nil, no temporary directory is used and files are built
167
+ # directly in the normal destination folder.
168
+ def tmp_destination
169
+ return @tmp_destination if @tmp_destination
170
+
171
+ result = ""
172
+
173
+ if self.options.has_key?(:tmp_destination)
174
+ if self.options[:tmp_destination]
175
+ result = File.expand_path(self.options[:tmp_destination])
176
+ end
177
+ else
178
+ result = File.join(cache_location, 'build-cache')
179
+ end
180
+
181
+ @tmp_destination = result
182
+ end
183
+
184
+ def tmp_destination?
185
+ self.tmp_destination.to_s.size > 0
186
+ end
187
+
188
+ protected
189
+ # Allows process to continue if the source directory exists. If the source directory does not
190
+ # exist, raise a source does not exist error.
191
+ def check_source!
192
+ raise SourceNotFound unless directory_exists?(source)
193
+ end
194
+
195
+ # Copy all files from within the tmp/ build directory into the actual destination.
196
+ #
197
+ # Warning: This will overwrite any files already in the destination.
198
+ def copy_to_destination!
199
+ if items?
200
+ self.setup_destination!
201
+
202
+ if tmp_destination?
203
+ log("Copying content to destination directory")
204
+ FileUtils.cp_r(Dir.glob("#{tmp_destination}**/*"), destination)
205
+ end
206
+ end
207
+ end
208
+
209
+ # Utility method for switching between ruby 1.8* and 1.9+
210
+ def directory_exists?(dir)
211
+ Dir.respond_to?(:exists?) ? Dir.exists?(dir) : File.directory?(dir)
212
+ end
213
+
214
+ # Loads the configuration options to use for rendering this site. By default, this information
215
+ # is loaded from a file located in config/plate.yml. If this file does not exist, no config
216
+ # data is loaded by default.
217
+ #
218
+ # You can specific additional options by passing them into the options block of this class:
219
+ #
220
+ # ## Custom Config File
221
+ #
222
+ # To load a different file, pass in the relative path of that file to the source root into the :config
223
+ # option:
224
+ #
225
+ # Builder.new(source, destination, :config => 'config/other-file.yml')
226
+ def load_config_file!
227
+ config_file = 'config/plate.yml'
228
+
229
+ # Check for provided config options
230
+ if options.has_key?(:config)
231
+ # If config is false, just return without loading anything.
232
+ if options[:config] == false
233
+ log("Skipping config file load.")
234
+ config_file = false
235
+ # If something is provided for config set the config_file
236
+ else
237
+ config_file = options[:config]
238
+ end
239
+ end
240
+
241
+ if config_file
242
+ config_file_path = File.join(self.source, config_file)
243
+
244
+ log("Checking for config file... [#{config_file_path}]")
245
+
246
+ # If the file doesn't exist, just ignore it. If the file exists, load and parse it.
247
+ if File.exists?(config_file_path)
248
+ yml = YAML.load_file(config_file_path)
249
+
250
+ if yml
251
+ yml.symbolize_keys!
252
+ @options = @options.reverse_merge(yml)
253
+ log("Options loaded from file", :indent)
254
+ end
255
+ end
256
+ end
257
+
258
+ # Make sure that the defaults are available.
259
+ @options.reverse_merge!({
260
+ :permalink => '/:category/:year/:month/:slug'
261
+ })
262
+ end
263
+
264
+ # Write to the log if enable_logging is enabled
265
+ def log(message, style = :arrow)
266
+ prefix = {
267
+ :arrow => ' -> ',
268
+ :indent => ' '
269
+ }[style] || style
270
+
271
+ puts "#{prefix}#{message}" if !!enable_logging
272
+ end
273
+
274
+ # Build out the site and store it in the destination directory
275
+ def render_site!
276
+ if items?
277
+ log("Rendering site...")
278
+
279
+ paths = []
280
+
281
+ self.site.run_callback(:before_render)
282
+
283
+ paths += self.site.assets.collect(&:write!)
284
+ paths += self.site.pages.collect(&:write!)
285
+ paths += self.site.posts.collect(&:write!)
286
+
287
+ @build_paths = paths
288
+
289
+ self.site.run_callback(:after_render)
290
+
291
+ log("Site rendered!", :indent)
292
+ else
293
+ log("No assets, posts or pages found. :(")
294
+ end
295
+ end
296
+
297
+ # Load any plugins and helpers in the ./lib folder. Any modules named with the
298
+ # format SomethingHelper will automatically be loaded into all views.
299
+ def require_plugins!
300
+ self.helpers = []
301
+
302
+ matcher = /^#{Regexp.quote(File.join(source, 'lib'))}\/?(.*).rb$/
303
+
304
+ plugins = Dir.glob(File.join(source, "lib/**/*.rb"))
305
+
306
+ if plugins.length > 0
307
+ log("Loading plugins...")
308
+
309
+ plugins.each do |file|
310
+ require file
311
+
312
+ underscore_name = file.sub(matcher, '\1')
313
+
314
+ # For helpers, make sure the module is defined, and add it to the helpers list
315
+ if underscore_name =~ /(.*?)_helper$/
316
+ class_name = underscore_name.classify
317
+
318
+ if defined? class_name
319
+ log("Loaded helper [#{class_name}]", :indent)
320
+
321
+ klass = class_name.constantize
322
+ self.helpers << klass
323
+
324
+ View.send(:include, klass)
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ # Clear out the destination directory, if it exists. Leave the root of the
332
+ # destination itself, but clear any files within it.
333
+ def setup_destination!
334
+ if directory_exists?(destination)
335
+ log("Clearing destination directory [#{destination}]")
336
+
337
+ FileUtils.rm_r(Dir.glob("#{destination}**/*"), :force => true)
338
+ elsif items?
339
+ log("Creating destination directory [#{destination}]")
340
+
341
+ FileUtils.mkdir_p(destination)
342
+ end
343
+ end
344
+
345
+ # Setup the Site instance and prepare it for loading
346
+ def setup_site!
347
+ log("Setting up site instance")
348
+
349
+ self.site = Site.new(source, destination, options)
350
+ self.site.logger = self
351
+ self.site.cache_location = self.cache_location
352
+
353
+ log("Site data loaded from source")
354
+ end
355
+
356
+ # Create a temporary folder to build everything in. Once the build was successful,
357
+ # all files will then be placed into the actual destination.
358
+ def setup_tmp_directory!
359
+ return unless tmp_destination?
360
+
361
+ log("Setting up tmp build directory [#{tmp_destination}]")
362
+
363
+ # Clear out any existing tmp folder contents
364
+ if directory_exists?(tmp_destination)
365
+ log("Clearing existing tmp directory content")
366
+
367
+ FileUtils.rm_rf(tmp_destination)
368
+ end
369
+
370
+ FileUtils.mkdir_p(tmp_destination)
371
+
372
+ self.site.build_destination = tmp_destination
373
+ end
374
+ end
375
+ end