oaktree 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,145 @@
1
+ require 'date'
2
+
3
+ class OakTree
4
+
5
+ # Specifications for the blog, operates similar to Gem::Specification.
6
+ # URLs and paths are strings and should not end in a slash.
7
+ class Specification
8
+ @@KEY_VALUE_PATTERN = /^\s*(?<key>[\w_]+)\s*:\s*(?<value>.*?)\s*(#|$)/
9
+ @@DEFAULT_DATE_PATH_FORMAT = '%Y/%m/'
10
+ @@DEFAULT_SLUG_SEPARATOR = '_'
11
+
12
+ def self.default_date_path_format
13
+ @@DEFAULT_DATE_PATH_FORMAT
14
+ end
15
+
16
+ def self.default_slug_separator
17
+ @@DEFAULT_SLUG_SEPARATOR
18
+ end
19
+
20
+ # The blog's title
21
+ attr_accessor :title
22
+ # A description of the blog
23
+ attr_accessor :description
24
+ # The blog root, where files are stored locally.
25
+ # Beneath this directory, there should be /source and /public directories,
26
+ # where post sources and the blog output, respectively, are stored. If these
27
+ # don't exist, they'll be created when generating the blog.
28
+ # This cannot be changed.
29
+ attr_reader :blog_root
30
+ # The name of the blog's author (currently assumes a single author)
31
+ attr_accessor :author
32
+ # The number of posts displayed per page
33
+ attr_accessor :posts_per_page
34
+ # Whether the timeline is reversed
35
+ attr_accessor :reversed
36
+ # The date format for post paths
37
+ attr_accessor :date_path_format
38
+ # The separator for words in slugs. May not be whitespace if loaded from a
39
+ # blog_spec file.
40
+ attr_accessor :slug_separator
41
+ # The length of the RSS feed in number of posts. Defaults to 20-most-recent.
42
+ attr_accessor :rss_length
43
+
44
+ # Sets the post path (i.e., the subdirectory where posts are stored).
45
+ # Should not begin with a slash, but can have a trailing slash if you want.
46
+ # If there is no trailing slash, it will be part of the filename up to a
47
+ # a point.
48
+ def post_path= path
49
+ raise "post_path provided is nil" if path.nil?
50
+ raise "post_path provided is not a string" unless path.kind_of?(String)
51
+
52
+ @post_path = path.clone().freeze()
53
+ end
54
+
55
+ # Gets the post path (i.e., the subdirectory where posts are stored).
56
+ def post_path
57
+ @post_path
58
+ end
59
+
60
+ def date_path_format= format
61
+ if format.empty?
62
+ @date_path_format = self.class.default_date_path_format
63
+ else
64
+ @date_path_format = format
65
+ end
66
+ end
67
+
68
+ # Sets the base URL of the blog (i.e., http://localhost/blog) - should have
69
+ # a trailing slash.
70
+ def base_url= url
71
+ url = String.new(url)
72
+ url << '/' unless url.end_with? '/'
73
+ @base_url = url.freeze()
74
+ end
75
+
76
+ # Gets the base URL of the blog (i.e., http://localhost/blog).
77
+ def base_url
78
+ @base_url
79
+ end
80
+
81
+ def sources_root
82
+ @sources_root ||= "#{@blog_root}source/"
83
+ end
84
+
85
+ # Loads a specification from a file.
86
+ def self.from_file(path)
87
+ raise "Spec file does not exist" unless File.exists? path
88
+
89
+ self.new { |spec|
90
+
91
+ spec_contents = File.open(path, 'r') { |io| io.read }
92
+ spec_hash = Psych.load(spec_contents)
93
+ spec_hash.each {
94
+ |key, value|
95
+ setter_sym = :"#{key}="
96
+ if spec.respond_to? setter_sym
97
+ spec.send setter_sym, value
98
+ else
99
+ raise "Invalid key/value in spec: #{key} => #{value}"
100
+ end
101
+ }
102
+
103
+ Dir.chdir(File.dirname(path))
104
+
105
+ }
106
+ end
107
+
108
+ # Initializes the Specification with its default values.
109
+ def initialize
110
+ # initialize default values for most properties
111
+ self.title = ''
112
+ self.description = ''
113
+ self.base_url = ''
114
+ self.post_path = 'post/'
115
+ self.author = ''
116
+ self.posts_per_page = 10
117
+ self.reversed = false
118
+ self.date_path_format = self.class.default_date_path_format
119
+ self.slug_separator = self.class.default_slug_separator
120
+ self.rss_length = 20
121
+
122
+ yield self if block_given?
123
+
124
+ @blog_root = File.expand_path(Dir.getwd) + '/'
125
+ end
126
+
127
+ def export_string
128
+ <<-EOT
129
+ # metadata
130
+ title: #{@title}
131
+ description: #{@description}
132
+ author: #{@author}
133
+ posts_per_page: 10
134
+
135
+ # public URL
136
+ base_url: #{@base_url}
137
+
138
+ # public content paths
139
+ post_path: #{@post_path}
140
+ EOT
141
+ end
142
+
143
+ end # Specification
144
+
145
+ end # OakTree
@@ -0,0 +1,26 @@
1
+ require 'date'
2
+ require 'mustache'
3
+ require 'oaktree/template'
4
+
5
+ class OakTree
6
+
7
+ module Template
8
+
9
+ class Base < Mustache
10
+ DEFAULT_DATETIME_FORMAT = '%-d %B %Y @ %l:%M %p'
11
+
12
+ def proc_for_datetime time
13
+ proc { |format|
14
+ format ||= DEFAULT_DATETIME_FORMAT
15
+
16
+ render(time.strftime(format))
17
+ }
18
+ end
19
+
20
+ self.template_path = 'template'
21
+
22
+ end # Base
23
+
24
+ end # Template
25
+
26
+ end # OakTree
@@ -0,0 +1,340 @@
1
+ require 'mustache'
2
+ require 'date'
3
+ require 'oaktree/template/base'
4
+ require 'oaktree/template/post'
5
+ require 'oaktree/template/post_archive'
6
+ require 'uri'
7
+
8
+ class OakTree
9
+
10
+ module Template
11
+
12
+ class Blog < Base
13
+
14
+ @@MODES = [:home, :archive, :single, :statics, :rss_feed].freeze()
15
+ @@MODE_TEMPLATES = {
16
+ :home => 'home'.freeze(),
17
+ :archive => 'archive'.freeze(),
18
+ :single => 'single'.freeze(),
19
+ :statics => 'statics'.freeze(),
20
+ :rss_feed => 'rss_feed'.freeze()
21
+ }
22
+
23
+ def self.modes
24
+ @@MODES
25
+ end
26
+
27
+ # tag to provide simple URL encoding
28
+ def url_encode
29
+ proc {
30
+ |input|
31
+ render(URI.encode_www_form_component(render(input)))
32
+ }
33
+ end
34
+
35
+ # returns an enumerator for all modes supported by the template
36
+ def modes
37
+ self.class.modes
38
+ end
39
+
40
+ # the default template file
41
+ self.template_name = 'blog'
42
+
43
+ def self.template_for_mode(mode)
44
+ tmp = @@MODE_TEMPLATES[mode]
45
+ tpath = File.expand_path("#{self.template_path}/#{tmp}.mustache")
46
+ (tmp && File.exists?(tpath)) ? tmp : self.template_name
47
+ end
48
+
49
+ def initialize tree, options = {}
50
+ @tree = tree
51
+ @spec = tree.blogspec
52
+ @page_index = 0
53
+ self.mode = options[:mode] || :home
54
+ raise "Invalid mode" unless @@MODES.include? @mode
55
+
56
+ @postdata = tree.posts.map { |post|
57
+ Post.new(@spec, post)
58
+ }.select(&:published?)
59
+
60
+ @posts = @postdata.select(&:post?)
61
+ @sorted_posts = @posts.sort_by { |p| p.post_data.time }.reverse!
62
+ @statics = @postdata.select(&:static?)
63
+
64
+ @posts.freeze
65
+ @statics.freeze
66
+
67
+ # build the archive
68
+ @archive = []
69
+ @posts.map { |post|
70
+ data = post.post_data
71
+ time = data.time
72
+
73
+ arch = @archive.last
74
+ if arch.nil? || arch.year != time.year || arch.month != time.month
75
+ arch = PostArchive.new(time.year, time.month, [], @spec, self) { |a|
76
+ a.next_archive = arch
77
+ arch.previous_archive = a unless arch.nil?
78
+ }
79
+ @archive << arch
80
+ end
81
+
82
+ arch.posts << post
83
+ }
84
+ @archive.freeze
85
+
86
+ page = options[:page]
87
+ page = page.nil? ? 0 : page - 1
88
+ @page_index = page
89
+ end
90
+
91
+ def mode= mode
92
+ raise "Invalid mode" unless @@MODES.include? mode
93
+ return mode if @mode == mode
94
+ @mode = mode
95
+ self.template_name = self.class.template_for_mode(mode)
96
+ end
97
+
98
+ def mode
99
+ @mode
100
+ end
101
+
102
+ ### blog tags
103
+
104
+ def local_path
105
+ path = @spec.blog_root + "public/"
106
+ date_format = @spec.date_path_format
107
+
108
+ if home? && @page_index == 0
109
+ path << "index.html"
110
+ elsif single? || statics?
111
+ return post.post_data.public_path
112
+ elsif rss_feed?
113
+ path << 'feeds/rss.xml'
114
+ else
115
+ path << @spec.post_path
116
+
117
+ case mode
118
+ when :home
119
+ path << "#{@page_index}.html"
120
+ when :archive
121
+ arch = @archive[@page_index]
122
+ archdate = DateTime.new(arch.year, arch.month, 1)
123
+ path << "#{archdate.strftime(date_format)}"
124
+ end
125
+
126
+ path << 'index.html' if path.end_with? '/'
127
+ end
128
+
129
+ path
130
+ end
131
+
132
+ def blog_title
133
+ @spec.title
134
+ end
135
+
136
+ def blog_author
137
+ @spec.author
138
+ end
139
+
140
+ def blog_description
141
+ @spec.description
142
+ end
143
+
144
+ def blog_url
145
+ @spec.base_url
146
+ end
147
+
148
+ ### page tags
149
+
150
+ def archive?
151
+ @mode == :archive
152
+ end
153
+
154
+ def single?
155
+ @mode == :single
156
+ end
157
+
158
+ def home?
159
+ @mode == :home
160
+ end
161
+
162
+ def statics?
163
+ @mode == :statics
164
+ end
165
+
166
+ def rss_feed?
167
+ @mode == :rss_feed
168
+ end
169
+
170
+ def rss_feed_url
171
+ "#{@spec.base_url}feeds/rss.xml"
172
+ end
173
+
174
+ # uses the input as a format string for the first day of the month and year
175
+ # of the archive page.
176
+ # this returns nil if not in archive mode.
177
+ def archive_date
178
+ return nil unless archive?
179
+ proc_for_datetime(@archive[@page_index])
180
+ end
181
+
182
+ def statics
183
+ @statics
184
+ end
185
+
186
+ # returns the number of pages
187
+ def pages
188
+ case mode
189
+ # you can render multiple home pages, but it's not something I recommend
190
+ # since re-syncing all homepage posts is a nightmare at some point
191
+ when :home
192
+ if @spec.posts_per_page > 0
193
+ (@posts.length / @spec.posts_per_page) + 1
194
+ else
195
+ 1
196
+ end
197
+
198
+ when :archive ; @archive.length
199
+ when :single ; @posts.length
200
+ when :statics ; @statics.length
201
+ when :rss_feed ; 1
202
+ end
203
+ end
204
+
205
+ # returns the current page number
206
+ def page
207
+ @page_index + 1
208
+ end
209
+
210
+ def page= page_num
211
+ @next_url_cache = nil
212
+ @prev_url_cache = nil
213
+ @posts_cache = nil
214
+ @page_index = (page_num - 1)
215
+ end
216
+
217
+ def paged?
218
+ has_previous? || has_next?
219
+ end
220
+
221
+ # determines whether there's a previous page. previous also means older.
222
+ def has_previous?
223
+ self.page < self.pages
224
+ end
225
+
226
+ # determines whether there is a next page. next also means newer.
227
+ def has_next?
228
+ 1 < self.page
229
+ end
230
+
231
+ def next_url
232
+ return "" unless has_next?
233
+
234
+ @next_url_cache ||= case mode
235
+ when :home
236
+ if @page_index == 1
237
+ blog_url
238
+ else
239
+ blog_url + @spec.post_path + "#{@page_index - 1}.html"
240
+ end
241
+ when :archive ; @archive[@page_index - 1].permalink
242
+ when :single ; @posts[@page_index - 1].permalink
243
+ end
244
+ end
245
+
246
+ def previous_url
247
+ return "" unless has_previous?
248
+
249
+ @prev_url_cache ||= case mode
250
+ when :home ; blog_url + @spec.post_path + "#{@page_index + 1}.html"
251
+ when :archive ; @archive[@page_index + 1].permalink
252
+ when :single ; @posts[@page_index + 1].permalink
253
+ end
254
+ end
255
+
256
+ ### archive tags
257
+
258
+ def archives
259
+ @archive
260
+ end
261
+
262
+ # the current archive
263
+ def archive
264
+ @archive[@page_index] if archive?
265
+ end
266
+
267
+ ### post tags
268
+
269
+ # returns all visible posts
270
+ # note: visible means whatever is in the current page
271
+ def posts
272
+ @posts_cache ||= case mode
273
+ when :home
274
+ if @spec.posts_per_page > 0
275
+ page_start = @page_index * @spec.posts_per_page
276
+ page_end = page_start + @spec.posts_per_page - 1
277
+ if @posts.length < page_end
278
+ page_end = @posts.length
279
+ end
280
+
281
+ return [] unless page_start < @posts.length
282
+
283
+ @posts[page_start .. page_end]
284
+ else
285
+ @posts[0..-1]
286
+ end
287
+
288
+ when :archive ; @archive[@page_index].posts
289
+ when :single ; [@posts[@page_index]]
290
+ when :statics ; [@statics[@page_index]]
291
+
292
+ when :rss_feed
293
+ rss_length = @spec.rss_length - 1
294
+ rss_length = -1 if rss_length < -1
295
+ @sorted_posts[0..(rss_length - 1)]
296
+ end
297
+ end
298
+
299
+ # if there's only one post being displayed, this returns its template.
300
+ # only works in single mode.
301
+ def post
302
+ case mode
303
+ when :single ; @posts[@page_index]
304
+ when :statics ; @statics[@page_index]
305
+ end
306
+ end
307
+
308
+ # returns the next post (you generally shouldn't use this except to get
309
+ # some small bit of info about the next post).
310
+ # only works in single mode.
311
+ def next_post
312
+ single? && has_next? ? @posts[page_index - 1] : nil
313
+ end
314
+
315
+ # returns the previous post.
316
+ # only works in single mode.
317
+ def previous_post
318
+ single? && has_previous? ? @posts[page_index + 1] : nil
319
+ end
320
+
321
+ def next_archive
322
+ archive? && has_next? ? @archive[page_index - 1] : nil
323
+ end
324
+
325
+ def previous_archive
326
+ archive? && has_previous? ? @archive[page_index + 1] : nil
327
+ end
328
+
329
+ ### general tags
330
+
331
+ # treats the input text as a format string for today's date and time
332
+ def today
333
+ proc_for_datetime(DateTime.now)
334
+ end
335
+
336
+ end # Blog
337
+
338
+ end # Template
339
+
340
+ end # OakTree
@@ -0,0 +1,91 @@
1
+ require 'kramdown'
2
+ require 'oaktree/template'
3
+ require 'oaktree/kramdown/oak_html'
4
+
5
+ class OakTree
6
+
7
+ module Template
8
+
9
+ class Post < Base
10
+
11
+ self.template_name = 'post'
12
+
13
+ def initialize spec, post
14
+ @post = post
15
+ @content = nil
16
+ @spec = spec
17
+ document = ::Kramdown::Document.new(@post.content)
18
+ @content, warnings = ::OakTree::Kramdown::OakHtml.convert(document.root, :auto_id_prefix => @post.time.strftime('%Y_%m_%d_'))
19
+ puts warnings unless warnings.empty?
20
+ end
21
+
22
+ def post_data
23
+ @post
24
+ end
25
+
26
+ def title
27
+ @post.title
28
+ end
29
+
30
+ def url
31
+ if source_link?
32
+ source_link
33
+ else
34
+ permalink
35
+ end
36
+ end
37
+
38
+ def permalink
39
+ @post.permalink
40
+ end
41
+
42
+ def source_link
43
+ @post.link
44
+ end
45
+
46
+ def source_link?
47
+ plink = @post.link
48
+ ! (plink.nil? || plink.empty?)
49
+ end
50
+
51
+ def static?
52
+ @post.kind == :static
53
+ end
54
+
55
+ def post?
56
+ @post.kind == :post
57
+ end
58
+
59
+ def content
60
+ @content
61
+ end
62
+
63
+ def time
64
+ proc_for_datetime(@post.time)
65
+ end
66
+
67
+ def public_path
68
+ @post.public_path
69
+ end
70
+
71
+ def slug
72
+ @post.slug
73
+ end
74
+
75
+ def published?
76
+ @post.status == :published
77
+ end
78
+
79
+ def unpublished?
80
+ ! published?
81
+ end
82
+
83
+ def status
84
+ @post.status
85
+ end
86
+
87
+ end # Post
88
+
89
+ end # Template
90
+
91
+ end # OakTree
@@ -0,0 +1,53 @@
1
+ require 'oaktree/template/base'
2
+ require 'mustache'
3
+ require 'date'
4
+
5
+ class OakTree
6
+
7
+ module Template
8
+
9
+ class PostArchive < Base
10
+ self.template_name = "postarchive"
11
+
12
+ attr_accessor :year
13
+ attr_accessor :month
14
+ attr_accessor :posts
15
+ # the next most recent archive
16
+ attr_accessor :next_archive
17
+ # the next oldest archive
18
+ attr_accessor :previous_archive
19
+
20
+ def initialize year=1, month=1, posts=[], spec, blog
21
+ @year = year
22
+ @month = month
23
+ @posts = posts
24
+ @spec = spec
25
+ @blog = blog
26
+ @datetime = DateTime.new(year, month, 1)
27
+ formatted_date = @datetime.strftime(@spec.date_path_format)
28
+ @permalink = "#{@spec.base_url}#{@spec.post_path}#{formatted_date}"
29
+
30
+ yield self if block_given?
31
+ end
32
+
33
+ def date
34
+ proc_for_datetime(@datetime)
35
+ end
36
+
37
+ def datetime
38
+ @datetime
39
+ end
40
+
41
+ def permalink
42
+ @permalink
43
+ end
44
+
45
+ def open?
46
+ self == @blog.archive
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,19 @@
1
+ require 'mustache'
2
+ require 'date'
3
+
4
+ class OakTree
5
+
6
+ module Template
7
+
8
+ # Probably shouldn't do this, but it simplifies life.
9
+ Mustache.template_path = 'template'
10
+
11
+ autoload :Base, 'oaktree/template/base'
12
+ autoload :PostArchive, 'oaktree/template/post_archive'
13
+ autoload :Post, 'oaktree/template/post'
14
+ autoload :Blog, 'oaktree/template/blog'
15
+
16
+ end # Template
17
+
18
+ end # OakTree
19
+