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
data/lib/plate/post.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
module Plate
|
2
|
+
# A model for each blog post
|
3
|
+
class Post < Page
|
4
|
+
# Returns the category for this blog post. If no category is given in the meta
|
5
|
+
# information, then the value for config[:default_category] is used.
|
6
|
+
#
|
7
|
+
# If no default category has been given, this will default to "Posts"
|
8
|
+
def category
|
9
|
+
default = self.meta[:category] || self.site.default_category
|
10
|
+
end
|
11
|
+
|
12
|
+
# Category for this post, formatted to be URL-friendly
|
13
|
+
def category_for_url
|
14
|
+
self.category.to_s.dasherize.parameterize
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the date for this post, either from the filename or meta hash.
|
18
|
+
# If both are provided, the meta information takes precedence.
|
19
|
+
def date
|
20
|
+
result = nil
|
21
|
+
|
22
|
+
if self.meta[:date]
|
23
|
+
result = self.meta[:date].to_s
|
24
|
+
elsif self.basename =~ /^(\d{4}-\d{2}-\d{2})-/
|
25
|
+
result = $1.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
return Time.parse(result)
|
30
|
+
rescue Exception => e
|
31
|
+
self.site.log(" ** Problem reading date for file #{relative_file} (#{e.message}). Post skipped.")
|
32
|
+
end
|
33
|
+
|
34
|
+
raise NoPostDateProvided
|
35
|
+
end
|
36
|
+
|
37
|
+
def day
|
38
|
+
date.strftime('%d')
|
39
|
+
end
|
40
|
+
|
41
|
+
# The full file path of where this file will be written to. (Relative to site root)
|
42
|
+
def file_path
|
43
|
+
"#{permalink}/index.html"
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect
|
47
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} name=#{name.to_s.inspect} date=#{date.to_s}>"
|
48
|
+
end
|
49
|
+
|
50
|
+
def month
|
51
|
+
date.strftime('%m')
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return the [relative] path for this post. Uses the +permalink_template+
|
55
|
+
# variable as the method for converting post data into a URL.
|
56
|
+
#
|
57
|
+
# The permalink_template can be set in the global config named 'permalink'.
|
58
|
+
#
|
59
|
+
# Available options are:
|
60
|
+
#
|
61
|
+
# * `date` - The date of this post, formatted as YYYY-MM-DD
|
62
|
+
# * `title` - The title of this post, formatted for URL
|
63
|
+
# * `slug` - The filename slug
|
64
|
+
# * `year` - The 4-digit year of this post
|
65
|
+
# * `month` - The 2-digit month for this post
|
66
|
+
# * `day` - The 2-digit day of month for this post
|
67
|
+
# * `category` - The category for this post
|
68
|
+
#
|
69
|
+
# All values are formatted to be URL-safe. (No spaces, underscores or weird characters.)
|
70
|
+
def permalink(cache_buster = false)
|
71
|
+
return @permalink if @permalink and !cache_buster
|
72
|
+
|
73
|
+
date = self.date
|
74
|
+
|
75
|
+
# All of these variables can be put into a URL
|
76
|
+
permalink_attributes = {
|
77
|
+
"date" => date.strftime('%Y-%m-%d'),
|
78
|
+
"slug" => slug,
|
79
|
+
"title" => title_for_url,
|
80
|
+
"year" => year,
|
81
|
+
"month" => month,
|
82
|
+
"day" => day,
|
83
|
+
"category" => category_for_url
|
84
|
+
}
|
85
|
+
|
86
|
+
# Copy the permalink template as a starting point
|
87
|
+
result = permalink_template.clone
|
88
|
+
|
89
|
+
# Replace all variables from the attributes into the template
|
90
|
+
permalink_attributes.each { |key, value| result.gsub!(/:#{Regexp.escape(key)}/, value) }
|
91
|
+
|
92
|
+
# Remove any double slashes
|
93
|
+
result.gsub!(/\/\//, '/')
|
94
|
+
|
95
|
+
# Remove file extensions, and cleanup URL
|
96
|
+
result = result.split('/').reject{ |segment| segment =~ /^\.+$/ }.join('/')
|
97
|
+
|
98
|
+
@permalink = result
|
99
|
+
end
|
100
|
+
|
101
|
+
# The template to use when generating the permalink.
|
102
|
+
def permalink_template
|
103
|
+
self.site.options[:permalink] || '/:category/:year/:month/:slug'
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns the URL slug to use for this blog post.
|
107
|
+
#
|
108
|
+
# This will convert the file name from a format like:
|
109
|
+
#
|
110
|
+
# 2012-01-01-post-name.md
|
111
|
+
#
|
112
|
+
# To simply:
|
113
|
+
#
|
114
|
+
# post-name
|
115
|
+
def slug
|
116
|
+
name = self.basename.to_s.downcase.gsub(/^(\d{4}-\d{2}-\d{2})-/, '').split('.')[0]
|
117
|
+
name.dasherize.parameterize
|
118
|
+
end
|
119
|
+
|
120
|
+
# Utility method to sanitize tags output. Tags are returned as an array.
|
121
|
+
def tags
|
122
|
+
@tags ||= (Array === self.meta[:tags] ? self.meta[:tags] : self.meta[:tags].to_s.strip.split(',')).collect(&:strip).sort
|
123
|
+
end
|
124
|
+
|
125
|
+
def year
|
126
|
+
date.strftime('%Y')
|
127
|
+
end
|
128
|
+
|
129
|
+
# Compare two posts, by date.
|
130
|
+
def <=>(other)
|
131
|
+
self.date <=> other.date
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Plate
|
2
|
+
# Post collection is an enumerable wrapper for the posts in a site.
|
3
|
+
class PostCollection
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_accessor :categories, :tags, :archives
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@posts = []
|
10
|
+
@categories = {}
|
11
|
+
@tags = {}
|
12
|
+
@archives = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Add a post to the collection
|
16
|
+
def <<(post)
|
17
|
+
add(post)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add a post to the collection, then add its meta data to the summary.
|
21
|
+
def add(post)
|
22
|
+
return nil unless Post === post
|
23
|
+
@posts << post
|
24
|
+
process_post_meta(post)
|
25
|
+
end
|
26
|
+
|
27
|
+
# A hash of all categories in this collection with the number of posts using each.
|
28
|
+
def category_counts
|
29
|
+
return @category_counts if @category_counts
|
30
|
+
|
31
|
+
result = {}
|
32
|
+
|
33
|
+
categories.keys.each do |key|
|
34
|
+
result[key] = categories[key].size
|
35
|
+
end
|
36
|
+
|
37
|
+
@category_counts = result
|
38
|
+
end
|
39
|
+
|
40
|
+
# A sorted array of all categories in this collection.
|
41
|
+
def category_list
|
42
|
+
@category_list ||= categories.keys.sort
|
43
|
+
end
|
44
|
+
|
45
|
+
# Loop through each Post
|
46
|
+
def each
|
47
|
+
@posts.each { |post| yield post }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the last post in the collection.
|
51
|
+
#
|
52
|
+
# Or, pass in a number to return in descending order that number of posts
|
53
|
+
def last(*args)
|
54
|
+
result = @posts.last(*args)
|
55
|
+
|
56
|
+
if Array === result
|
57
|
+
result.reverse!
|
58
|
+
end
|
59
|
+
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
# Any methods called on the collection can be passed through to the Array
|
64
|
+
def method_missing(method, *args, &block)
|
65
|
+
@posts.send(method, *args, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Size of the posts collection
|
69
|
+
def size
|
70
|
+
@posts.size
|
71
|
+
end
|
72
|
+
|
73
|
+
# A hash of all tags in this collection with the number of posts using that tag.
|
74
|
+
def tag_counts
|
75
|
+
return @tag_counts if @tag_counts
|
76
|
+
|
77
|
+
result = {}
|
78
|
+
|
79
|
+
tags.keys.each do |key|
|
80
|
+
result[key] = tags[key].size
|
81
|
+
end
|
82
|
+
|
83
|
+
@tag_counts = result
|
84
|
+
end
|
85
|
+
|
86
|
+
# A sorted array of all tag names in this collection.
|
87
|
+
def tag_list
|
88
|
+
@tag_list ||= tags.keys.sort
|
89
|
+
end
|
90
|
+
|
91
|
+
def years
|
92
|
+
@years ||= archives.keys.sort
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
def process_post_meta(post)
|
97
|
+
# load up tags
|
98
|
+
post.tags.each do |tag|
|
99
|
+
@tags[tag] ||= []
|
100
|
+
@tags[tag] << post
|
101
|
+
end
|
102
|
+
|
103
|
+
# load up category
|
104
|
+
@categories[post.category] ||= []
|
105
|
+
@categories[post.category] << post
|
106
|
+
|
107
|
+
# load up yearly, monthly, and daily archives
|
108
|
+
@archives[post.year] ||= {}
|
109
|
+
@archives[post.year][post.month] ||= {}
|
110
|
+
@archives[post.year][post.month][post.day] ||= []
|
111
|
+
@archives[post.year][post.month][post.day] << post
|
112
|
+
|
113
|
+
post
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Plate
|
2
|
+
# Mostly lifted from the default SassTemplate class at
|
3
|
+
# https://github.com/rtomayko/tilt/blob/master/lib/tilt/css.rb
|
4
|
+
#
|
5
|
+
# Modifications have been made to use the site caching folder
|
6
|
+
class SassTemplate < Tilt::Template
|
7
|
+
self.default_mime_type = 'text/css'
|
8
|
+
|
9
|
+
def self.engine_initialized?
|
10
|
+
defined? ::Sass::Engine
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize_engine
|
14
|
+
require_template_library 'sass'
|
15
|
+
end
|
16
|
+
|
17
|
+
def prepare
|
18
|
+
end
|
19
|
+
|
20
|
+
def syntax
|
21
|
+
:sass
|
22
|
+
end
|
23
|
+
|
24
|
+
def evaluate(scope, locals, &block)
|
25
|
+
options = {
|
26
|
+
:filename => eval_file,
|
27
|
+
:line => line,
|
28
|
+
:syntax => syntax
|
29
|
+
}
|
30
|
+
|
31
|
+
locals ||= {}
|
32
|
+
|
33
|
+
if locals[:site] and locals[:site].cache_location
|
34
|
+
options[:cache_location] = File.join(locals[:site].cache_location, 'sass-cache')
|
35
|
+
end
|
36
|
+
|
37
|
+
::Sass::Engine.new(data, options).render
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/plate/site.rb
ADDED
@@ -0,0 +1,249 @@
|
|
1
|
+
module Plate
|
2
|
+
require 'tilt'
|
3
|
+
|
4
|
+
# This class contains everything you'll want to know about a site. It contains all data
|
5
|
+
# about the site, including blog posts, content pages, static files, assets and anything else.
|
6
|
+
class Site
|
7
|
+
include Callbacks
|
8
|
+
|
9
|
+
attr_accessor :assets, :build_destination, :cache_location, :destination,
|
10
|
+
:layouts, :logger, :options, :pages, :posts, :source
|
11
|
+
|
12
|
+
def initialize(source, destination, options = {})
|
13
|
+
# Setup source and destination for the site files
|
14
|
+
self.source = source
|
15
|
+
self.destination = destination
|
16
|
+
|
17
|
+
# By default, the build goes into the destination folder.
|
18
|
+
# Override this to output to a different folder by default
|
19
|
+
self.build_destination = destination
|
20
|
+
|
21
|
+
# Sanitize options
|
22
|
+
self.options = Hash === options ? options.clone : {}
|
23
|
+
self.options.symbolize_keys!
|
24
|
+
|
25
|
+
clear
|
26
|
+
end
|
27
|
+
|
28
|
+
%w( assets layouts pages posts ).each do |group|
|
29
|
+
class_eval "def #{group}; load!; @#{group}; end"
|
30
|
+
end
|
31
|
+
|
32
|
+
def all_files
|
33
|
+
@all_files ||= self.assets + self.layouts + self.pages + self.posts
|
34
|
+
end
|
35
|
+
|
36
|
+
# All extensions that are registered, as strings.
|
37
|
+
def asset_engine_extensions
|
38
|
+
@asset_engine_extensions ||= self.registered_asset_engines.keys.collect { |e| ".#{e}" }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Alphabetical list of all blog post categories used.
|
42
|
+
def categories
|
43
|
+
@categories ||= self.posts.collect(&:category).uniq.sort
|
44
|
+
end
|
45
|
+
|
46
|
+
# Clear out all data related to this site. Prepare for a reload, or first time load.
|
47
|
+
def clear
|
48
|
+
@loaded = false
|
49
|
+
@tags_counts = nil
|
50
|
+
@default_layout = nil
|
51
|
+
|
52
|
+
self.assets = []
|
53
|
+
self.layouts = []
|
54
|
+
self.pages = []
|
55
|
+
self.posts = PostCollection.new
|
56
|
+
end
|
57
|
+
|
58
|
+
# The default blog post category
|
59
|
+
def default_category
|
60
|
+
options[:default_category] || 'Posts'
|
61
|
+
end
|
62
|
+
|
63
|
+
# The default layout for all pages that do not specifically name their own
|
64
|
+
def default_layout
|
65
|
+
return nil if self.layouts.size == 0
|
66
|
+
return @default_layout if @default_layout
|
67
|
+
|
68
|
+
layout ||= self.layouts.reject { |l| !l.default? }
|
69
|
+
layout = self.layouts if layout.size == 0
|
70
|
+
|
71
|
+
if Array === layout and layout.size > 0
|
72
|
+
layout = layout[0]
|
73
|
+
end
|
74
|
+
|
75
|
+
@default_layout = layout
|
76
|
+
end
|
77
|
+
|
78
|
+
# Find a page, asset or layout by source relative file path
|
79
|
+
def find(search_path)
|
80
|
+
self.all_files.find { |file| file == search_path }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Find all pages and posts with this layout
|
84
|
+
def find_by_layout(layout_name)
|
85
|
+
result = []
|
86
|
+
|
87
|
+
result += self.pages.find_all { |page| page.layout == layout_name }
|
88
|
+
result += self.posts.find_all { |post| post.layout == layout_name }
|
89
|
+
|
90
|
+
result
|
91
|
+
end
|
92
|
+
|
93
|
+
# Find a specific layout by its file name. Any extensions are removed.
|
94
|
+
def find_layout(layout_name)
|
95
|
+
search_name = layout_name.to_s.downcase.strip.split('.')[0]
|
96
|
+
matches = self.layouts.reject { |l| l.name != search_name }
|
97
|
+
matches.empty? ? self.default_layout : matches[0]
|
98
|
+
end
|
99
|
+
|
100
|
+
def inspect
|
101
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} source=#{source.to_s.inspect}>"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Load all data from the various source directories.
|
105
|
+
def load!
|
106
|
+
return if @loaded
|
107
|
+
|
108
|
+
log("Loading site from source [#{source}]")
|
109
|
+
|
110
|
+
run_callback :before_load
|
111
|
+
|
112
|
+
self.load_pages!
|
113
|
+
self.load_layouts!
|
114
|
+
self.load_posts!
|
115
|
+
|
116
|
+
run_callback :after_load
|
117
|
+
|
118
|
+
@loaded = true
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns true if the site has been loaded from the source directories.
|
122
|
+
def loaded?
|
123
|
+
!!@loaded
|
124
|
+
end
|
125
|
+
|
126
|
+
# Write to the log if enable_logging is enabled
|
127
|
+
def log(message, style = :indent)
|
128
|
+
logger.send(:log, message, style) if logger and logger.respond_to?(:log)
|
129
|
+
end
|
130
|
+
|
131
|
+
def page_engine_extensions
|
132
|
+
@page_engine_extensions ||= self.registered_page_engines.keys.collect { |e| ".#{e}" }
|
133
|
+
end
|
134
|
+
|
135
|
+
def relative_path(file_or_directory)
|
136
|
+
file_or_directory.to_s.gsub(/^#{Regexp.quote(source)}(.*)$/, '\1')
|
137
|
+
end
|
138
|
+
|
139
|
+
def reload!
|
140
|
+
clear
|
141
|
+
load!
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns the asset engines that are available for use.
|
145
|
+
def registered_asset_engines
|
146
|
+
Plate.asset_engines
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns the engines available for use in page and layout formatting.
|
150
|
+
def registered_page_engines
|
151
|
+
Plate.template_engines
|
152
|
+
end
|
153
|
+
|
154
|
+
# All tags used on this site
|
155
|
+
def tags
|
156
|
+
@tags ||= self.posts.tag_list
|
157
|
+
end
|
158
|
+
|
159
|
+
def to_url(str)
|
160
|
+
result = str.to_s.strip.downcase
|
161
|
+
result = result.gsub(/[^-a-z0-9~\s\.:;+=_]/, '')
|
162
|
+
result = result.gsub(/[\.:;=+-]+/, '')
|
163
|
+
result = result.gsub(/[\s]/, '-')
|
164
|
+
result
|
165
|
+
end
|
166
|
+
alias_method :sanitize_slug, :to_url
|
167
|
+
|
168
|
+
# The base URL for this site. The url can be set using the config option named `:base_url`.
|
169
|
+
#
|
170
|
+
# The base URL will not have any trailing slashes.
|
171
|
+
def url
|
172
|
+
return '' unless self.options[:base_url]
|
173
|
+
@url ||= self.options[:base_url].to_s.gsub(/(.*?)\/?$/, '\1')
|
174
|
+
end
|
175
|
+
|
176
|
+
protected
|
177
|
+
# Load all layouts from layouts/
|
178
|
+
def load_layouts!(log = true)
|
179
|
+
@layouts = []
|
180
|
+
|
181
|
+
Dir.glob(File.join(source, "layouts/**/*")).each do |file|
|
182
|
+
# If this 'file' is a directory, just skip it. We only care about files.
|
183
|
+
unless File.directory?(file)
|
184
|
+
@layouts << Layout.new(self, file)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
log("#{@layouts.size} layouts loaded") if log
|
189
|
+
|
190
|
+
@layouts
|
191
|
+
end
|
192
|
+
|
193
|
+
def load_pages!(log = true)
|
194
|
+
@assets = []
|
195
|
+
@pages = []
|
196
|
+
|
197
|
+
# Load all pages, static pages and assets from content/
|
198
|
+
Dir.glob(File.join(source, "content/**/*")).each do |file|
|
199
|
+
# If this 'file' is a directory, just skip it. We only care about files.
|
200
|
+
unless File.directory?(file)
|
201
|
+
# Check for assets that need to be compiled. Currently only looks to see if the file
|
202
|
+
# ends in .coffee, .scss or .sass.
|
203
|
+
if asset_engine_extensions.include?(File.extname(file))
|
204
|
+
@assets << Asset.new(self, file)
|
205
|
+
else
|
206
|
+
# Check for YAML meta header. If it starts with ---, then process it as a page
|
207
|
+
intro = File.open(file) { |f| f.read(3) }
|
208
|
+
|
209
|
+
# If file contents start with ---, then it is something we should process as a page.
|
210
|
+
if intro == "---"
|
211
|
+
@pages << Page.new(self, file)
|
212
|
+
else
|
213
|
+
@pages << StaticPage.new(self, file)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
log("#{@assets.size} assets loaded") if log
|
220
|
+
log("#{@pages.size} pages and other files loaded") if log
|
221
|
+
|
222
|
+
@pages
|
223
|
+
end
|
224
|
+
|
225
|
+
# Load blog posts from posts/
|
226
|
+
def load_posts!(log = true)
|
227
|
+
@posts = PostCollection.new
|
228
|
+
|
229
|
+
Dir.glob(File.join(source, "posts/**/*")).each do |file|
|
230
|
+
# If this 'file' is a directory, just skip it. We only care about files.
|
231
|
+
unless File.directory?(file)
|
232
|
+
# Check for YAML meta header. If it starts with ---, then process it as a page
|
233
|
+
intro = File.open(file) { |f| f.read(3) }
|
234
|
+
|
235
|
+
# If file contents start with ---, then it is something we should process as a page.
|
236
|
+
if intro == "---"
|
237
|
+
@posts.add(Post.new(self, file))
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
@posts.sort
|
243
|
+
|
244
|
+
log("#{@posts.size} posts loaded") if log
|
245
|
+
|
246
|
+
@posts
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|