plate 0.5.4 → 0.6.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.
@@ -1,28 +1,44 @@
1
1
  module Plate
2
+ # Callbacks are used to extend the base functionality of Plate. You might want to create
3
+ # some dynamic pages after all posts have been loaded (such as a blog archives view) or
4
+ # clean up some page output after it is rendered but before it is written to disk. Callbacks
5
+ # are the best way to achieve these and other common use cases.
6
+ #
7
+ # Callbacks must be registered with an object to be used. If you generated a new site
8
+ # using the `plate new .` or `platify` command, you should see an example callbacks file
9
+ # in `lib/callbacks.rb`.
10
+ #
2
11
  module Callbacks
3
12
  def self.included(base)
4
13
  base.send(:include, InstanceMethods)
5
14
  base.send(:extend, ClassMethods)
6
15
  end
7
-
16
+
8
17
  module ClassMethods
18
+ # All of the callbacks that have been registered.
9
19
  def callbacks
10
20
  @callbacks ||= {}
11
21
  end
12
-
22
+
23
+ # Register a new callback
24
+ #
25
+ # @example
26
+ # Plate::Page.register_callback(:after_render) do |page|
27
+ # puts "Rendered page! #{page.path}"
28
+ # end
13
29
  def register_callback(name, method_name = nil, &block)
14
30
  callbacks[name] ||= []
15
31
  callbacks[name] << (block || method_name)
16
32
  end
17
33
  end
18
-
34
+
19
35
  module InstanceMethods
20
36
  def around_callback(name, &block)
21
37
  run_callback "before_#{name}".to_sym
22
38
  block.call
23
39
  run_callback "after_#{name}".to_sym
24
40
  end
25
-
41
+
26
42
  def run_callback(name)
27
43
  if callbacks = self.class.callbacks[name]
28
44
  callbacks.each do |callback|
@@ -2,45 +2,45 @@ module Plate
2
2
  # The CLI class controls the behavior of plate when it is used as a command line interface.
3
3
  class CLI
4
4
  require 'optparse'
5
-
5
+
6
6
  attr_accessor :source, :destination, :args, :options
7
-
7
+
8
8
  def initialize(args = [])
9
9
  @args = String === args ? args.split(' ') : args.dup
10
10
  @options = {}
11
-
11
+
12
12
  # Some defaults
13
13
  @source = Dir.pwd
14
14
  @destination = File.join(@source, 'public')
15
15
  end
16
-
16
+
17
17
  def builder
18
18
  @builder ||= Builder.new(self.source, self.destination, self.options)
19
19
  end
20
-
20
+
21
21
  # The current command to be run. Pulled from the args attribute.
22
22
  def command
23
23
  self.args.size > 0 ? self.args[0] : 'build'
24
24
  end
25
-
25
+
26
26
  def parse_options!
27
27
  options = {}
28
28
 
29
29
  opts = OptionParser.new do |opts|
30
30
  banner = "Usage: plate [command] [options]"
31
-
31
+
32
32
  opts.on('--category [CATEGORY]', '-c', 'Pass in a category for creating a new post.') do |c|
33
33
  options[:category] = c
34
34
  end
35
-
35
+
36
36
  opts.on('--config [PATH]', '-C', 'Set the config file location for the site.') do |c|
37
37
  options[:config] = c
38
38
  end
39
-
39
+
40
40
  opts.on('--destination [PATH]', '-d', 'Set the destination directory for this build.') do |d|
41
41
  @destination = File.expand_path(d)
42
42
  end
43
-
43
+
44
44
  opts.on('--layout [LAYOUT]', '-l', 'Pass in a layout for creating a new post.') do |l|
45
45
  options[:layout] = l
46
46
  end
@@ -57,23 +57,23 @@ module Plate
57
57
  puts "You're running Plate version #{Plate::VERSION}!"
58
58
  exit 0
59
59
  end
60
-
60
+
61
61
  opts.on('--watch', '-w', 'Watch the source directory for changes.') do
62
62
  options[:watch] = true
63
63
  end
64
64
  end
65
65
 
66
66
  opts.parse!(self.args)
67
-
67
+
68
68
  @options = options
69
69
  end
70
-
70
+
71
71
  # Run the given command. If the command does not exist, nothing will be run.
