moft 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 (48) hide show
  1. data/Gemfile +2 -0
  2. data/LICENSE +21 -0
  3. data/bin/moft +83 -0
  4. data/lib/moft.rb +89 -0
  5. data/lib/moft/command.rb +27 -0
  6. data/lib/moft/commands/build.rb +64 -0
  7. data/lib/moft/commands/new.rb +50 -0
  8. data/lib/moft/commands/serve.rb +33 -0
  9. data/lib/moft/configuration.rb +171 -0
  10. data/lib/moft/converter.rb +48 -0
  11. data/lib/moft/converters/identity.rb +21 -0
  12. data/lib/moft/converters/markdown.rb +43 -0
  13. data/lib/moft/converters/markdown/kramdown_parser.rb +44 -0
  14. data/lib/moft/converters/markdown/maruku_parser.rb +47 -0
  15. data/lib/moft/converters/markdown/rdiscount_parser.rb +26 -0
  16. data/lib/moft/converters/markdown/redcarpet_parser.rb +40 -0
  17. data/lib/moft/converters/textile.rb +50 -0
  18. data/lib/moft/convertible.rb +152 -0
  19. data/lib/moft/core_ext.rb +68 -0
  20. data/lib/moft/deprecator.rb +34 -0
  21. data/lib/moft/draft.rb +35 -0
  22. data/lib/moft/errors.rb +4 -0
  23. data/lib/moft/filters.rb +141 -0
  24. data/lib/moft/generator.rb +4 -0
  25. data/lib/moft/generators/pagination.rb +131 -0
  26. data/lib/moft/layout.rb +42 -0
  27. data/lib/moft/logger.rb +52 -0
  28. data/lib/moft/mime.types +85 -0
  29. data/lib/moft/page.rb +147 -0
  30. data/lib/moft/plugin.rb +75 -0
  31. data/lib/moft/post.rb +377 -0
  32. data/lib/moft/site.rb +422 -0
  33. data/lib/moft/static_file.rb +70 -0
  34. data/lib/moft/tags/gist.rb +30 -0
  35. data/lib/moft/tags/highlight.rb +83 -0
  36. data/lib/moft/tags/include.rb +37 -0
  37. data/lib/moft/tags/post_url.rb +46 -0
  38. data/lib/site_template/_config.yml +1 -0
  39. data/lib/site_template/_layouts/default.html +38 -0
  40. data/lib/site_template/_layouts/post.html +6 -0
  41. data/lib/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +24 -0
  42. data/lib/site_template/css/screen.css +189 -0
  43. data/lib/site_template/css/syntax.css +60 -0
  44. data/lib/site_template/images/.gitkeep +0 -0
  45. data/lib/site_template/images/rss.png +0 -0
  46. data/lib/site_template/index.html +13 -0
  47. data/moft.gemspec +100 -0
  48. metadata +412 -0
