oaktree 0.4.4

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.
@@ -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
+