72
72
  def run
73
73
  parse_options!
74
-
74
+
75
75
  command_name = "run_#{command}_command".to_sym
76
-
76
+
77
77
  if self.respond_to?(command_name)
78
78
  # remove command name
79
79
  self.args.shift
@@ -82,20 +82,20 @@ module Plate
82
82
  puts "Command #{command} not found"
83
83
  return false
84
84
  end
85
-
85
+
86
86
  true
87
87
  end
88
-
88
+
89
89
  class << self
90
90
  def run!
91
91
  new(ARGV).run
92
92
  end
93
93
  end
94
-
94
+
95
95
  protected
96
96
  def process_file_change(event)
97
- relative_path = builder.relative_path(event.path)
98
-
97
+ relative_path = builder.relative_path(event.path)
98
+
99
99
  unless relative_path.start_with?('/public/')
100
100
  if builder.reloadable?(relative_path)
101
101
  case event.type
@@ -115,13 +115,15 @@ module Plate
115
115
  end
116
116
  end
117
117
  end
118
-
118
+
119
119
  def run_build_command
120
- puts "Building your site from #{source} to #{destination}"
121
-
122
120
  builder.enable_logging = true if options[:verbose]
121
+ builder.load!
122
+
123
+ puts "Building your site from #{source} to #{builder.destination}"
124
+
123
125
  builder.render!
124
-
126
+
125
127
  if builder.items?
126
128
  # If we want to watch directories, keep the chain open
127
129
  if options[:watch]
@@ -139,86 +141,96 @@ module Plate
139
141
  puts "Site build complete."
140
142
  end
141
143
  else
142
- 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"
144
+ puts "** There seems to be no site content in this folder. Did you mean to create a new site first?\n\n platify .\n\n"
143
145
  end
144
146
  end
145
-
146
- def run_new_command
147
+
148
+ def run_new_command
147
149
  # Set the base root
148
150
  root = './'
149
-
151
+
150
152
  if args.size > 0
151
153
  root = args[0]
152
154
  end
153
-
155
+
154
156
  puts "Generating new plate site at #{root}..."
155
-
157
+
156
158
  # The starting path for the new site
157
159
  root_path = File.expand_path(root)
158
-
160
+
159
161
  # Create all folders needed for a base site.
160
162
  %w(
161
163
  /
162
164
  config
163
165
  content
166
+ content/css
164
167
  layouts
168
+ lib
165
169
  posts
166
170
  ).each do |dir|
167
- action = File.directory?(File.join(root_path, dir)) ? "exists" : "create"
171
+ action = File.directory?(File.join(root_path, dir)) ? "exists" : "create"
168
172
  puts " #{action} #{File.join(root, dir)}"
169
-
173
+
170
174
  if action == "create"
171
175
  FileUtils.mkdir_p(File.join(root_path, dir))
172
- end
176
+ end
173
177
  end
174
-
178
+
175
179
  # Create a blank layout file
176
- create_template('layouts/default.erb', 'layout.erb', root, root_path)
177
-
180
+ create_template('layouts/default.html.erb', 'layout.html.erb', root, root_path)
181
+
178
182
  # Config file
179
183
  create_template('config/plate.yml', 'config.yml', root, root_path)
180
-
184
+
181
185
  # Index page
182
- create_template('content/index.md', 'index.md', root, root_path)
183
-
186
+ create_template('content/index.html', 'index.html', root, root_path)
187
+
188
+ # RSS Feed
189
+ create_template('content/rss.erb', 'rss.erb', root, root_path)
190
+
191
+ # Sample callbacks file
192
+ create_template('lib/callbacks.rb', 'callbacks.rb', root, root_path)
193
+
194
+ # Default CSS file from Bootstrap
195
+ create_template('content/css/bootstrap.min.css', 'bootstrap.min.css', root, root_path)
196
+
184
197
  puts "New site generated!"
185
198
  end
186
-
199
+
187
200
  def run_post_command
188
201
  title = args.size == 0 ? "" : args[0]
189
202
  slug = title.parameterize
190
203
  date = Time.now
191
-
204
+
192
205
  # if there are any post defaults in the config file, use those as the default options
193
206
  if builder.config.has_key?(:post_defaults)
194
207
  options.reverse_merge!(builder.config[:post_defaults])