@@ -0,0 +1,52 @@
1
+ module Moft
2
+ module Logger
3
+ # Public: Print a moft message to stdout
4
+ #
5
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
6
+ # message - the message detail
7
+ #
8
+ # Returns nothing
9
+ def self.info(topic, message)
10
+ $stdout.puts message(topic, message)
11
+ end
12
+
13
+ # Public: Print a moft message to stderr
14
+ #
15
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
16
+ # message - the message detail
17
+ #
18
+ # Returns nothing
19
+ def self.warn(topic, message)
20
+ $stderr.puts message(topic, message).yellow
21
+ end
22
+
23
+ # Public: Print a moft error message to stderr
24
+ #
25
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
26
+ # message - the message detail
27
+ #
28
+ # Returns nothing
29
+ def self.error(topic, message)
30
+ $stderr.puts message(topic, message).red
31
+ end
32
+
33
+ # Public: Build a Moft topic method
34
+ #
35
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
36
+ # message - the message detail
37
+ #
38
+ # Returns the formatted message
39
+ def self.message(topic, message)
40
+ formatted_topic(topic) + message.gsub(/\s+/, ' ')
41
+ end
42
+
43
+ # Public: Format the topic
44
+ #
45
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
46
+ #
47
+ # Returns the formatted topic statement
48
+ def self.formatted_topic(topic)
49
+ "#{topic} ".rjust(20)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,85 @@
1
+ # These are the same MIME types that GitHub Pages uses as of 17 Mar 2013.
2
+
3
+ text/html html htm shtml
4
+ text/css css
5
+ text/xml xml rss xsl
6
+ image/gif gif
7
+ image/jpeg jpeg jpg
8
+ application/x-javascript js
9
+ application/atom+xml atom
10
+
11
+ text/mathml mml
12
+ text/plain txt
13
+ text/vnd.sun.j2me.app-descriptor jad
14
+ text/vnd.wap.wml wml
15
+ text/x-component htc
16
+ text/cache-manifest manifest appcache
17
+ text/coffeescript coffee
18
+ text/plain pde
19
+ text/plain md markdown
20
+
21
+ image/png png
22
+ image/svg+xml svg
23
+ image/tiff tif tiff
24
+ image/vnd.wap.wbmp wbmp
25
+ image/x-icon ico
26
+ image/x-jng jng
27
+ image/x-ms-bmp bmp
28
+
29
+ application/json json
30
+ application/java-archive jar ear
31
+ application/mac-binhex40 hqx
32
+ application/msword doc
33
+ application/pdf pdf
34
+ application/postscript ps eps ai
35
+ application/rdf+xml rdf
36
+ application/rtf rtf
37
+ text/vcard vcf vcard
38
+ application/vnd.apple.pkpass pkpass
39
+ application/vnd.ms-excel xls
40
+ application/vnd.ms-powerpoint ppt
41
+ application/vnd.wap.wmlc wmlc
42
+ application/xhtml+xml xhtml
43
+ application/x-chrome-extension crx
44
+ application/x-cocoa cco
45
+ application/x-font-ttf ttf
46
+ application/x-java-archive-diff jardiff
47
+ application/x-java-jnlp-file jnlp
48
+ application/x-makeself run
49
+ application/x-ns-proxy-autoconfig pac
50
+ application/x-perl pl pm
51
+ application/x-pilot prc pdb
52
+ application/x-rar-compressed rar
53
+ application/x-redhat-package-manager rpm
54
+ application/x-sea sea
55
+ application/x-shockwave-flash swf
56
+ application/x-stuffit sit
57
+ application/x-tcl tcl tk
58
+ application/x-web-app-manifest+json webapp
59
+ application/x-x509-ca-cert der pem crt
60
+ application/x-xpinstall xpi
61
+ application/x-zip war
62
+ application/zip zip
63
+
64
+ application/octet-stream bin exe dll
65
+ application/octet-stream deb
66
+ application/octet-stream dmg
67
+ application/octet-stream eot
68
+ application/octet-stream iso img
69
+ application/octet-stream msi msp msm
70
+
71
+ audio/midi mid midi kar
72
+ audio/mpeg mp3
73
+ audio/x-realaudio ra
74
+ audio/ogg ogg
75
+
76
+ video/3gpp 3gpp 3gp
77
+ video/mpeg mpeg mpg
78
+ video/quicktime mov
79
+ video/x-flv flv
80
+ video/x-mng mng
81
+ video/x-ms-asf asx asf
82
+ video/x-ms-wmv wmv
83
+ video/x-msvideo avi
84
+ video/ogg ogv
85
+ video/webm webm
@@ -0,0 +1,147 @@
1
+ module Moft
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
+ # Initialize a new Page.
11
+ #
12
+ # site - The Site object.
13
+ # base - The String path to the source.
14
+ # dir - The String path between the source and the file.
15
+ # name - The String filename of the file.
16
+ def initialize(site, base, dir, name)
17
+ @site = site
18
+ @base = base
19
+ @dir = dir
20
+ @name = name
21
+
22
+ self.process(name)
23
+ self.read_yaml(File.join(base, dir), name)
24
+ end
25
+
26
+ # The generated directory into which the page will be placed
27
+ # upon generation. This is derived from the permalink or, if
28
+ # permalink is absent, we be '/'
29
+ #
30
+ # Returns the String destination directory.
31
+ def dir
32
+ url[-1, 1] == '/' ? url : File.dirname(url)
33
+ end
34
+
35
+ # The full path and filename of the post. Defined in the YAML of the post
36
+ # body.
37
+ #
38
+ # Returns the String permalink or nil if none has been set.
39
+ def permalink
40
+ self.data && self.data['permalink']
41
+ end
42
+
43
+ # The template of the permalink.
44
+ #
45
+ # Returns the template String.
46
+ def template
47
+ if self.site.permalink_style == :pretty
48
+ if index? && html?
49
+ "/:path/"
50
+ elsif html?
51
+ "/:path/:basename/"
52
+ else
53
+ "/:path/:basename:output_ext"
54
+ end
55
+ else
56
+ "/:path/:basename:output_ext"
57
+ end
58
+ end
59
+
60
+ # The generated relative url of this page. e.g. /about.html.
61
+ #
62
+ # Returns the String url.
63
+ def url
64
+ return @url if @url
65
+
66
+ url = if permalink
67
+ permalink
68
+ else
69
+ {
70
+ "path" => @dir,
71
+ "basename" => self.basename,
72
+ "output_ext" => self.output_ext,
73
+ }.inject(template) { |result, token|
74
+ result.gsub(/:#{token.first}/, token.last)
75
+ }.gsub(/\/\//, "/")
76
+ end
77
+
78
+ # sanitize url
79
+ @url = url.split('/').reject{ |part| part =~ /^\.+$/ }.join('/')
80
+ @url += "/" if url =~ /\/$/
81
+ @url
82
+ end
83
+
84
+ # Extract information from the page filename.
85
+ #
86
+ # name - The String filename of the page file.
87
+ #
88
+ # Returns nothing.
89
+ def process(name)
90
+ self.ext = File.extname(name)
91
+ self.basename = name[0 .. -self.ext.length-1]
92
+ end
93
+
94
+ # Add any necessary layouts to this post
95
+ #
96
+ # layouts - The Hash of {"name" => "layout"}.
97
+ # site_payload - The site payload Hash.
98
+ #
99
+ # Returns nothing.
100
+ def render(layouts, site_payload)
101
+ payload = {
102
+ "page" => self.to_liquid,
103
+ 'paginator' => pager.to_liquid
104
+ }.deep_merge(site_payload)
105
+
106
+ do_layout(payload, layouts)
107
+ end
108
+
109
+ # Convert this Page's data to a Hash suitable for use by Liquid.
110
+ #
111
+ # Returns the Hash representation of this Page.
112
+ def to_liquid
113
+ self.data.deep_merge({
114
+ "url" => self.url,
115
+ "content" => self.content,
116
+ "path" => self.data['path'] || File.join(@dir, @name).sub(/\A\//, '') })
117
+ end
118
+
119
+ # Obtain destination path.
120
+ #
121
+ # dest - The String path to the destination dir.
122
+ #
123
+ # Returns the destination file path String.
124
+ def destination(dest)
125
+ # The url needs to be unescaped in order to preserve the correct
126
+ # filename.
127
+ path = File.join(dest, CGI.unescape(self.url))
128
+ path = File.join(path, "index.html") if self.url =~ /\/$/
129
+ path
130
+ end
131
+
132
+ # Returns the object as a debug String.
133
+ def inspect
134
+ "#<Moft:Page @name=#{self.name.inspect}>"
135
+ end
136
+
137
+ # Returns the Boolean of whether this Page is HTML or not.
138
+ def html?
139
+ output_ext == '.html'
140
+ end
141
+
142
+ # Returns the Boolean of whether this Page is an index file or not.
143
+ def index?
144
+ basename == 'index'
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,75 @@
1
+ module Moft
2
+ class Plugin
3
+ PRIORITIES = { :lowest => -100,
4
+ :low => -10,
5
+ :normal => 0,
6
+ :high => 10,
7
+ :highest => 100 }
8
+
9
+ # Install a hook so that subclasses are recorded. This method is only
10
+ # ever called by Ruby itself.
11
+ #
12
+ # base - The Class subclass.
13
+ #
14
+ # Returns nothing.
15
+ def self.inherited(base)
16
+ subclasses << base
17
+ subclasses.sort!
18
+ end
19
+
20
+ # The list of Classes that have been subclassed.
21
+ #
22
+ # Returns an Array of Class objects.
23
+ def self.subclasses
24
+ @subclasses ||= []
25
+ end
26
+
27
+ # Get or set the priority of this plugin. When called without an
28
+ # argument it returns the priority. When an argument is given, it will
29
+ # set the priority.
30
+ #
31
+ # priority - The Symbol priority (default: nil). Valid options are:
32
+ # :lowest, :low, :normal, :high, :highest
33
+ #
34
+ # Returns the Symbol priority.
35
+ def self.priority(priority = nil)
36
+ @priority ||= nil
37
+ if priority && PRIORITIES.has_key?(priority)
38
+ @priority = priority
39
+ end
40
+ @priority || :normal
41
+ end
42
+
43
+ # Get or set the safety of this plugin. When called without an argument
44
+ # it returns the safety. When an argument is given, it will set the
45
+ # safety.
46
+ #
47
+ # safe - The Boolean safety (default: nil).
48
+ #
49
+ # Returns the safety Boolean.
50
+ def self.safe(safe = nil)
51
+ if safe
52
+ @safe = safe
53
+ end
54
+ @safe || false
55
+ end
56
+
57
+ # Spaceship is priority [higher -> lower]
58
+ #
59
+ # other - The class to be compared.
60
+ #
61
+ # Returns -1, 0, 1.
62
+ def self.<=>(other)
63
+ PRIORITIES[other.priority] <=> PRIORITIES[self.priority]
64
+ end
65
+
66
+ # Initialize a new plugin. This should be overridden by the subclass.
67
+ #
68
+ # config - The Hash of configuration options.
69
+ #
70
+ # Returns a new instance.
71
+ def initialize(config = {})
72
+ # no-op for default
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,377 @@
1
+ module Moft
2
+ class Post
3
+ include Comparable
4
+ include Convertible
5
+
6
+ class << self
7
+ attr_accessor :lsi
8
+ end
9
+
10
+ # Valid post name regex.
11
+ MATCHER = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/
12
+
13
+ # Attributes for Liquid templates
14
+ ATTRIBUTES_FOR_LIQUID = %w[
15
+ title
16
+ url
17
+ date
18
+ id
19
+ categories
20
+ next
21
+ previous
22
+ tags
23
+ content
24
+ excerpt
25
+ path
26
+ ]
27
+
28
+ # Post name validator. Post filenames must be like:
29
+ # 2008-11-05-my-awesome-post.textile
30
+ #
31
+ # Returns true if valid, false if not.
32
+ def self.valid?(name)
33
+ name =~ MATCHER
34
+ end
35
+
36
+ attr_accessor :site
37
+ attr_accessor :data, :extracted_excerpt, :content, :output, :ext
38
+ attr_accessor :date, :slug, :published, :tags, :categories
39
+
40
+ attr_reader :name
41
+
42
+ # Initialize this Post instance.
43
+ #
44
+ # site - The Site.
45
+ # base - The String path to the dir containing the post file.
46
+ # name - The String filename of the post file.
47
+ #
48
+ # Returns the new Post.
49
+ def initialize(site, source, dir, name)
50
+ @site = site
51
+ @dir = dir
52
+ @base = self.containing_dir(source, dir)
53
+ @name = name
54
+
55
+ self.categories = dir.downcase.split('/').reject { |x| x.empty? }
56
+ self.process(name)
57
+ self.read_yaml(@base, name)
58
+
59
+ if self.data.has_key?('date')
60
+ self.date = Time.parse(self.data["date"].to_s)
61
+ end
62
+
63
+ self.published = self.published?
64
+
65
+ self.populate_categories
66
+ self.populate_tags
67
+ end
68
+
69
+ def published?
70
+ if self.data.has_key?('published') && self.data['published'] == false
71
+ false
72
+ else
73
+ true
74
+ end
75
+ end
76
+
77
+ def populate_categories
78
+ if self.categories.empty?
79
+ self.categories = self.data.pluralized_array('category', 'categories').map {|c| c.downcase}
80
+ end
81
+ self.categories.flatten!
82
+ end
83
+
84
+ def populate_tags
85
+ self.tags = self.data.pluralized_array("tag", "tags").flatten
86
+ end
87
+
88
+ # Get the full path to the directory containing the post files
89
+ def containing_dir(source, dir)
90
+ return File.join(source, dir, '_posts')
91
+ end
92
+
93
+ # Read the YAML frontmatter.
94
+ #
95
+ # base - The String path to the dir containing the file.
96
+ # name - The String filename of the file.
97
+ #
98
+ # Returns nothing.
99
+ def read_yaml(base, name)
100
+ super(base, name)
101
+ self.extracted_excerpt = self.extract_excerpt
102
+ end
103
+
104
+ # The post excerpt. This is either a custom excerpt
105
+ # set in YAML front matter or the result of extract_excerpt.
106
+ #
107
+ # Returns excerpt string.
108
+ def excerpt
109
+ if self.data.has_key? 'excerpt'
110
+ self.data['excerpt']
111
+ else
112
+ self.extracted_excerpt
113
+ end
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
+ self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' ')
121
+ end
122
+
123
+ # Public: the path to the post relative to the site source,
124
+ # from the YAML Front-Matter or from a combination of
125
+ # the directory it's in, "_posts", and the name of the
126
+ # post file
127
+ #
128
+ # Returns the path to the file relative to the site source
129
+ def path
130
+ self.data['path'] || File.join(@dir, '_posts', @name).sub(/\A\//, '')
131
+ end
132
+
133
+ # Compares Post objects. First compares the Post date. If the dates are
134
+ # equal, it compares the Post slugs.
135
+ #
136
+ # other - The other Post we are comparing to.
137
+ #
138
+ # Returns -1, 0, 1
139
+ def <=>(other)
140
+ cmp = self.date <=> other.date
141
+ if 0 == cmp
142
+ cmp = self.slug <=> other.slug
143
+ end
144
+ return cmp
145
+ end
146
+
147
+ # Extract information from the post filename.
148
+ #
149
+ # name - The String filename of the post file.
150
+ #
151
+ # Returns nothing.
152
+ def process(name)
153
+ m, cats, date, slug, ext = *name.match(MATCHER)
154
+ self.date = Time.parse(date)
155
+ self.slug = slug
156
+ self.ext = ext
157
+ rescue ArgumentError
158
+ raise FatalException.new("Post #{name} does not have a valid date.")
159
+ end
160
+
161
+ # Transform the contents and excerpt based on the content type.
162
+ #
163
+ # Returns nothing.
164
+ def transform
165
+ super
166
+ self.extracted_excerpt = converter.convert(self.extracted_excerpt)
167
+ end
168
+
169
+ # The generated directory into which the post will be placed
170
+ # upon generation. This is derived from the permalink or, if
171
+ # permalink is absent, set to the default date
172
+ # e.g. "/2008/11/05/" if the permalink style is :date, otherwise nothing.
173
+ #
174
+ # Returns the String directory.
175
+ def dir
176
+ File.dirname(url)
177
+ end
178
+
179
+ # The full path and filename of the post. Defined in the YAML of the post
180
+ # body (optional).
181
+ #
182
+ # Returns the String permalink.
183
+ def permalink
184
+ self.data && self.data['permalink']
185
+ end
186
+
187
+ def template
188
+ case self.site.permalink_style
189
+ when :pretty
190
+ "/:categories/:year/:month/:day/:title/"
191
+ when :none
192
+ "/:categories/:title.html"
193
+ when :date
194
+ "/:categories/:year/:month/:day/:title.html"
195
+ when :ordinal
196
+ "/:categories/:year/:y_day/:title.html"
197
+ else
198
+ self.site.permalink_style.to_s
199
+ end
200
+ end
201
+
202
+ # The generated relative url of this post.
203
+ # e.g. /2008/11/05/my-awesome-post.html
204
+ #
205
+ # Returns the String URL.
206
+ def url
207
+ return @url if @url
208
+
209
+ url = if permalink
210
+ permalink
211
+ else
212
+ {
213
+ "year" => date.strftime("%Y"),
214
+ "month" => date.strftime("%m"),
215
+ "day" => date.strftime("%d"),
216
+ "title" => CGI.escape(slug),
217
+ "i_day" => date.strftime("%d").to_i.to_s,
218
+ "i_month" => date.strftime("%m").to_i.to_s,
219
+ "categories" => categories.map { |c| URI.escape(c.to_s) }.join('/'),
220
+ "short_month" => date.strftime("%b"),
221
+ "y_day" => date.strftime("%j"),
222
+ "output_ext" => self.output_ext
223
+ }.inject(template) { |result, token|
224
+ result.gsub(/:#{Regexp.escape token.first}/, token.last)
225
+ }.gsub(/\/\//, "/")
226
+ end
227
+
228
+ # sanitize url
229
+ @url = url.split('/').reject{ |part| part =~ /^\.+$/ }.join('/')
230
+ @url += "/" if url =~ /\/$/
231
+ @url
232
+ end
233
+
234
+ # The UID for this post (useful in feeds).
235
+ # e.g. /2008/11/05/my-awesome-post
236
+ #
237
+ # Returns the String UID.
238
+ def id
239
+ File.join(self.dir, self.slug)
240
+ end
241
+
242
+ # Calculate related posts.
243
+ #
244
+ # Returns an Array of related Posts.
245
+ def related_posts(posts)
246
+ return [] unless posts.size > 1
247
+
248
+ if self.site.lsi
249
+ build_index
250
+
251
+ related = self.class.lsi.find_related(self.content, 11)
252
+ related - [self]
253
+ else
254
+ (posts - [self])[0..9]
255
+ end
256
+ end
257
+
258
+ def build_index
259
+
260
+ # self.class.lsi ||= begin
261
+ # puts "Starting the classifier..."
262
+ # lsi = Classifier::LSI.new(:auto_rebuild => false)
263
+ # $stdout.print(" Populating LSI... "); $stdout.flush
264
+ # posts.each { |x| $stdout.print("."); $stdout.flush; lsi.add_item(x) }
265
+ # $stdout.print("\n Rebuilding LSI index... ")
266
+ # lsi.build_index
267
+ # puts ""
268
+ # lsi
269
+ # end
270
+ end
271
+
272
+ # Add any necessary layouts to this post.
273
+ #
274
+ # layouts - A Hash of {"name" => "layout"}.
275
+ # site_payload - The site payload hash.
276
+ #
277
+ # Returns nothing.
278
+ def render(layouts, site_payload)
279
+ # construct payload
280
+ payload = {
281
+ "site" => { "related_posts" => related_posts(site_payload["site"]["posts"]) },
282
+ "page" => self.to_liquid
283
+ }.deep_merge(site_payload)
284
+
285
+ do_layout(payload, layouts)
286
+ end
287
+
288
+ # Obtain destination path.
289
+ #
290
+ # dest - The String path to the destination dir.
291
+ #
292
+ # Returns destination file path String.
293
+ def destination(dest)
294
+ # The url needs to be unescaped in order to preserve the correct filename
295
+ path = File.join(dest, CGI.unescape(self.url))
296
+ path = File.join(path, "index.html") if template[/\.html$/].nil?
297
+ path
298
+ end
299
+
300
+ # Convert this post into a Hash for use in Liquid templates.
301
+ #
302
+ # Returns the representative Hash.
303
+ def to_liquid
304
+ further_data = Hash[ATTRIBUTES_FOR_LIQUID.map { |attribute|
305
+ [attribute, send(attribute)]
306
+ }]
307
+ data.deep_merge(further_data)
308
+ end
309
+
310
+ # Returns the shorthand String identifier of this Post.
311
+ def inspect
312
+ "<Post: #{self.id}>"
313
+ end
314
+
315
+ def next
316
+ pos = self.site.posts.index(self)
317
+
318
+ if pos && pos < self.site.posts.length-1
319
+ self.site.posts[pos+1]
320
+ else
321
+ nil
322
+ end
323
+ end
324
+
325
+ def previous
326
+ pos = self.site.posts.index(self)
327
+ if pos && pos > 0
328
+ self.site.posts[pos-1]
329
+ else
330
+ nil
331
+ end
332
+ end
333
+
334
+ protected
335
+
336
+ # Internal: Extract excerpt from the content
337
+ #
338
+ # By default excerpt is your first paragraph of a post: everything before
339
+ # the first two new lines:
340
+ #
341
+ # ---
342
+ # title: Example
343
+ # ---
344
+ #
345
+ # First paragraph with [link][1].
346
+ #
347
+ # Second paragraph.
348
+ #
349
+ # [1]: http://example.com/
350
+ #
351
+ # This is fairly good option for Markdown and Textile files. But might cause
352
+ # problems for HTML posts (which is quite unusual for Moft). If default
353
+ # excerpt delimiter is not good for you, you might want to set your own via
354
+ # configuration option `excerpt_separator`. For example, following is a good
355
+ # alternative for HTML posts:
356
+ #
357
+ # # file: _config.yml
358
+ # excerpt_separator: "<!-- more -->"
359
+ #
360
+ # Notice that all markdown-style link references will be appended to the
361
+ # excerpt. So the example post above will have this excerpt source:
362
+ #
363
+ # First paragraph with [link][1].
364
+ #
365
+ # [1]: http://example.com/
366
+ #
367
+ # Excerpts are rendered same time as content is rendered.
368
+ #
369
+ # Returns excerpt String
370
+ def extract_excerpt
371
+ separator = self.site.config['excerpt_separator']
372
+ head, _, tail = self.content.partition(separator)
373
+
374
+ "" << head << "\n\n" << tail.scan(/^\[[^\]]+\]:.+$/).join("\n")
375
+ end
376
+ end
377
+ end