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.
@@ -0,0 +1,11 @@
1
+ module Plate
2
+ # Includes basic helpers for managing URLs within your site.
3
+ module URLHelper
4
+ # Cleans up a string to make it URl-friendly, removing all special
5
+ # characters, spaces, and sanitizing to a dashed, lowercase string.
6
+ def sanitize_slug(str)
7
+ self.site.sanitize_slug(str)
8
+ end
9
+ alias_method :s, :sanitize_slug
10
+ end
11
+ end
@@ -0,0 +1,169 @@
1
+ module Plate
2
+ class Layout
3
+ attr_accessor :site, :file, :meta, :content
4
+
5
+ def initialize(site, file = nil, load_on_initialize = true)
6
+ self.site = site
7
+ self.file = file
8
+ self.meta = {}
9
+ self.content = ""
10
+
11
+ load! if load_on_initialize and file?
12
+ end
13
+
14
+ # The name of the layound, without any path data
15
+ def basename
16
+ File.basename(self.file)
17
+ end
18
+
19
+ # Is this layout the default layout, by name.
20
+ def default?
21
+ self.name.downcase.strip.start_with? "default"
22
+ end
23
+
24
+ # The layout engine to use. Based off of the last file extension for this layout.
25
+ def engine
26
+ @engine ||= self.site.registered_page_engines[self.extension.gsub(/\./, '').to_sym]
27
+ end
28
+
29
+ # The last file extension of this layout.
30
+ def extension
31
+ File.extname(self.file)
32
+ end
33
+
34
+ # Does the file exist or not.
35
+ def file?
36
+ return false if self.file.nil?
37
+ File.exists?(self.file)
38
+ end
39
+
40
+ # A unique ID for this layout.
41
+ def id
42
+ @id ||= Digest::MD5.hexdigest(relative_file)
43
+ end
44
+
45
+ def inspect
46
+ "#<#{self.class}:0x#{object_id.to_s(16)} name=#{name.to_s.inspect}>"
47
+ end
48
+
49
+ def load!
50
+ return if @loaded
51
+ raise FileNotFound unless file?
52
+
53
+ read_file!
54
+ read_metadata!
55
+
56
+ @loaded = true
57
+ end
58
+
59
+ # The name for a layout is just the lowercase, first part of the file name.
60
+ def name
61
+ return "" unless file?
62
+ @name ||= self.basename.to_s.downcase.strip.split('.')[0]
63
+ end
64
+
65
+ # A parent layout for this current layout file. If no layout is specified for this
66
+ # layout's parent, then nil is returned. If there is a parent layout for this layout,
67
+ # any pages using it will be rendered using this layout first, then sent to the parent
68
+ # for further rendering.
69
+ def parent
70
+ return @parent if @parent
71
+
72
+ if self.meta[:layout]
73
+ @parent = self.site.find_layout(self.meta[:layout])
74
+ else
75
+ @parent = nil
76
+ end
77
+
78
+ @parent
79
+ end
80
+
81
+ def relative_file
82
+ @relative_file ||= self.site.relative_path(self.file)
83
+ end
84
+
85
+ def reload!
86
+ @template = nil
87
+ @loaded = false
88
+ @name = nil
89
+ @engine = nil
90
+ end
91
+
92
+ # Render the given content against the current layout template.
93
+ def render(content, page = nil, view = nil)
94
+ if self.template
95
+ view ||= View.new(self.site, page)
96
+ result = self.template.render(view) { content }
97
+
98
+ if self.parent
99
+ result = self.parent.render(result, page, view)
100
+ end
101
+
102
+ view = nil
103
+
104
+ result
105
+ else
106
+ content.respond_to?(:rendered_content) ? content.rendered_content : content.to_s
107
+ end
108
+ end
109
+
110
+ # The render template to use for this layout. A template is only used if the
111
+ # file extension for the layout is a valid layout extension from the current
112
+ # site.
113
+ def template
114
+ return @template if @template
115
+
116
+ if template?
117
+ @template = self.engine.new() { self.content }
118
+ else
119
+ nil
120
+ end
121
+ end
122
+
123
+ # Does this file have the ability to be used as a template?
124
+ #
125
+ # This currently only works if the layout is a .erb file. Otherwise anything that
126
+ # calls this layout just returns the text it is given.
127
+ def template?
128
+ self.site.page_engine_extensions.include?(self.extension)
129
+ end
130
+
131
+ # Is this layout equal to another page being sent?
132
+ def ==(other)
133
+ other = other.relative_file if other.respond_to?(:relative_file)
134
+ self.id == other or self.relative_file == other
135
+ end
136
+
137
+ protected
138
+ # Read the file and store it in @content
139
+ def read_file!
140
+ self.content = file? ? File.read(self.file) : nil
141
+ end
142
+
143
+ # Reads all content from a layouts's meta data. At this time, the layout only supports
144
+ # loading a parent layout. All other meta data is unused.
145
+ #
146
+ # Meta data is stored in YAML format within the head of a page after the -- declaration like so:
147
+ #
148
+ # ---
149
+ # layout: default
150
+ #
151
+ # # Start of layout content
152
+ def read_metadata!
153
+ return unless self.content
154
+
155
+ begin
156
+ if matches = /^(---\n)(.*?)^\s*?$/m.match(self.content)
157
+ if matches.size == 3
158
+ self.content = matches.post_match.strip
159
+ self.meta = YAML.load(matches[2])
160
+ self.meta.symbolize_keys!
161
+ end
162
+ end
163
+ rescue Exception => e
164
+ self.meta = {}
165
+ self.site.log(" ** Problem reading YAML for file #{relative_file} (#{e.message}). Meta data skipped")
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,22 @@
1
+ module Plate
2
+ # Generic markdown template, currently uses RedCarpet2
3
+ class MarkdownTemplate < Tilt::Template
4
+ self.default_mime_type = 'text/html'
5
+
6
+ def self.engine_initialized?
7
+ defined? ::Redcarpet::Render
8
+ end
9
+
10
+ def initialize_engine
11
+ require_template_library 'redcarpet'
12
+ end
13
+
14
+ def prepare
15
+ @engine = ::Redcarpet::Markdown.new(Redcarpet::Render::HTML, options)
16
+ end
17
+
18
+ def evaluate(scope, locals, &block)
19
+ @output ||= @engine.render(data)
20
+ end
21
+ end
22
+ end
data/lib/plate/page.rb ADDED
@@ -0,0 +1,280 @@
1
+ require 'digest'
2
+
3
+ module Plate
4
+ class Page
5
+ include Callbacks
6
+
7
+ attr_accessor :body, :content, :file, :meta, :site
8
+
9
+ def initialize(site, file = nil, load_on_initialize = true)
10
+ self.site = site
11
+ self.file = file
12
+ self.meta = {}
13
+ self.content = ""
14
+
15
+ load! if load_on_initialize and file?
16
+ end
17
+
18
+ # Setup some shortcut getters for meta attributes
19
+ %w( title description tags category ).each do |meta_attribute|
20
+ class_eval <<-META
21
+ def #{meta_attribute} # def title
22
+ self.meta[:#{meta_attribute}] # self.meta[:title]
23
+ end # end
24
+
25
+ def #{meta_attribute}=(value) # def title=(value)
26
+ self.meta[:#{meta_attribute}] = value # self.meta[:title] = value
27
+ end # end
28
+ META
29
+ end
30
+
31
+ # The name of the file, without any path data
32
+ def basename
33
+ File.basename(self.file)
34
+ end
35
+ alias_method :name, :basename
36
+
37
+ # The directory this page is located in, relative to the site root.
38
+ def directory
39
+ return @directory if @directory
40
+
41
+ base = Pathname.new(File.join(self.site.source, 'content'))
42
+ current = Pathname.new(self.file)
43
+
44
+ dirs = current.relative_path_from(base).to_s.split('/')
45
+
46
+ if dirs.size > 1
47
+ dirs.pop
48
+ @directory = "/#{dirs.join('/')}"
49
+ else
50
+ @directory = "/"
51
+ end
52
+ end
53
+
54
+ def engines
55
+ @engines ||= self.extensions.reverse.collect { |e| self.site.registered_page_engines[e.gsub(/\./, '').to_sym] }.reject { |e| !e }
56
+ end
57
+
58
+ def extensions
59
+ @extensions ||= self.basename.scan(/\.[^.]+/)
60
+ end
61
+
62
+ # Does the file exist or not.
63
+ def file?
64
+ return false if self.file.nil?
65
+ File.exists?(self.file)
66
+ end
67
+
68
+ # Returns just the file name, no extension.
69
+ def file_name
70
+ File.basename(self.file, '.*')
71
+ end
72
+
73
+ # The full file path of where this file will be written to. (Relative to site root)
74
+ def file_path
75
+ return self.meta[:path] if self.meta.has_key?(:path)
76
+ return @file_path if @file_path
77
+
78
+ result = directory
79
+ result << '/' unless result =~ /\/$/
80
+ result << slug unless slug == "index"
81
+
82
+ # Remove any double slashes
83
+ result.gsub!(/\/\//, '/')
84
+
85
+ # Remove file extensions, and cleanup URL
86
+ result = result.split('/').reject{ |segment| segment =~ /^\.+$/ }.join('/')
87
+
88
+ # Add a trailing slash
89
+ result << '/' unless result =~ /\/$/
90
+
91
+ # Tack on index.html for the folder
92
+ result << "index.html"
93
+ @file_path = result
94
+ end
95
+
96
+ def format_extension
97
+ format = self.extensions.reverse.detect() { |e| !self.site.page_engine_extensions.include?(e) }
98
+ format = ".html" if format.nil?
99
+ format
100
+ end
101
+
102
+ # A unique ID for this page.
103
+ def id
104
+ @id ||= Digest::MD5.hexdigest(relative_file)
105
+ end
106
+
107
+ def inspect
108
+ "#<#{self.class}:0x#{object_id.to_s(16)} name=#{name.to_s.inspect}>"
109
+ end
110
+
111
+ # Utility method to sanitize keywords output. Keywords are returned as an array.
112
+ def keywords
113
+ @keywords ||= (Array === self.meta[:keywords] ? self.meta[:keywords] : self.meta[:keywords].to_s.strip.split(',').collect(&:strip))
114
+ end
115
+
116
+ # The layout to use when rendering this page. Returns nil if no default layout is available,
117
+ # or the layout has specifically been turned off within the config.
118
+ def layout
119
+ return nil if self.meta[:layout] == false
120
+ return @layout if @layout
121
+
122
+ if self.meta[:layout]
123
+ @layout = self.site.find_layout(self.meta[:layout])
124
+ else
125
+ @layout = self.site.default_layout
126
+ end
127
+
128
+ @layout
129
+ end
130
+
131
+ # Has this page been loaded from file?
132
+ def loaded?
133
+ !!@loaded
134
+ end
135
+
136
+ # Read the file data for this page
137
+ def load!
138
+ return if @loaded
139
+ raise FileNotFound unless file?
140
+
141
+ read_file!
142
+ read_metadata!
143
+
144
+ @loaded = true
145
+ end
146
+
147
+ def path
148
+ return '/' if self.file_path == '/index.html'
149
+ @path ||= self.file_path.sub(/(.*?)\/index\.html$/i, '\1')
150
+ end
151
+
152
+ # The file's source path, relative to site root.
153
+ def relative_file
154
+ @relative_file ||= self.site.relative_path(self.file)
155
+ end
156
+
157
+ def reload!
158
+ @layout = nil
159
+ @loaded = false
160
+ @content = nil
161
+ @meta = {}
162
+ @keywords = nil
163
+ @rendered_content = nil
164
+ @body = nil
165
+
166
+ load!
167
+ end
168
+
169
+ # Returns the rendered body of this page, without the layout.
170
+ def rendered_body
171
+ return @body if @body
172
+
173
+ result = ""
174
+
175
+ around_callback :render do
176
+ result = self.content
177
+
178
+ view = View.new(self.site, self)
179
+
180
+ self.engines.each do |engine|
181
+ template = engine.new(self.file) { result }
182
+ result = template.render(view, {})
183
+ end
184
+
185
+ view = nil
186
+
187
+ @body = result
188
+ end
189
+
190
+ @body
191
+ end
192
+
193
+ def rendered_content
194
+ @rendered_content ||= self.apply_layout_to(rendered_body)
195
+ end
196
+
197
+ # Name of the file to be saved. Just takes the current file name and removes any extensions.
198
+ def slug
199
+ self.basename.to_s.downcase.split('.')[0].dasherize.parameterize
200
+ end
201
+
202
+ # The title from this page's meta data, turned into a parameter for use in a url.
203
+ def title_for_url
204
+ self.title.to_s.dasherize.parameterize
205
+ end
206
+
207
+ # Returns this page's content
208
+ def to_s
209
+ self.inspect
210
+ end
211
+
212
+ # The full URL of this page. Depends on the site's URL attribute and a config option of `:base_url`
213
+ def url
214
+ @url ||= "#{site.url}#{path}"
215
+ end
216
+
217
+ # Write this compiled page to its destination file.
218
+ def write!
219
+ path = File.join(site.build_destination, file_path)
220
+
221
+ FileUtils.mkdir_p(File.dirname(path))
222
+
223
+ File.open(path, 'w') do |f|
224
+ f.write(self.rendered_content)
225
+ end
226
+
227
+ path
228
+ end
229
+
230
+ # Is this page equal to another page being sent?
231
+ def ==(other)
232
+ other = other.relative_file if other.respond_to?(:relative_file)
233
+ self.id == other or self.relative_file == other
234
+ end
235
+
236
+ protected
237
+ def apply_layout_to(content)
238
+ return content unless Layout === self.layout
239
+ self.layout.render(content, self)
240
+ end
241
+
242
+ # Reading page details
243
+ # #####################################################################
244
+
245
+ # Read the file and store it in @content
246
+ def read_file!
247
+ self.content = file? ? File.read(self.file) : nil
248
+ end
249
+
250
+ # Reads all content from a page's meta data
251
+ #
252
+ # Meta data is stored in YAML format within the head of a page after the -- declaration like so:
253
+ #
254
+ # ---
255
+ # title: "Hello"
256
+ # description: "This is some meta data"
257
+ # keywords: [ blah, blah ]
258
+ # tags: [ blah, blah ]
259
+ # category: Test
260
+ # layout: default
261
+ #
262
+ # # Start of actual content
263
+ def read_metadata!
264
+ return unless self.content
265
+
266
+ begin
267
+ if matches = /^(---\n)(.*?)^\s*?$/m.match(self.content)
268
+ if matches.size == 3
269
+ self.content = matches.post_match.strip
270
+ self.meta = YAML.load(matches[2])
271
+ self.meta.symbolize_keys!
272
+ end
273
+ end
274
+ rescue Exception => e
275
+ self.meta = {}
276
+ self.site.log(" ** Problem reading YAML for file #{relative_file} (#{e.message}). Meta data skipped")
277
+ end
278
+ end
279
+ end
280
+ end