195
208
  end
196
-
209
+
197
210
  category = options[:category] ? "\ncategory: #{options[:category]}" : ""
198
211
  layout = options[:layout] ? "\nlayout: #{options[:layout]}" : ""
199
-
212
+
200
213
  filename = File.join(self.source, 'posts', date.strftime('%Y/%m'), "#{date.strftime('%Y-%m-%d')}-#{slug}.md")
201
214
  content = %Q(---\ntitle: "#{title}"\ndate: #{date.strftime('%Y-%m-%d %H:%M:%S')}#{category}#{layout}\ntags: []\n\n# #{title}\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.)
202
-
215
+
203
216
  FileUtils.mkdir_p(File.dirname(filename))
204
217
  File.open(filename, 'w') { |f| f.write(content) }
205
-
218
+
206
219
  puts "New post file added [#{filename}]"
207
220
  end
208
-
221
+
209
222
  def create_template(path, template, root, root_path)
210
- action = File.exist?(File.join(root_path, path)) ? "exists" : "create"
223
+ action = File.exist?(File.join(root_path, path)) ? "exists" : "create"
211
224
  puts " #{action} #{File.join(root, path)}"
212
-
225
+
213
226
  if action == "create"
227
+ dest = File.join(root_path, path)
214
228
  template_root = File.expand_path(File.join(File.dirname(__FILE__), '..', 'templates'))
215
-
216
229
  contents = File.read(File.join(template_root, template))
217
-
218
230
  unless contents.to_s.blank?
219
- File.open(File.join(root_path, path), 'w') { |f| f.write(contents) }
231
+ File.open(dest, 'w') { |f| f.write(contents) }
220
232
  end
221
- end
233
+ end
222
234
  end
223
235
  end
224
236
  end
@@ -0,0 +1,68 @@
1
+ module Plate
2
+ # The Dsl class provides the methods available in plugin files in order to extend
3
+ # the functionality a generated site.
4
+ class Dsl
5
+ class << self
6
+ # Evaluate the given file path into the {Plate::Dsl dsl} instance.
7
+ def evaluate_plugin(file_path)
8
+ instance_eval_plugin(read_plugin_file(file_path))
9
+ end
10
+
11
+ # Performs evaluation of the file's content
12
+ def instance_eval_plugin(content)
13
+ new.instance_eval(content)
14
+ end
15
+
16
+ # Read the given plugin file path into a string
17
+ #
18
+ # @return [String] The plugin file's contents
19
+ def read_plugin_file(file_path)
20
+ File.read(file_path)
21
+ rescue
22
+ raise PluginNotReadable
23
+ end
24
+ end
25
+
26
+ # Register a new callback for the given object and event.
27
+ #
28
+ # @example Run block after rendering a site
29
+ # callback :site, :after_render do
30
+ # puts 'done!'
31
+ # end
32
+ #
33
+ # @example Run block before rendering a page
34
+ # callback :page, :before_render do |page|
35
+ # puts "Rendering page #{page.path}"
36
+ # end
37
+ #
38
+ # @example Run a method on the site after write
39
+ # class Site
40
+ # def finished!
41
+ # log('All done.')
42
+ # end
43
+ # end
44
+ #
45
+ # callback :site, :after_write, :finished!
46
+ #
47
+ def callback(object, event, method_name = nil, &block)
48
+ if Symbol === object
49
+ object = "Plate::#{object.to_s.classify}".constantize
50
+ end
51
+
52
+ object.register_callback(event, method_name, &block)
53
+ end
54
+ alias :register_callback :callback
55
+
56
+ # Set up a new engine designed for rendering dynamic assets.
57
+ # Engines should be compatible with the Tilt::Template syntax.
58
+ def register_asset_engine(extension, klass)
59
+ Plate.register_asset_engine(extension, klass)
60
+ end
61
+
62
+ # Set up a new engine designed for rendering dynamic assets.
63
+ # Engines should be compatible with the Tilt::Template syntax.
64
+ def register_template_engine(extension, klass)
65
+ Plate.register_template_engine(extension, klass)
66
+ end
67
+ end
68
+ end
@@ -1,10 +1,13 @@
1
1
  module Plate
2
- class SourceNotFound < StandardError
3
- end
4
-
5
2
  class FileNotFound < StandardError
6
3
  end
7
-
4
+
8
5
  class NoPostDateProvided < StandardError
9
6
  end
7
+
8
+ class PluginNotReadable < StandardError
9
+ end
10
+
11
+ class SourceNotFound < StandardError
12
+ end
10
13
  end
@@ -0,0 +1,27 @@
1
+ module Plate
2
+ # Just a blank class that allows you to call methods that return hash values
3
+ class HashProxy
4
+ # Pass in a hash object to use this class as a proxy for its keys.
5
+ def initialize(source = {})
6
+ @source = source
7
+ end
8
+
9
+ # Pass through for getting the hash's value at the given key.
10
+ def [](key)
11
+ @source[key]
12
+ end
13
+
14
+ # Handle method missing calls and proxy methods to hash values.
15
+ #
16
+ # Allows for checking to see if a key exists and returning a key value.
17
+ def method_missing(method, *args)
18
+ if @source.has_key?(method)
19
+ self[method]
20
+ elsif method.to_s =~ /^[a-zA-Z0-9_]*\?$/
21
+ @source.has_key?(method.to_s.gsub!(/\?/, '').to_sym)
22
+ else
23
+ nil
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,6 +1,25 @@
1
1
  module Plate
2
2
  # Includes basic helpers for managing URLs within your site.
3
3
  module URLHelper
4
+ # replace all relative links within the given content block to absolute
5
+ # links containing the base url for this site.
6
+ #
7
+ # This is useful in creating RSS and/or Atom feeds that you'd like to
8
+ # contain links back to your site.
9
+ #
10
+ # href or src values that don't start with a slash are left intact.
11
+ #
12
+ # @example
13
+ # <a href="/posts/one"><img src="/images/sample.jpg"></a>
14
+ #
15
+ # <!-- turns into: -->
16
+ #
17
+ # <a href="http://example.com/posts/one"><img src="http://example.com/images/sample.jpg"></a>
18
+ def absolutize_paths(content)
19
+ content.gsub(Regexp.new("(src|href)=(\"|')/"), "\\1=\\2#{site.url}/")
20
+ end
21
+ alias_method :a, :absolutize_paths
22
+
4
23
  # Cleans up a string to make it URl-friendly, removing all special
5
24
  # characters, spaces, and sanitizing to a dashed, lowercase string.
6
25
  def sanitize_slug(str)
@@ -1,125 +1,125 @@
1
1
  module Plate
2
2
  class Layout
3
3
  attr_accessor :site, :file, :meta, :content
4
-
4
+
5
5
  def initialize(site, file = nil, load_on_initialize = true)
6
6
  self.site = site
7
7
  self.file = file
8
8
  self.meta = {}
9
9
  self.content = ""
10
-
10
+
11
11
  load! if load_on_initialize and file?
12
12
  end
13
-
13
+
14
14
  # The name of the layound, without any path data
15
15
  def basename
16
16
  File.basename(self.file)
17
17
  end
18
-
18
+
19
19
  # Is this layout the default layout, by name.
20
20
  def default?
21
21
  self.name.downcase.strip.start_with? "default"
22
22
  end
23
-
23
+
24
24
  # The layout engine to use. Based off of the last file extension for this layout.
25
25
  def engine
26
26
  @engine ||= self.site.registered_page_engines[self.extension.gsub(/\./, '').to_sym]
27
27
  end
28
-
28
+
29
29
  # The last file extension of this layout.
30
30
  def extension
31
31
  File.extname(self.file)
32
32
  end
33
-
34
- # Does the file exist or not.
33
+
34
+ # Does the file exist or not.
35
35
  def file?
36
36
  return false if self.file.nil?
37
37
  File.exists?(self.file)
38
38
  end
39
-
39
+
40
40
  # A unique ID for this layout.
41
- def id
41
+ def id
42
42
  @id ||= Digest::MD5.hexdigest(relative_file)
43
43
  end
44
-
44
+
45
45
  def inspect
46
46
  "#<#{self.class}:0x#{object_id.to_s(16)} name=#{name.to_s.inspect}>"
47
47
  end
48
-
48
+
49
49
  def load!
50
- return if @loaded
50
+ return if @loaded
51
51
  raise FileNotFound unless file?
52
-
52
+
53
53
  read_file!
54
54
  read_metadata!
55
-
55
+
56
56
  @loaded = true
57
57
  end
58
-
58
+
59
59
  # The name for a layout is just the lowercase, first part of the file name.
60
60
  def name
61
- return "" unless file?
61
+ return "" unless file?
62
62
  @name ||= self.basename.to_s.downcase.strip.split('.')[0]
63
63
  end
64
-
64
+
65
65
  # A parent layout for this current layout file. If no layout is specified for this
66
66
  # layout's parent, then nil is returned. If there is a parent layout for this layout,
67
67
  # any pages using it will be rendered using this layout first, then sent to the parent
68
68
  # for further rendering.
69
69
  def parent
70
70
  return @parent if @parent
71
-
71
+
72
72
  if self.meta[:layout]
73
73
  @parent = self.site.find_layout(self.meta[:layout])
74
74
  else
75
75
  @parent = nil
76
76
  end
77
-
77
+
78
78
  @parent
79
79
  end
80
-
80
+
81
81
  def relative_file
82
82
  @relative_file ||= self.site.relative_path(self.file)
83
83
  end
84
-
84
+
85
85
  def reload!
86
86
  @template = nil
87
87
  @loaded = false
88
88
  @name = nil
89
89
  @engine = nil
90
90
  end
91
-
91
+
92
92
  # Render the given content against the current layout template.
93
93
  def render(content, page = nil, view = nil)
94
94
  if self.template
95
95
  view ||= View.new(self.site, page)
96
96
  result = self.template.render(view) { content }
97
-
97
+
98
98
  if self.parent
99
99
  result = self.parent.render(result, page, view)
100
100
  end
101
-
101
+
102
102
  view = nil
103
-
103
+
104
104
  result
105
105
  else
106
106
  content.respond_to?(:rendered_content) ? content.rendered_content : content.to_s
107
107
  end
108
108
  end
109
-
110
- # The render template to use for this layout. A template is only used if the
109
+
110
+ # The render template to use for this layout. A template is only used if the
111
111
  # file extension for the layout is a valid layout extension from the current
112
112
  # site.
113
113
  def template
114
114
  return @template if @template
115
-
115
+
116
116
  if template?
117
117
  @template = self.engine.new() { self.content }
118
118
  else
119
119
  nil
120
120
  end
121
121
  end
122
-
122
+
123
123
  # Does this file have the ability to be used as a template?
124
124
  #
125
125
  # This currently only works if the layout is a .erb file. Otherwise anything that
@@ -127,31 +127,36 @@ module Plate
127
127
  def template?
128
128
  self.site.page_engine_extensions.include?(self.extension)
129
129
  end
130
-
130
+
131
131
  # Is this layout equal to another page being sent?
132
132
  def ==(other)
133
133
  other = other.relative_file if other.respond_to?(:relative_file)
134
134
  self.id == other or self.relative_file == other
135
135
  end
136
-
136
+
137
+ # Compare two layouts, by name.
138
+ def <=>(other)
139
+ self.name <=> other.name
140
+ end
141
+
137
142
  protected
138
143
  # Read the file and store it in @content
139
144
  def read_file!
140
145
  self.content = file? ? File.read(self.file) : nil
141
146
  end
142
-
147
+
143
148
  # Reads all content from a layouts's meta data. At this time, the layout only supports
144
149
  # loading a parent layout. All other meta data is unused.
145
150
  #
146
151
  # Meta data is stored in YAML format within the head of a page after the -- declaration like so:
147
- #
152
+ #
148
153
  # ---
149
154
  # layout: default
150
155
  #
151
156
  # # Start of layout content
152
157
  def read_metadata!
153
158
  return unless self.content
154
-
159
+
155
160
  begin
156
161
  if matches = /^(---\n)(.*?)^\s*?$/m.match(self.content)
157
162
  if matches.size == 3
@@ -159,7 +164,7 @@ module Plate
159
164
  self.meta = YAML.load(matches[2])
160
165
  self.meta.symbolize_keys!
161
166
  end
162
- end
167
+ end
163
168
  rescue Exception => e
164
169
  self.meta = {}
165
170
  self.site.log(" ** Problem reading YAML for file #{relative_file} (#{e.message}). Meta data skipped")