bunto 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.markdown +59 -0
  4. data/bin/bunto +51 -0
  5. data/lib/bunto.rb +179 -0
  6. data/lib/bunto/cleaner.rb +105 -0
  7. data/lib/bunto/collection.rb +205 -0
  8. data/lib/bunto/command.rb +65 -0
  9. data/lib/bunto/commands/build.rb +77 -0
  10. data/lib/bunto/commands/clean.rb +42 -0
  11. data/lib/bunto/commands/doctor.rb +114 -0
  12. data/lib/bunto/commands/help.rb +31 -0
  13. data/lib/bunto/commands/new.rb +82 -0
  14. data/lib/bunto/commands/serve.rb +204 -0
  15. data/lib/bunto/commands/serve/servlet.rb +61 -0
  16. data/lib/bunto/configuration.rb +323 -0
  17. data/lib/bunto/converter.rb +48 -0
  18. data/lib/bunto/converters/identity.rb +21 -0
  19. data/lib/bunto/converters/markdown.rb +92 -0
  20. data/lib/bunto/converters/markdown/kramdown_parser.rb +117 -0
  21. data/lib/bunto/converters/markdown/rdiscount_parser.rb +33 -0
  22. data/lib/bunto/converters/markdown/redcarpet_parser.rb +102 -0
  23. data/lib/bunto/converters/smartypants.rb +34 -0
  24. data/lib/bunto/convertible.rb +297 -0
  25. data/lib/bunto/deprecator.rb +46 -0
  26. data/lib/bunto/document.rb +444 -0
  27. data/lib/bunto/drops/bunto_drop.rb +21 -0
  28. data/lib/bunto/drops/collection_drop.rb +22 -0
  29. data/lib/bunto/drops/document_drop.rb +27 -0
  30. data/lib/bunto/drops/drop.rb +176 -0
  31. data/lib/bunto/drops/site_drop.rb +38 -0
  32. data/lib/bunto/drops/unified_payload_drop.rb +25 -0
  33. data/lib/bunto/drops/url_drop.rb +83 -0
  34. data/lib/bunto/entry_filter.rb +72 -0
  35. data/lib/bunto/errors.rb +10 -0
  36. data/lib/bunto/excerpt.rb +127 -0
  37. data/lib/bunto/external.rb +59 -0
  38. data/lib/bunto/filters.rb +367 -0
  39. data/lib/bunto/frontmatter_defaults.rb +188 -0
  40. data/lib/bunto/generator.rb +3 -0
  41. data/lib/bunto/hooks.rb +101 -0
  42. data/lib/bunto/layout.rb +49 -0
  43. data/lib/bunto/liquid_extensions.rb +22 -0
  44. data/lib/bunto/liquid_renderer.rb +39 -0
  45. data/lib/bunto/liquid_renderer/file.rb +50 -0
  46. data/lib/bunto/liquid_renderer/table.rb +94 -0
  47. data/lib/bunto/log_adapter.rb +115 -0
  48. data/lib/bunto/mime.types +800 -0
  49. data/lib/bunto/page.rb +180 -0
  50. data/lib/bunto/plugin.rb +96 -0
  51. data/lib/bunto/plugin_manager.rb +95 -0
  52. data/lib/bunto/post.rb +329 -0
  53. data/lib/bunto/publisher.rb +21 -0
  54. data/lib/bunto/reader.rb +126 -0
  55. data/lib/bunto/readers/collection_reader.rb +20 -0
  56. data/lib/bunto/readers/data_reader.rb +69 -0
  57. data/lib/bunto/readers/layout_reader.rb +53 -0
  58. data/lib/bunto/readers/page_reader.rb +21 -0
  59. data/lib/bunto/readers/post_reader.rb +62 -0
  60. data/lib/bunto/readers/static_file_reader.rb +21 -0
  61. data/lib/bunto/regenerator.rb +175 -0
  62. data/lib/bunto/related_posts.rb +56 -0
  63. data/lib/bunto/renderer.rb +191 -0
  64. data/lib/bunto/site.rb +391 -0
  65. data/lib/bunto/static_file.rb +141 -0
  66. data/lib/bunto/stevenson.rb +58 -0
  67. data/lib/bunto/tags/highlight.rb +122 -0
  68. data/lib/bunto/tags/include.rb +190 -0
  69. data/lib/bunto/tags/post_url.rb +88 -0
  70. data/lib/bunto/url.rb +136 -0
  71. data/lib/bunto/utils.rb +287 -0
  72. data/lib/bunto/utils/ansi.rb +59 -0
  73. data/lib/bunto/utils/platforms.rb +30 -0
  74. data/lib/bunto/version.rb +3 -0
  75. data/lib/site_template/.gitignore +3 -0
  76. data/lib/site_template/_config.yml +21 -0
  77. data/lib/site_template/_includes/footer.html +38 -0
  78. data/lib/site_template/_includes/head.html +12 -0
  79. data/lib/site_template/_includes/header.html +27 -0
  80. data/lib/site_template/_includes/icon-github.html +1 -0
  81. data/lib/site_template/_includes/icon-github.svg +1 -0
  82. data/lib/site_template/_includes/icon-twitter.html +1 -0
  83. data/lib/site_template/_includes/icon-twitter.svg +1 -0
  84. data/lib/site_template/_layouts/default.html +20 -0
  85. data/lib/site_template/_layouts/page.html +14 -0
  86. data/lib/site_template/_layouts/post.html +15 -0
  87. data/lib/site_template/_posts/0000-00-00-welcome-to-bunto.markdown.erb +25 -0
  88. data/lib/site_template/_sass/_base.scss +206 -0
  89. data/lib/site_template/_sass/_layout.scss +242 -0
  90. data/lib/site_template/_sass/_syntax-highlighting.scss +71 -0
  91. data/lib/site_template/about.md +15 -0
  92. data/lib/site_template/css/main.scss +53 -0
  93. data/lib/site_template/feed.xml +30 -0
  94. data/lib/site_template/index.html +23 -0
  95. metadata +252 -0
