plate 0.5.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.
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