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,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
@@ -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
@@ -0,0 +1,10 @@
1
+ module Plate
2
+ class SourceNotFound < StandardError
3
+ end
4
+
5
+ class FileNotFound < StandardError
6
+ end
7
+
8
+ class NoPostDateProvided < StandardError
9
+ end
10
+ end
@@ -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