data/lib/bunto/page.rb ADDED
@@ -0,0 +1,180 @@
1
+ module Bunto
2
+ class Page
3
+ include Convertible
4
+
5
+ attr_writer :dir
6
+ attr_accessor :site, :pager
7
+ attr_accessor :name, :ext, :basename
8
+ attr_accessor :data, :content, :output
9
+
10
+ alias_method :extname, :ext
11
+
12
+ FORWARD_SLASH = '/'.freeze
13
+
14
+ # Attributes for Liquid templates
15
+ ATTRIBUTES_FOR_LIQUID = %w(
16
+ content
17
+ dir
18
+ name
19
+ path
20
+ url
21
+ )
22
+
23
+ # A set of extensions that are considered HTML or HTML-like so we
24
+ # should not alter them, this includes .xhtml through XHTM5.
25
+
26
+ HTML_EXTENSIONS = %W(
27
+ .html
28
+ .xhtml
29
+ .htm
30
+ )
31
+
32
+ # Initialize a new Page.
33
+ #
34
+ # site - The Site object.
35
+ # base - The String path to the source.
36
+ # dir - The String path between the source and the file.
37
+ # name - The String filename of the file.
38
+ def initialize(site, base, dir, name)
39
+ @site = site
40
+ @base = base
41
+ @dir = dir
42
+ @name = name
43
+
44
+ process(name)
45
+ read_yaml(File.join(base, dir), name)
46
+
47
+ data.default_proc = proc do |_, key|
48
+ site.frontmatter_defaults.find(File.join(dir, name), type, key)
49
+ end
50
+
51
+ Bunto::Hooks.trigger :pages, :post_init, self
52
+ end
53
+
54
+ # The generated directory into which the page will be placed
55
+ # upon generation. This is derived from the permalink or, if
56
+ # permalink is absent, we be '/'
57
+ #
58
+ # Returns the String destination directory.
59
+ def dir
60
+ if url.end_with?(FORWARD_SLASH)
61
+ url
62
+ else
63
+ url_dir = File.dirname(url)
64
+ url_dir.end_with?(FORWARD_SLASH) ? url_dir : "#{url_dir}/"
65
+ end
66
+ end
67
+
68
+ # The full path and filename of the post. Defined in the YAML of the post
69
+ # body.
70
+ #
71
+ # Returns the String permalink or nil if none has been set.
72
+ def permalink
73
+ data.nil? ? nil : data['permalink']
74
+ end
75
+
76
+ # The template of the permalink.
77
+ #
78
+ # Returns the template String.
79
+ def template
80
+ if !html?
81
+ "/:path/:basename:output_ext"
82
+ elsif index?
83
+ "/:path/"
84
+ else
85
+ Utils.add_permalink_suffix("/:path/:basename", site.permalink_style)
86
+ end
87
+ end
88
+
89
+ # The generated relative url of this page. e.g. /about.html.
90
+ #
91
+ # Returns the String url.
92
+ def url
93
+ @url ||= URL.new({
94
+ :template => template,
95
+ :placeholders => url_placeholders,
96
+ :permalink => permalink
97
+ }).to_s
98
+ end
99
+
100
+ # Returns a hash of URL placeholder names (as symbols) mapping to the
101
+ # desired placeholder replacements. For details see "url.rb"
102
+ def url_placeholders
103
+ {
104
+ :path => @dir,
105
+ :basename => basename,
106
+ :output_ext => output_ext
107
+ }
108
+ end
109
+
110
+ # Extract information from the page filename.
111
+ #
112
+ # name - The String filename of the page file.
113
+ #
114
+ # Returns nothing.
115
+ def process(name)
116
+ self.ext = File.extname(name)
117
+ self.basename = name[0..-ext.length - 1]
118
+ end
119
+
120
+ # Add any necessary layouts to this post
121
+ #
122
+ # layouts - The Hash of {"name" => "layout"}.
123
+ # site_payload - The site payload Hash.
124
+ #
125
+ # Returns nothing.
126
+ def render(layouts, site_payload)
127
+ site_payload["page"] = to_liquid
128
+ site_payload["paginator"] = pager.to_liquid
129
+
130
+ do_layout(site_payload, layouts)
131
+ end
132
+
133
+ # The path to the source file
134
+ #
135
+ # Returns the path to the source file
136
+ def path
137
+ data.fetch('path') { relative_path.sub(/\A\//, '') }
138
+ end
139
+
140
+ # The path to the page source file, relative to the site source
141
+ def relative_path
142
+ File.join(*[@dir, @name].map(&:to_s).reject(&:empty?))
143
+ end
144
+
145
+ # Obtain destination path.
146
+ #
147
+ # dest - The String path to the destination dir.
148
+ #
149
+ # Returns the destination file path String.
150
+ def destination(dest)
151
+ path = site.in_dest_dir(dest, URL.unescape_path(url))
152
+ path = File.join(path, "index") if url.end_with?("/")
153
+ path << output_ext unless path.end_with? output_ext
154
+ path
155
+ end
156
+
157
+ # Returns the object as a debug String.
158
+ def inspect
159
+ "#<Bunto:Page @name=#{name.inspect}>"
160
+ end
161
+
162
+ # Returns the Boolean of whether this Page is HTML or not.
163
+ def html?
164
+ HTML_EXTENSIONS.include?(output_ext)
165
+ end
166
+
167
+ # Returns the Boolean of whether this Page is an index file or not.
168
+ def index?
169
+ basename == 'index'
170
+ end
171
+
172
+ def trigger_hooks(hook_name, *args)
173
+ Bunto::Hooks.trigger :pages, hook_name, self, *args
174
+ end
175
+
176
+ def write?
177
+ true
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,96 @@
1
+ module Bunto
2
+ class Plugin
3
+ PRIORITIES = {
4
+ :low => -10,
5
+ :highest => 100,
6
+ :lowest => -100,
7
+ :normal => 0,
8
+ :high => 10
9
+ }
10
+
11
+ #
12
+
13
+ def self.inherited(const)
14
+ return catch_inheritance(const) do |const_|
15
+ catch_inheritance(const_)
16
+ end
17
+ end
18
+
19
+ #
20
+
21
+ def self.catch_inheritance(const)
22
+ const.define_singleton_method :inherited do |const_|
23
+ (@children ||= Set.new).add const_
24
+ if block_given?
25
+ yield const_
26
+ end
27
+ end
28
+ end
29
+
30
+ #
31
+
32
+ def self.descendants
33
+ @children ||= Set.new
34
+ out = @children.map(&:descendants)
35
+ out << self unless superclass == Plugin
36
+ Set.new(out).flatten
37
+ end
38
+
39
+ # Get or set the priority of this plugin. When called without an
40
+ # argument it returns the priority. When an argument is given, it will
41
+ # set the priority.
42
+ #
43
+ # priority - The Symbol priority (default: nil). Valid options are:
44
+ # :lowest, :low, :normal, :high, :highest
45
+ #
46
+ # Returns the Symbol priority.
47
+ def self.priority(priority = nil)
48
+ @priority ||= nil
49
+ if priority && PRIORITIES.key?(priority)
50
+ @priority = priority
51
+ end
52
+ @priority || :normal
53
+ end
54
+
55
+ # Get or set the safety of this plugin. When called without an argument
56
+ # it returns the safety. When an argument is given, it will set the
57
+ # safety.
58
+ #
59
+ # safe - The Boolean safety (default: nil).
60
+ #
61
+ # Returns the safety Boolean.
62
+ def self.safe(safe = nil)
63
+ if safe
64
+ @safe = safe
65
+ end
66
+ @safe || false
67
+ end
68
+
69
+ # Spaceship is priority [higher -> lower]
70
+ #
71
+ # other - The class to be compared.
72
+ #
73
+ # Returns -1, 0, 1.
74
+ def self.<=>(other)
75
+ PRIORITIES[other.priority] <=> PRIORITIES[self.priority]
76
+ end
77
+
78
+ # Spaceship is priority [higher -> lower]
79
+ #
80
+ # other - The class to be compared.
81
+ #
82
+ # Returns -1, 0, 1.
83
+ def <=>(other)
84
+ self.class <=> other.class
85
+ end
86
+
87
+ # Initialize a new plugin. This should be overridden by the subclass.
88
+ #
89
+ # config - The Hash of configuration options.
90
+ #
91
+ # Returns a new instance.
92
+ def initialize(config = {})
93
+ # no-op for default
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,95 @@
1
+ module Bunto
2
+ class PluginManager
3
+ attr_reader :site
4
+
5
+ # Create an instance of this class.
6
+ #
7
+ # site - the instance of Bunto::Site we're concerned with
8
+ #
9
+ # Returns nothing
10
+ def initialize(site)
11
+ @site = site
12
+ end
13
+
14
+ # Require all the plugins which are allowed.
15
+ #
16
+ # Returns nothing
17
+ def conscientious_require
18
+ require_plugin_files
19
+ require_gems
20
+ deprecation_checks
21
+ end
22
+
23
+ # Require each of the gem plugins specified.
24
+ #
25
+ # Returns nothing.
26
+ def require_gems
27
+ Bunto::External.require_with_graceful_fail(site.gems.select { |gem| plugin_allowed?(gem) })
28
+ end
29
+
30
+ def self.require_from_bundler
31
+ if !ENV["BUNTO_NO_BUNDLER_REQUIRE"] && File.file?("Gemfile")
32
+ require "bundler"
33
+ Bundler.setup # puts all groups on the load path
34
+ required_gems = Bundler.require(:bunto_plugins) # requires the gems in this group only
35
+ Bunto.logger.debug("PluginManager:", "Required #{required_gems.map(&:name).join(', ')}")
36
+ ENV["BUNTO_NO_BUNDLER_REQUIRE"] = "true"
37
+ true
38
+ else
39
+ false
40
+ end
41
+ rescue LoadError, Bundler::GemfileNotFound
42
+ false
43
+ end
44
+
45
+ # Check whether a gem plugin is allowed to be used during this build.
46
+ #
47
+ # gem_name - the name of the gem
48
+ #
49
+ # Returns true if the gem name is in the whitelist or if the site is not
50
+ # in safe mode.
51
+ def plugin_allowed?(gem_name)
52
+ !site.safe || whitelist.include?(gem_name)
53
+ end
54
+
55
+ # Build an array of allowed plugin gem names.
56
+ #
57
+ # Returns an array of strings, each string being the name of a gem name
58
+ # that is allowed to be used.
59
+ def whitelist
60
+ @whitelist ||= Array[site.config['whitelist']].flatten
61
+ end
62
+
63
+ # Require all .rb files if safe mode is off
64
+ #
65
+ # Returns nothing.
66
+ def require_plugin_files
67
+ unless site.safe
68
+ plugins_path.each do |plugin_search_path|
69
+ plugin_files = Utils.safe_glob(plugin_search_path, File.join("**", "*.rb"))
70
+ Bunto::External.require_with_graceful_fail(plugin_files)
71
+ end
72
+ end
73
+ end
74
+
75
+ # Public: Setup the plugin search path
76
+ #
77
+ # Returns an Array of plugin search paths
78
+ def plugins_path
79
+ if site.config['plugins_dir'] == Bunto::Configuration::DEFAULTS['plugins_dir']
80
+ [site.in_source_dir(site.config['plugins_dir'])]
81
+ else
82
+ Array(site.config['plugins_dir']).map { |d| File.expand_path(d) }
83
+ end
84
+ end
85
+
86
+ def deprecation_checks
87
+ pagination_included = (site.config['gems'] || []).include?('bunto-paginate') || defined?(Bunto::Paginate)
88
+ if site.config['paginate'] && !pagination_included
89
+ Bunto::Deprecator.deprecation_message "You appear to have pagination " \
90
+ "turned on, but you haven't included the `bunto-paginate` gem. " \
91
+ "Ensure you have `gems: [bunto-paginate]` in your configuration file."
92
+ end
93
+ end
94
+ end
95
+ end
data/lib/bunto/post.rb ADDED
@@ -0,0 +1,329 @@
1
+ module Bunto
2
+ class Post
3
+ include Comparable
4
+ include Convertible
5
+
6
+ # Valid post name regex.
7
+ MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/
8
+
9
+ EXCERPT_ATTRIBUTES_FOR_LIQUID = %w[
10
+ title
11
+ url
12
+ dir
13
+ date
14
+ id
15
+ categories
16
+ next
17
+ previous
18
+ tags
19
+ path
20
+ ]
21
+
22
+ # Attributes for Liquid templates
23
+ ATTRIBUTES_FOR_LIQUID = EXCERPT_ATTRIBUTES_FOR_LIQUID + %w[
24
+ content
25
+ excerpt
26
+ excerpt_separator
27
+ draft?
28
+ ]
29
+
30
+ # Post name validator. Post filenames must be like:
31
+ # 2008-11-05-my-awesome-post.textile
32
+ #
33
+ # Returns true if valid, false if not.
34
+ def self.valid?(name)
35
+ name =~ MATCHER
36
+ end
37
+
38
+ attr_accessor :site
39
+ attr_accessor :data, :extracted_excerpt, :content, :output, :ext
40
+ attr_accessor :date, :slug, :tags, :categories
41
+
42
+ attr_reader :name
43
+
44
+ # Initialize this Post instance.
45
+ #
46
+ # site - The Site.
47
+ # base - The String path to the dir containing the post file.
48
+ # name - The String filename of the post file.
49
+ #
50
+ # Returns the new Post.
51
+ def initialize(site, source, dir, name)
52
+ @site = site
53
+ @dir = dir
54
+ @base = containing_dir(dir)
55
+ @name = name
56
+
57
+ self.categories = dir.split('/').reject { |x| x.empty? }
58
+ process(name)
59
+ read_yaml(@base, name)
60
+
61
+ data.default_proc = proc do |hash, key|
62
+ site.frontmatter_defaults.find(relative_path, type, key)
63
+ end
64
+
65
+ if data.key?('date')
66
+ self.date = Utils.parse_date(data["date"].to_s, "Post '#{relative_path}' does not have a valid date in the YAML front matter.")
67
+ end
68
+
69
+ populate_categories
70
+ populate_tags
71
+ end
72
+
73
+ def published?
74
+ if data.key?('published') && data['published'] == false
75
+ false
76
+ else
77
+ true
78
+ end
79
+ end
80
+
81
+ def populate_categories
82
+ categories_from_data = Utils.pluralized_array_from_hash(data, 'category', 'categories')
83
+ self.categories = (
84
+ Array(categories) + categories_from_data
85
+ ).map { |c| c.to_s }.flatten.uniq
86
+ end
87
+
88
+ def populate_tags
89
+ self.tags = Utils.pluralized_array_from_hash(data, "tag", "tags").flatten
90
+ end
91
+
92
+ # Get the full path to the directory containing the post files
93
+ def containing_dir(dir)
94
+ site.in_source_dir(dir, '_posts')
95
+ end
96
+
97
+ # Read the YAML frontmatter.
98
+ #
99
+ # base - The String path to the dir containing the file.
100
+ # name - The String filename of the file.
101
+ #
102
+ # Returns nothing.
103
+ def read_yaml(base, name)
104
+ super(base, name)
105
+ self.extracted_excerpt = extract_excerpt
106
+ end
107
+
108
+ # The post excerpt. This is either a custom excerpt
109
+ # set in YAML front matter or the result of extract_excerpt.
110
+ #
111
+ # Returns excerpt string.
112
+ def excerpt
113
+ data.fetch('excerpt') { extracted_excerpt.to_s }
114
+ end
115
+
116
+ # Public: the Post title, from the YAML Front-Matter or from the slug
117
+ #
118
+ # Returns the post title
119
+ def title
120
+ data.fetch('title') { titleized_slug }
121
+ end
122
+
123
+ # Public: the Post excerpt_separator, from the YAML Front-Matter or site default
124
+ # excerpt_separator value
125
+ #
126
+ # Returns the post excerpt_separator
127
+ def excerpt_separator
128
+ (data['excerpt_separator'] || site.config['excerpt_separator']).to_s
129
+ end
130
+
131
+ # Turns the post slug into a suitable title
132
+ def titleized_slug
133
+ slug.split('-').select {|w| w.capitalize! || w }.join(' ')
134
+ end
135
+
136
+ # Public: the path to the post relative to the site source,
137
+ # from the YAML Front-Matter or from a combination of
138
+ # the directory it's in, "_posts", and the name of the
139
+ # post file
140
+ #
141
+ # Returns the path to the file relative to the site source
142
+ def path
143
+ data.fetch('path') { relative_path.sub(/\A\//, '') }
144
+ end
145
+
146
+ # The path to the post source file, relative to the site source
147
+ def relative_path
148
+ File.join(*[@dir, "_posts", @name].map(&:to_s).reject(&:empty?))
149
+ end
150
+
151
+ # Compares Post objects. First compares the Post date. If the dates are
152
+ # equal, it compares the Post slugs.
153
+ #
154
+ # other - The other Post we are comparing to.
155
+ #
156
+ # Returns -1, 0, 1
157
+ def <=>(other)
158
+ cmp = self.date <=> other.date
159
+ if 0 == cmp
160
+ cmp = self.slug <=> other.slug
161
+ end
162
+ return cmp
163
+ end
164
+
165
+ # Extract information from the post filename.
166
+ #
167
+ # name - The String filename of the post file.
168
+ #
169
+ # Returns nothing.
170
+ def process(name)
171
+ m, cats, date, slug, ext = *name.match(MATCHER)
172
+ self.date = Utils.parse_date(date, "Post '#{relative_path}' does not have a valid date in the filename.")
173
+ self.slug = slug
174
+ self.ext = ext
175
+ end
176
+
177
+ # The generated directory into which the post will be placed
178
+ # upon generation. This is derived from the permalink or, if
179
+ # permalink is absent, set to the default date
180
+ # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing.
181
+ #
182
+ # Returns the String directory.
183
+ def dir
184
+ File.dirname(url)
185
+ end
186
+
187
+ # The full path and filename of the post. Defined in the YAML of the post
188
+ # body (optional).
189
+ #
190
+ # Returns the String permalink.
191
+ def permalink
192
+ data && data['permalink']
193
+ end
194
+
195
+ def template
196
+ case site.permalink_style
197
+ when :pretty
198
+ "/:categories/:year/:month/:day/:title/"
199
+ when :none
200
+ "/:categories/:title.html"
201
+ when :date
202
+ "/:categories/:year/:month/:day/:title.html"
203
+ when :ordinal
204
+ "/:categories/:year/:y_day/:title.html"
205
+ else
206
+ site.permalink_style.to_s
207
+ end
208
+ end
209
+
210
+ # The generated relative url of this post.
211
+ #
212
+ # Returns the String url.
213
+ def url
214
+ @url ||= URL.new({
215
+ :template => template,
216
+ :placeholders => url_placeholders,
217
+ :permalink => permalink
218
+ }).to_s
219
+ end
220
+
221
+ # Returns a hash of URL placeholder names (as symbols) mapping to the
222
+ # desired placeholder replacements. For details see "url.rb"
223
+ def url_placeholders
224
+ {
225
+ :year => date.strftime("%Y"),
226
+ :month => date.strftime("%m"),
227
+ :day => date.strftime("%d"),
228
+ :title => slug,
229
+ :i_day => date.strftime("%-d"),
230
+ :i_month => date.strftime("%-m"),
231
+ :categories => (categories || []).map { |c| c.to_s.downcase }.uniq.join('/'),
232
+ :short_month => date.strftime("%b"),
233
+ :short_year => date.strftime("%y"),
234
+ :y_day => date.strftime("%j"),
235
+ :output_ext => output_ext
236
+ }
237
+ end
238
+
239
+ # The UID for this post (useful in feeds).
240
+ # e.g. /2008/11/05/my-awesome-post
241
+ #
242
+ # Returns the String UID.
243
+ def id
244
+ File.join(dir, slug)
245
+ end
246
+
247
+ # Calculate related posts.
248
+ #
249
+ # Returns an Array of related Posts.
250
+ def related_posts(posts)
251
+ Bunto::RelatedPosts.new(self).build
252
+ end
253
+
254
+ # Add any necessary layouts to this post.
255
+ #
256
+ # layouts - A Hash of {"name" => "layout"}.
257
+ # site_payload - The site payload hash.
258
+ #
259
+ # Returns nothing.
260
+ def render(layouts, site_payload)
261
+ # construct payload
262
+ payload = Utils.deep_merge_hashes({
263
+ "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
264
+ "page" => to_liquid(self.class::EXCERPT_ATTRIBUTES_FOR_LIQUID)
265
+ }, site_payload)
266
+
267
+ if generate_excerpt?
268
+ extracted_excerpt.do_layout(payload, {})
269
+ end
270
+
271
+ do_layout(payload.merge({"page" => to_liquid}), layouts)
272
+ end
273
+
274
+ # Obtain destination path.
275
+ #
276
+ # dest - The String path to the destination dir.
277
+ #
278
+ # Returns destination file path String.
279
+ def destination(dest)
280
+ # The url needs to be unescaped in order to preserve the correct filename
281
+ path = site.in_dest_dir(dest, URL.unescape_path(url))
282
+ path = File.join(path, "index.html") if self.url.end_with?("/")
283
+ path << output_ext unless path.end_with?(output_ext)
284
+ path
285
+ end
286
+
287
+ # Returns the shorthand String identifier of this Post.
288
+ def inspect
289
+ "<Post: #{id}>"
290
+ end
291
+
292
+ def next
293
+ pos = site.posts.index {|post| post.equal?(self) }
294
+ if pos && pos < site.posts.length - 1
295
+ site.posts[pos + 1]
296
+ else
297
+ nil
298
+ end
299
+ end
300
+
301
+ def previous
302
+ pos = site.posts.index {|post| post.equal?(self) }
303
+ if pos && pos > 0
304
+ site.posts[pos - 1]
305
+ else
306
+ nil
307
+ end
308
+ end
309
+
310
+ # Returns if this Post is a Draft
311
+ def draft?
312
+ is_a?(Bunto::Draft)
313
+ end
314
+
315
+ protected
316
+
317
+ def extract_excerpt
318
+ if generate_excerpt?
319
+ Bunto::Excerpt.new(self)
320
+ else
321
+ ""
322
+ end
323
+ end
324
+
325
+ def generate_excerpt?
326
+ !excerpt_separator.empty?
327
+ end
328
+ end
329
+ end