bunto 1.0.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.
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