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/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
@@ -0,0 +1,9 @@
1
+ module Plate
2
+ class ScssTemplate < SassTemplate
3
+ self.default_mime_type = 'text/css'
4
+
5
+ def syntax
6
+ :scss
7
+ end
8
+ end
9
+ 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