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 +6 -0
- data/LICENSE +21 -0
- data/README.md +92 -0
- data/Rakefile +27 -0
- data/bin/plate +4 -0
- data/lib/plate.rb +56 -0
- data/lib/plate/asset.rb +47 -0
- data/lib/plate/builder.rb +375 -0
- data/lib/plate/callbacks.rb +39 -0
- data/lib/plate/cli.rb +203 -0
- data/lib/plate/dynamic_page.rb +27 -0
- data/lib/plate/engine.rb +25 -0
- data/lib/plate/errors.rb +10 -0
- data/lib/plate/haml_template.rb +18 -0
- data/lib/plate/helpers/blogging_helper.rb +102 -0
- data/lib/plate/helpers/meta_helper.rb +71 -0
- data/lib/plate/helpers/url_helper.rb +11 -0
- data/lib/plate/layout.rb +169 -0
- data/lib/plate/markdown_template.rb +22 -0
- data/lib/plate/page.rb +280 -0
- data/lib/plate/post.rb +134 -0
- data/lib/plate/post_collection.rb +116 -0
- data/lib/plate/sass_template.rb +40 -0
- data/lib/plate/scss_template.rb +9 -0
- data/lib/plate/site.rb +249 -0
- data/lib/plate/static_page.rb +37 -0
- data/lib/plate/version.rb +3 -0
- data/lib/plate/view.rb +48 -0
- data/lib/templates/config.yml +1 -0
- data/lib/templates/index.md +6 -0
- data/lib/templates/layout.erb +12 -0
- metadata +143 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module Plate
|
2
|
+
module Callbacks
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:include, InstanceMethods)
|
5
|
+
base.send(:extend, ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def callbacks
|
10
|
+
@callbacks ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def register_callback(name, method_name = nil, &block)
|
14
|
+
callbacks[name] ||= []
|
15
|
+
callbacks[name] << (block || method_name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
def around_callback(name, &block)
|
21
|
+
run_callback "before_#{name}".to_sym
|
22
|
+
block.call
|
23
|
+
run_callback "after_#{name}".to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
def run_callback(name)
|
27
|
+
if callbacks = self.class.callbacks[name]
|
28
|
+
callbacks.each do |callback|
|
29
|
+
if Proc === callback
|
30
|
+
callback.call(self)
|
31
|
+
elsif self.respond_to?(callback)
|
32
|
+
self.send(callback)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/plate/cli.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
module Plate
|
2
|
+
# The CLI class controls the behavior of plate when it is used as a command line interface.
|
3
|
+
class CLI
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
attr_accessor :source, :destination, :args, :options
|
7
|
+
|
8
|
+
def initialize(args = [])
|
9
|
+
@args = String === args ? args.split(' ') : args.dup
|
10
|
+
@options = {}
|
11
|
+
|
12
|
+
# Some defaults
|
13
|
+
@source = Dir.pwd
|
14
|
+
@destination = File.join(@source, 'public')
|
15
|
+
end
|
16
|
+
|
17
|
+
def builder
|
18
|
+
@builder ||= Builder.new(self.source, self.destination, self.options)
|
19
|
+
end
|
20
|
+
|
21
|
+
# The current command to be run. Pulled from the args attribute.
|
22
|
+
def command
|
23
|
+
self.args.size > 0 ? self.args[0] : 'build'
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_options!
|
27
|
+
options = {}
|
28
|
+
|
29
|
+
opts = OptionParser.new do |opts|
|
30
|
+
banner = "Usage: plate [command] [options]"
|
31
|
+
|
32
|
+
opts.on('--destination [PATH]', '-d', 'Set the destination directory for this build.') do |d|
|
33
|
+
@destination = File.expand_path(d)
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('--source [PATH]', '-s', 'Set the source directory for this build.') do |s|
|
37
|
+
@source = File.expand_path(s)
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on('--verbose', '-V', 'Show output about the generation of the site.') do
|
41
|
+
options[:verbose] = true
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on('--version', '-v', 'Show the current Plate version number.') do
|
45
|
+
puts "You're running Plate version #{Plate::VERSION}!"
|
46
|
+
exit 0
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on('--watch', '-w', 'Watch the source directory for changes.') do
|
50
|
+
options[:watch] = true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.parse!(self.args)
|
55
|
+
|
56
|
+
@options = options
|
57
|
+
end
|
58
|
+
|
59
|
+
# Run the given command. If the command does not exist, nothing will be run.
|
60
|
+
def run
|
61
|
+
parse_options!
|
62
|
+
|
63
|
+
command_name = "run_#{command}_command".to_sym
|
64
|
+
|
65
|
+
if self.respond_to?(command_name)
|
66
|
+
# remove command name
|
67
|
+
self.args.shift
|
68
|
+
self.send(command_name)
|
69
|
+
else
|
70
|
+
puts "Command #{command} not found"
|
71
|
+
return false
|
72
|
+
end
|
73
|
+
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
class << self
|
78
|
+
def run!
|
79
|
+
new(ARGV).run
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
def process_file_change(event)
|
85
|
+
relative_path = builder.relative_path(event.path)
|
86
|
+
|
87
|
+
unless relative_path.start_with?('/public/')
|
88
|
+
if builder.reloadable?(relative_path)
|
89
|
+
case event.type
|
90
|
+
when :added
|
91
|
+
puts " -> New File: #{relative_path}"
|
92
|
+
builder.rebuild!
|
93
|
+
when :modified
|
94
|
+
puts " -> Changed File: #{relative_path}"
|
95
|
+
builder.render_file!(relative_path)
|
96
|
+
when :removed
|
97
|
+
puts " -> Deleted File: #{relative_path}"
|
98
|
+
builder.rebuild!
|
99
|
+
end
|
100
|
+
else
|
101
|
+
puts " -> Modified file [#{relative_path}]"
|
102
|
+
puts " Re-run `plate build` to see the changes."
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def run_build_command
|
108
|
+
puts "Building your site from #{source} to #{destination}"
|
109
|
+
|
110
|
+
builder.enable_logging = true if options[:verbose]
|
111
|
+
builder.render!
|
112
|
+
|
113
|
+
if builder.items?
|
114
|
+
# If we want to watch directories, keep the chain open
|
115
|
+
if options[:watch]
|
116
|
+
puts "Initial site build complete. Watching #{source} for changes..."
|
117
|
+
|
118
|
+
dw = DirectoryWatcher.new(source, :pre_load => true, :glob => '/**/*')
|
119
|
+
dw.interval = 1
|
120
|
+
dw.add_observer { |*args| args.each { |event| process_file_change(event) } }
|
121
|
+
dw.start
|
122
|
+
|
123
|
+
trap('INT') { dw.stop and exit 0 }
|
124
|
+
|
125
|
+
loop { sleep 100 }
|
126
|
+
else
|
127
|
+
puts "Site build complete."
|
128
|
+
end
|
129
|
+
else
|
130
|
+
puts "** There seems to be no site content in this folder. Did you mean to create a new site?\n\n plate new .\n\n"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def run_new_command
|
135
|
+
# Set the base root
|
136
|
+
root = './'
|
137
|
+
|
138
|
+
if args.size > 0
|
139
|
+
root = args[0]
|
140
|
+
end
|
141
|
+
|
142
|
+
puts "Generating new plate site at #{root}..."
|
143
|
+
|
144
|
+
# The starting path for the new site
|
145
|
+
root_path = File.expand_path(root)
|
146
|
+
|
147
|
+
# Create all folders needed for a base site.
|
148
|
+
%w(
|
149
|
+
/
|
150
|
+
config
|
151
|
+
content
|
152
|
+
layouts
|
153
|
+
posts
|
154
|
+
).each do |dir|
|
155
|
+
action = File.directory?(File.join(root_path, dir)) ? "exists" : "create"
|
156
|
+
puts " #{action} #{File.join(root, dir)}"
|
157
|
+
|
158
|
+
if action == "create"
|
159
|
+
FileUtils.mkdir_p(File.join(root_path, dir))
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Create a blank layout file
|
164
|
+
create_template('layouts/default.erb', 'layout.erb', root, root_path)
|
165
|
+
|
166
|
+
# Config file
|
167
|
+
create_template('config/plate.yml', 'config.yml', root, root_path)
|
168
|
+
|
169
|
+
# Index page
|
170
|
+
create_template('content/index.md', 'index.md', root, root_path)
|
171
|
+
|
172
|
+
puts "New site generated!"
|
173
|
+
end
|
174
|
+
|
175
|
+
def run_post_command
|
176
|
+
title = args.size == 0 ? "" : args[0]
|
177
|
+
slug = title.parameterize
|
178
|
+
date = Time.now
|
179
|
+
filename = File.join(self.source, 'posts', date.strftime('%Y/%m'), "#{date.strftime('%Y-%m-%d')}-#{slug}.md")
|
180
|
+
content = %Q(---\ntitle: "#{title}"\ndate: #{date.strftime('%Y-%m-%d %H:%M:%S')}\ntags: []\n\n# #{title}\n\n)
|
181
|
+
|
182
|
+
FileUtils.mkdir_p(File.dirname(filename))
|
183
|
+
File.open(filename, 'w') { |f| f.write(content) }
|
184
|
+
|
185
|
+
puts "New post file added [#{filename}]"
|
186
|
+
end
|
187
|
+
|
188
|
+
def create_template(path, template, root, root_path)
|
189
|
+
action = File.exist?(File.join(root_path, path)) ? "exists" : "create"
|
190
|
+
puts " #{action} #{File.join(root, path)}"
|
191
|
+
|
192
|
+
if action == "create"
|
193
|
+
template_root = File.expand_path(File.join(File.dirname(__FILE__), '..', 'templates'))
|
194
|
+
|
195
|
+
contents = File.read(File.join(template_root, template))
|
196
|
+
|
197
|
+
unless contents.to_s.blank?
|
198
|
+
File.open(File.join(root_path, path), 'w') { |f| f.write(contents) }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Plate
|
2
|
+
class DynamicPage < Page
|
3
|
+
attr_accessor :file_path, :locals
|
4
|
+
|
5
|
+
def initialize(site, destination_path, meta = {})
|
6
|
+
self.site = site
|
7
|
+
self.file_path = destination_path
|
8
|
+
self.meta = meta
|
9
|
+
self.content = ""
|
10
|
+
self.locals = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def locals=(hash)
|
14
|
+
@locals = hash.symbolize_keys!
|
15
|
+
end
|
16
|
+
|
17
|
+
def rendered_body
|
18
|
+
self.content
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check to see if a method called is in your locals.
|
22
|
+
def method_missing(sym, *args)
|
23
|
+
return locals[sym] if locals.has_key?(sym)
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/plate/engine.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Plate
|
2
|
+
module Engine
|
3
|
+
# All engines that have been registered for markup templating
|
4
|
+
def asset_engines
|
5
|
+
@engines[:assets].dup
|
6
|
+
end
|
7
|
+
|
8
|
+
# Register a new asset engine.
|
9
|
+
def register_asset_engine(extension, klass)
|
10
|
+
@engines[:assets] ||= {}
|
11
|
+
@engines[:assets][extension] = klass
|
12
|
+
end
|
13
|
+
|
14
|
+
# Register a new templating engine.
|
15
|
+
def register_template_engine(extension, klass)
|
16
|
+
@engines[:templates] ||= {}
|
17
|
+
@engines[:templates][extension] = klass
|
18
|
+
end
|
19
|
+
|
20
|
+
# All engines that have been registered for markup templating
|
21
|
+
def template_engines
|
22
|
+
@engines[:templates].dup
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/plate/errors.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Plate
|
2
|
+
# Mostly lifted from the default SassTemplate class at
|
3
|
+
# https://github.com/rtomayko/tilt/blob/master/lib/tilt/haml.rb
|
4
|
+
#
|
5
|
+
# Modifications have been made to default to html5 style
|
6
|
+
class HamlTemplate < Tilt::HamlTemplate
|
7
|
+
def prepare
|
8
|
+
options = @options.merge(:filename => eval_file, :line => line)
|
9
|
+
@engine = ::Haml::Engine.new(data, haml_options)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def haml_options
|
14
|
+
# Default style to html5 and double quoted attributes
|
15
|
+
options.merge(:format => :html5, :attr_wrapper => '"')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Plate
|
4
|
+
# Various view helpers related to blogging. Tags, categories, archives and the like.
|
5
|
+
module BloggingHelper
|
6
|
+
# Returns a customized hash of post archives for the given category
|
7
|
+
def category_archives(category)
|
8
|
+
result = {}
|
9
|
+
|
10
|
+
posts.archives.keys.each do |year|
|
11
|
+
posts.archives[year].keys.each do |month|
|
12
|
+
posts.archives[year][month].keys.each do |day|
|
13
|
+
posts.archives[year][month][day].each do |post|
|
14
|
+
if post.category == category
|
15
|
+
result[year] ||= {}
|
16
|
+
result[year][month] ||= {}
|
17
|
+
result[year][month][day] ||= []
|
18
|
+
result[year][month][day] << post
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
# Escape markup for use in XML or HTML
|
29
|
+
def html_escape(text)
|
30
|
+
::CGI.escapeHTML(text)
|
31
|
+
end
|
32
|
+
alias_method :xml_escape, :html_escape
|
33
|
+
|
34
|
+
# Grab the next blog post.
|
35
|
+
#
|
36
|
+
# Returns nil if this is the last post.
|
37
|
+
def next_post
|
38
|
+
return nil if post_index < 0 or post_index >= post_count
|
39
|
+
@next_post ||= self.posts[post_index + 1]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Grab the post previous to this one.
|
43
|
+
#
|
44
|
+
# Returns nil if this is the first post.
|
45
|
+
def previous_post
|
46
|
+
return nil if post_index < 1
|
47
|
+
@previous_post ||= self.posts[post_index - 1]
|
48
|
+
end
|
49
|
+
|
50
|
+
# The total number blog posts
|
51
|
+
def post_count
|
52
|
+
@post_count ||= self.posts.length
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns a number of where this post is in all posts
|
56
|
+
def post_index
|
57
|
+
@post_index ||= self.posts.index(self.post)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Find all posts for the given year, month and optional category.
|
61
|
+
def posts_for_month(year, month, category = nil)
|
62
|
+
result = []
|
63
|
+
|
64
|
+
if months = self.posts.archives[year.to_s]
|
65
|
+
if month = months[month.to_s]
|
66
|
+
month.each_value do |day|
|
67
|
+
result << day.select { |post| category == nil or post.category == category }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
result.flatten
|
73
|
+
end
|
74
|
+
|
75
|
+
# Find all posts for the given year and optional category.
|
76
|
+
def posts_for_year(year, category = nil)
|
77
|
+
result = []
|
78
|
+
|
79
|
+
if months = self.posts.archives[year.to_s]
|
80
|
+
months.each_value do |month|
|
81
|
+
month.each_value do |day|
|
82
|
+
result << day.select { |post| category == nil or post.category == category }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
result.flatten
|
88
|
+
end
|
89
|
+
|
90
|
+
def years_for_category(category)
|
91
|
+
result = []
|
92
|
+
|
93
|
+
self.posts.archives.keys.each do |year|
|
94
|
+
if posts_for_year(year, category).size > 0
|
95
|
+
result << year
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
result.sort
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Plate
|
2
|
+
# Methods related to the meta-data of a page
|
3
|
+
module MetaHelper
|
4
|
+
# Is there a description available for this page?
|
5
|
+
def description?
|
6
|
+
!page.description.to_s.blank?
|
7
|
+
end
|
8
|
+
|
9
|
+
# Sanitize keywords output into a comma separated list string
|
10
|
+
def keywords
|
11
|
+
page.keywords.join(", ")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Are there keywords available on this page?
|
15
|
+
def keywords?
|
16
|
+
page.keywords.length > 0
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a description meta tag if there is a description available for
|
20
|
+
# this page.
|
21
|
+
def meta_description_tag
|
22
|
+
%Q(<meta name="description" content="#{description}">) if description?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a keywords meta tag if there are keywords for this page.
|
26
|
+
def meta_keywords_tag
|
27
|
+
%Q(<meta name="keywords" content="#{keywords}">) if keywords?
|
28
|
+
end
|
29
|
+
|
30
|
+
# Output the standard meta tags (keywords and description) to a page.
|
31
|
+
# Omits any tags with blank data.
|
32
|
+
#
|
33
|
+
# Optionally pass in a list of which tags to display.
|
34
|
+
#
|
35
|
+
# @example Show all available meta tags
|
36
|
+
# <%= meta_tags %>
|
37
|
+
#
|
38
|
+
# @example Show only the keywords tag
|
39
|
+
# <%= meta_tags :keywords %>
|
40
|
+
#
|
41
|
+
# @example Show both keywords and description (default)
|
42
|
+
#
|
43
|
+
# <%= meta_tags :keywords, :description %>
|
44
|
+
#
|
45
|
+
# @overload meta_tags([tags...], options)
|
46
|
+
# @param [optional, Symbol] tags Pass in any number of symbols to output those tags.
|
47
|
+
# @param [optional, Hash] options Customize display options
|
48
|
+
# @option options [String] :joiner String to use to join together multiple tags.
|
49
|
+
def meta_tags(*args)
|
50
|
+
options = args.extract_options!
|
51
|
+
options = options.reverse_merge({
|
52
|
+
:joiner => "\n"
|
53
|
+
})
|
54
|
+
|
55
|
+
tags = args.length > 0 ? args : %w( description keywords )
|
56
|
+
|
57
|
+
result = []
|
58
|
+
|
59
|
+
tags.each do |tag|
|
60
|
+
result << self.send("meta_#{tag}_tag") if self.respond_to?("meta_#{tag}_tag")
|
61
|
+
end
|
62
|
+
|
63
|
+
result.join(options[:joiner]).strip
|
64
|
+
end
|
65
|
+
|
66
|
+
# Does this page have a title?
|
67
|
+
def title?
|
68
|
+
!page.title.to_s.blank?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|