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.
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/bin/moft +83 -0
- data/lib/moft.rb +89 -0
- data/lib/moft/command.rb +27 -0
- data/lib/moft/commands/build.rb +64 -0
- data/lib/moft/commands/new.rb +50 -0
- data/lib/moft/commands/serve.rb +33 -0
- data/lib/moft/configuration.rb +171 -0
- data/lib/moft/converter.rb +48 -0
- data/lib/moft/converters/identity.rb +21 -0
- data/lib/moft/converters/markdown.rb +43 -0
- data/lib/moft/converters/markdown/kramdown_parser.rb +44 -0
- data/lib/moft/converters/markdown/maruku_parser.rb +47 -0
- data/lib/moft/converters/markdown/rdiscount_parser.rb +26 -0
- data/lib/moft/converters/markdown/redcarpet_parser.rb +40 -0
- data/lib/moft/converters/textile.rb +50 -0
- data/lib/moft/convertible.rb +152 -0
- data/lib/moft/core_ext.rb +68 -0
- data/lib/moft/deprecator.rb +34 -0
- data/lib/moft/draft.rb +35 -0
- data/lib/moft/errors.rb +4 -0
- data/lib/moft/filters.rb +141 -0
- data/lib/moft/generator.rb +4 -0
- data/lib/moft/generators/pagination.rb +131 -0
- data/lib/moft/layout.rb +42 -0
- data/lib/moft/logger.rb +52 -0
- data/lib/moft/mime.types +85 -0
- data/lib/moft/page.rb +147 -0
- data/lib/moft/plugin.rb +75 -0
- data/lib/moft/post.rb +377 -0
- data/lib/moft/site.rb +422 -0
- data/lib/moft/static_file.rb +70 -0
- data/lib/moft/tags/gist.rb +30 -0
- data/lib/moft/tags/highlight.rb +83 -0
- data/lib/moft/tags/include.rb +37 -0
- data/lib/moft/tags/post_url.rb +46 -0
- data/lib/site_template/_config.yml +1 -0
- data/lib/site_template/_layouts/default.html +38 -0
- data/lib/site_template/_layouts/post.html +6 -0
- data/lib/site_template/_posts/0000-00-00-welcome-to-jekyll.markdown.erb +24 -0
- data/lib/site_template/css/screen.css +189 -0
- data/lib/site_template/css/syntax.css +60 -0
- data/lib/site_template/images/.gitkeep +0 -0
- data/lib/site_template/images/rss.png +0 -0
- data/lib/site_template/index.html +13 -0
- data/moft.gemspec +100 -0
- metadata +412 -0
data/lib/moft/logger.rb
ADDED
@@ -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
|
data/lib/moft/mime.types
ADDED
@@ -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
|
data/lib/moft/page.rb
ADDED
@@ -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
|
data/lib/moft/plugin.rb
ADDED
@@ -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
|
data/lib/moft/post.rb
ADDED
@@ -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
|