monad 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. data/CONTRIBUTING.md +68 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE +21 -0
  4. data/README.md +88 -0
  5. data/Rakefile +136 -0
  6. data/bin/monad +102 -0
  7. data/cucumber.yml +3 -0
  8. data/features/create_sites.feature +112 -0
  9. data/features/data_sources.feature +76 -0
  10. data/features/drafts.feature +25 -0
  11. data/features/embed_filters.feature +60 -0
  12. data/features/markdown.feature +30 -0
  13. data/features/pagination.feature +54 -0
  14. data/features/permalinks.feature +65 -0
  15. data/features/post_data.feature +214 -0
  16. data/features/site_configuration.feature +206 -0
  17. data/features/site_data.feature +101 -0
  18. data/features/step_definitions/monad_steps.rb +175 -0
  19. data/features/support/env.rb +25 -0
  20. data/lib/monad.rb +90 -0
  21. data/lib/monad/command.rb +27 -0
  22. data/lib/monad/commands/build.rb +64 -0
  23. data/lib/monad/commands/doctor.rb +29 -0
  24. data/lib/monad/commands/new.rb +50 -0
  25. data/lib/monad/commands/serve.rb +33 -0
  26. data/lib/monad/configuration.rb +183 -0
  27. data/lib/monad/converter.rb +48 -0
  28. data/lib/monad/converters/identity.rb +21 -0
  29. data/lib/monad/converters/markdown.rb +43 -0
  30. data/lib/monad/converters/markdown/kramdown_parser.rb +44 -0
  31. data/lib/monad/converters/markdown/maruku_parser.rb +47 -0
  32. data/lib/monad/converters/markdown/rdiscount_parser.rb +35 -0
  33. data/lib/monad/converters/markdown/redcarpet_parser.rb +70 -0
  34. data/lib/monad/converters/textile.rb +50 -0
  35. data/lib/monad/convertible.rb +152 -0
  36. data/lib/monad/core_ext.rb +68 -0
  37. data/lib/monad/deprecator.rb +32 -0
  38. data/lib/monad/draft.rb +35 -0
  39. data/lib/monad/drivers/json_driver.rb +39 -0
  40. data/lib/monad/drivers/yaml_driver.rb +23 -0
  41. data/lib/monad/errors.rb +4 -0
  42. data/lib/monad/filters.rb +154 -0
  43. data/lib/monad/generator.rb +4 -0
  44. data/lib/monad/generators/pagination.rb +143 -0
  45. data/lib/monad/layout.rb +42 -0
  46. data/lib/monad/logger.rb +54 -0
  47. data/lib/monad/mime.types +85 -0
  48. data/lib/monad/page.rb +163 -0
  49. data/lib/monad/plugin.rb +75 -0
  50. data/lib/monad/post.rb +377 -0
  51. data/lib/monad/site.rb +455 -0
  52. data/lib/monad/static_file.rb +70 -0
  53. data/lib/monad/tags/gist.rb +30 -0
  54. data/lib/monad/tags/highlight.rb +85 -0
  55. data/lib/monad/tags/include.rb +37 -0
  56. data/lib/monad/tags/post_url.rb +61 -0
  57. data/lib/site_template/.gitignore +1 -0
  58. data/lib/site_template/_config.yml +2 -0
  59. data/lib/site_template/_layouts/default.html +46 -0
  60. data/lib/site_template/_layouts/post.html +9 -0
  61. data/lib/site_template/_posts/0000-00-00-welcome-to-monad.markdown.erb +24 -0
  62. data/lib/site_template/css/main.css +165 -0
  63. data/lib/site_template/css/syntax.css +60 -0
  64. data/lib/site_template/index.html +13 -0
  65. data/monad.gemspec +197 -0
  66. data/script/bootstrap +2 -0
  67. data/test/fixtures/broken_front_matter1.erb +5 -0
  68. data/test/fixtures/broken_front_matter2.erb +4 -0
  69. data/test/fixtures/broken_front_matter3.erb +7 -0
  70. data/test/fixtures/exploit_front_matter.erb +4 -0
  71. data/test/fixtures/front_matter.erb +4 -0
  72. data/test/fixtures/members.yaml +7 -0
  73. data/test/helper.rb +62 -0
  74. data/test/source/.htaccess +8 -0
  75. data/test/source/_includes/sig.markdown +3 -0
  76. data/test/source/_layouts/default.html +27 -0
  77. data/test/source/_layouts/simple.html +1 -0
  78. data/test/source/_plugins/dummy.rb +8 -0
  79. data/test/source/_posts/2008-02-02-not-published.textile +8 -0
  80. data/test/source/_posts/2008-02-02-published.textile +8 -0
  81. data/test/source/_posts/2008-10-18-foo-bar.textile +8 -0
  82. data/test/source/_posts/2008-11-21-complex.textile +8 -0
  83. data/test/source/_posts/2008-12-03-permalinked-post.textile +9 -0
  84. data/test/source/_posts/2008-12-13-include.markdown +8 -0
  85. data/test/source/_posts/2009-01-27-array-categories.textile +10 -0
  86. data/test/source/_posts/2009-01-27-categories.textile +7 -0
  87. data/test/source/_posts/2009-01-27-category.textile +7 -0
  88. data/test/source/_posts/2009-01-27-empty-categories.textile +7 -0
  89. data/test/source/_posts/2009-01-27-empty-category.textile +7 -0
  90. data/test/source/_posts/2009-03-12-hash-#1.markdown +6 -0
  91. data/test/source/_posts/2009-05-18-empty-tag.textile +6 -0
  92. data/test/source/_posts/2009-05-18-empty-tags.textile +6 -0
  93. data/test/source/_posts/2009-05-18-tag.textile +6 -0
  94. data/test/source/_posts/2009-05-18-tags.textile +9 -0
  95. data/test/source/_posts/2009-06-22-empty-yaml.textile +3 -0
  96. data/test/source/_posts/2009-06-22-no-yaml.textile +1 -0
  97. data/test/source/_posts/2010-01-08-triple-dash.markdown +5 -0
  98. data/test/source/_posts/2010-01-09-date-override.textile +7 -0
  99. data/test/source/_posts/2010-01-09-time-override.textile +7 -0
  100. data/test/source/_posts/2010-01-09-timezone-override.textile +7 -0
  101. data/test/source/_posts/2010-01-16-override-data.textile +4 -0
  102. data/test/source/_posts/2011-04-12-md-extension.md +7 -0
  103. data/test/source/_posts/2011-04-12-text-extension.text +0 -0
  104. data/test/source/_posts/2013-01-02-post-excerpt.markdown +14 -0
  105. data/test/source/_posts/2013-01-12-nil-layout.textile +6 -0
  106. data/test/source/_posts/2013-01-12-no-layout.textile +5 -0
  107. data/test/source/_posts/2013-03-19-not-a-post.markdown/.gitkeep +0 -0
  108. data/test/source/_posts/2013-04-11-custom-excerpt.markdown +10 -0
  109. data/test/source/_posts/2013-05-10-number-category.textile +7 -0
  110. data/test/source/_posts/es/2008-11-21-nested.textile +8 -0
  111. data/test/source/about.html +6 -0
  112. data/test/source/category/_posts/2008-9-23-categories.textile +6 -0
  113. data/test/source/contacts.html +5 -0
  114. data/test/source/contacts/bar.html +5 -0
  115. data/test/source/contacts/index.html +5 -0
  116. data/test/source/css/screen.css +76 -0
  117. data/test/source/deal.with.dots.html +7 -0
  118. data/test/source/foo/_posts/bar/2008-12-12-topical-post.textile +8 -0
  119. data/test/source/index.html +22 -0
  120. data/test/source/sitemap.xml +32 -0
  121. data/test/source/symlink-test/symlinked-file +22 -0
  122. data/test/source/win/_posts/2009-05-24-yaml-linebreak.markdown +7 -0
  123. data/test/source/z_category/_posts/2008-9-23-categories.textile +6 -0
  124. data/test/suite.rb +11 -0
  125. data/test/test_command.rb +39 -0
  126. data/test/test_configuration.rb +137 -0
  127. data/test/test_convertible.rb +51 -0
  128. data/test/test_core_ext.rb +88 -0
  129. data/test/test_filters.rb +102 -0
  130. data/test/test_generated_site.rb +83 -0
  131. data/test/test_json_driver.rb +63 -0
  132. data/test/test_kramdown.rb +35 -0
  133. data/test/test_new_command.rb +104 -0
  134. data/test/test_page.rb +193 -0
  135. data/test/test_pager.rb +115 -0
  136. data/test/test_post.rb +573 -0
  137. data/test/test_rdiscount.rb +22 -0
  138. data/test/test_redcarpet.rb +61 -0
  139. data/test/test_redcloth.rb +86 -0
  140. data/test/test_site.rb +374 -0
  141. data/test/test_tags.rb +310 -0
  142. data/test/test_yaml_driver.rb +35 -0
  143. metadata +554 -0
@@ -0,0 +1,42 @@
1
+ module Monad
2
+ class Layout
3
+ include Convertible
4
+
5
+ # Gets the Site object.
6
+ attr_reader :site
7
+
8
+ # Gets/Sets the extension of this layout.
9
+ attr_accessor :ext
10
+
11
+ # Gets/Sets the Hash that holds the metadata for this layout.
12
+ attr_accessor :data
13
+
14
+ # Gets/Sets the content of this layout.
15
+ attr_accessor :content
16
+
17
+ # Initialize a new Layout.
18
+ #
19
+ # site - The Site.
20
+ # base - The String path to the source.
21
+ # name - The String filename of the post file.
22
+ def initialize(site, base, name)
23
+ @site = site
24
+ @base = base
25
+ @name = name
26
+
27
+ self.data = {}
28
+
29
+ self.process(name)
30
+ self.read_yaml(base, name)
31
+ end
32
+
33
+ # Extract information from the layout filename.
34
+ #
35
+ # name - The String filename of the layout file.
36
+ #
37
+ # Returns nothing.
38
+ def process(name)
39
+ self.ext = File.extname(name)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ require 'logger'
2
+
3
+ module Monad
4
+ class Logger < Logger
5
+ # Public: Print a monad message to stdout
6
+ #
7
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
8
+ # message - the message detail
9
+ #
10
+ # Returns nothing
11
+ def self.info(topic, message)
12
+ $stdout.puts message(topic, message)
13
+ end
14
+
15
+ # Public: Print a monad message to stderr
16
+ #
17
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
18
+ # message - the message detail
19
+ #
20
+ # Returns nothing
21
+ def self.warn(topic, message)
22
+ $stderr.puts message(topic, message).yellow
23
+ end
24
+
25
+ # Public: Print a monad error message to stderr
26
+ #
27
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
28
+ # message - the message detail
29
+ #
30
+ # Returns nothing
31
+ def self.error(topic, message)
32
+ $stderr.puts message(topic, message).red
33
+ end
34
+
35
+ # Public: Build a Monad topic method
36
+ #
37
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
38
+ # message - the message detail
39
+ #
40
+ # Returns the formatted message
41
+ def self.message(topic, message)
42
+ formatted_topic(topic) + message.gsub(/\s+/, ' ')
43
+ end
44
+
45
+ # Public: Format the topic
46
+ #
47
+ # topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
48
+ #
49
+ # Returns the formatted topic statement
50
+ def self.formatted_topic(topic)
51
+ "#{topic} ".rjust(20)
52
+ end
53
+ end
54
+ 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,163 @@
1
+ module Monad
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
+ if site.config['relative_permalinks']
68
+ File.join(@dir, permalink)
69
+ else
70
+ permalink
71
+ end
72
+ else
73
+ {
74
+ "path" => @dir,
75
+ "basename" => self.basename,
76
+ "output_ext" => self.output_ext,
77
+ }.inject(template) { |result, token|
78
+ result.gsub(/:#{token.first}/, token.last)
79
+ }.gsub(/\/\//, "/")
80
+ end
81
+
82
+ # sanitize url
83
+ @url = url.split('/').reject{ |part| part =~ /^\.+$/ }.join('/')
84
+ @url += "/" if url =~ /\/$/
85
+ @url.gsub!(/\A([^\/])/, '/\1')
86
+ @url
87
+ end
88
+
89
+ # Extract information from the page filename.
90
+ #
91
+ # name - The String filename of the page file.
92
+ #
93
+ # Returns nothing.
94
+ def process(name)
95
+ self.ext = File.extname(name)
96
+ self.basename = name[0 .. -self.ext.length-1]
97
+ end
98
+
99
+ # Add any necessary layouts to this post
100
+ #
101
+ # layouts - The Hash of {"name" => "layout"}.
102
+ # site_payload - The site payload Hash.
103
+ #
104
+ # Returns nothing.
105
+ def render(layouts, site_payload)
106
+ payload = {
107
+ "page" => self.to_liquid,
108
+ 'paginator' => pager.to_liquid
109
+ }.deep_merge(site_payload)
110
+
111
+ do_layout(payload, layouts)
112
+ end
113
+
114
+ # Convert this Page's data to a Hash suitable for use by Liquid.
115
+ #
116
+ # Returns the Hash representation of this Page.
117
+ def to_liquid
118
+ self.data.deep_merge({
119
+ "url" => self.url,
120
+ "content" => self.content,
121
+ "path" => self.data['path'] || path })
122
+ end
123
+
124
+ # The path to the source file
125
+ #
126
+ # Returns the path to the source file
127
+ def path
128
+ File.join(@dir, @name).sub(/\A\//, '')
129
+ end
130
+
131
+ # Obtain destination path.
132
+ #
133
+ # dest - The String path to the destination dir.
134
+ #
135
+ # Returns the destination file path String.
136
+ def destination(dest)
137
+ # The url needs to be unescaped in order to preserve the correct
138
+ # filename.
139
+ path = File.join(dest, CGI.unescape(self.url))
140
+ path = File.join(path, "index.html") if self.url =~ /\/$/
141
+ path
142
+ end
143
+
144
+ # Returns the object as a debug String.
145
+ def inspect
146
+ "#<Monad:Page @name=#{self.name.inspect}>"
147
+ end
148
+
149
+ # Returns the Boolean of whether this Page is HTML or not.
150
+ def html?
151
+ output_ext == '.html'
152
+ end
153
+
154
+ # Returns the Boolean of whether this Page is an index file or not.
155
+ def index?
156
+ basename == 'index'
157
+ end
158
+
159
+ def uses_relative_permalinks
160
+ permalink && @dir != "" && site.config['relative_permalinks']
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,75 @@
1
+ module Monad
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 Monad
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.to_s.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.gsub!(/\A([^\/])/, '/\1')
232
+ @url
233
+ end
234
+
235
+ # The UID for this post (useful in feeds).
236
+ # e.g. /2008/11/05/my-awesome-post
237
+ #
238
+ # Returns the String UID.
239
+ def id
240
+ File.join(self.dir, self.slug)
241
+ end
242
+
243
+ # Calculate related posts.
244
+ #
245
+ # Returns an Array of related Posts.
246
+ def related_posts(posts)
247
+ return [] unless posts.size > 1
248
+
249
+ if self.site.lsi
250
+ build_index
251
+
252
+ related = self.class.lsi.find_related(self.content, 11)
253
+ related - [self]
254
+ else
255
+ (posts - [self])[0..9]
256
+ end
257
+ end
258
+
259
+ def build_index
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
+ self.site.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 Monad). 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