oaktree 0.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/COPYING +6 -0
- data/README.md +46 -0
- data/Rakefile +21 -0
- data/bin/oak +285 -0
- data/lib/oaktree/kramdown/oak_html.rb +48 -0
- data/lib/oaktree/post_data.rb +236 -0
- data/lib/oaktree/specification.rb +145 -0
- data/lib/oaktree/template/base.rb +26 -0
- data/lib/oaktree/template/blog.rb +340 -0
- data/lib/oaktree/template/post.rb +91 -0
- data/lib/oaktree/template/post_archive.rb +53 -0
- data/lib/oaktree/template.rb +19 -0
- data/lib/oaktree.rb +144 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 75d3078e95fe1e754b7ec050599c359309ac9987
|
4
|
+
data.tar.gz: e8922bf74025c7677559e57e930c38d75ac4ac6f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5bc2963c2cf05e06f91b260463b8f744ed60e10046044098abd75ce52660cfe42ec806f9e2d18d3fe450b87c7ca6f2de048603e02fc1ce1409307f3a4a406fe3
|
7
|
+
data.tar.gz: ef0ececc42e093219f6442aeb1a2f5485f06c4e119b5d22336942ce00dcedc68df73da2acb203900fdd1da451461e50fbdc52b4697356eb4b533182ba6f0c44e
|
data/COPYING
ADDED
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# OakTree
|
2
|
+
|
3
|
+
## What in the nine hells is OakTree?
|
4
|
+
|
5
|
+
OakTree is an odd little static HTML blog gem thingy written by Noel R. Cower (hereinafter "yours truly" or just "I," because writing in the third person about my work is annoying). It is designed to fit my purposes - which is to say a very simple blog with very simple needs (has text, posts, some links, and my template). At present, it works moderately well at generating simple blogs – that is, HTML-only, no RSS feeds yet.
|
6
|
+
|
7
|
+
The entire interface for OakTree goes through its lone executable, `oak`. `oak` is intended to allow you to create posts, watch for changes, generate output HTML, and so on. If it's not obvious, this is all designed to be _very_ simple, because complex setups would be overkill.
|
8
|
+
|
9
|
+
## How do I get all up in this OakTree?
|
10
|
+
|
11
|
+
For one, I apologize that I've used the phrase "get all up in X." For two, the only way to use OakTree is to download this and build the gem and install it yourself. This is actually fairly simple, provided I didn't commit something which opened a portal to hell. You really just want to do something like this:
|
12
|
+
|
13
|
+
$ rake package
|
14
|
+
$ cd pkg
|
15
|
+
$ gem install oaktree-<version>.gem
|
16
|
+
... or, more likely ...
|
17
|
+
$ sudo gem install oaktree-<version>.gem
|
18
|
+
|
19
|
+
And, if you use rbenv, run `rbenv rehash` afterward, of course. I don't know what to tell you about RVM (aside from it being the destroyer of worlds, but that's another issue entirely), but you can figure it out. It's probably got a manpage or something.
|
20
|
+
|
21
|
+
After that, hop into some empty directory and run `oak init` to start a blog. At this point, you can being creating posts by either a) creating your own files in the _source/_ directory or using `oak newpost [title]` (where `[title]` is the title of your post). If you want to customize the blog's template, which I imagine you will, head on into _template/_ and begin tweaking _blog.mustache_. You will probably want to break things up into smaller template pieces to avoid getting bogged down with figuring out what's where in the mess of mustaches. For documentation on all available template tags, read _tags.md_. Finally, you'll want to configure your _blog_spec_ file. See _specs.md_ for info what options are available there, or just read _specification.rb_.
|
22
|
+
|
23
|
+
Once you've got all those things sorted out and you want to generate a blog, run `oak sync` in the same directory as your _blog_spec_ (I might do something git-ier later for that). This will build the HTML for your blog under _public/_. If you make further changes to the source files, you can run `oak sync` again and it will rebuild only files that need to be rebuilt (hopefully). Should you make changes to the template, you'll need to run `oak rebuild` to rebuild the entire blog (or delete the contents of _public/_), as `oak sync` does not account for template changes (it's a lot harder to figure out template dependencies accurately and doing it poorly seems like a bad idea, so I don't do it at all).
|
24
|
+
|
25
|
+
You may now commence uploading or using rsync or what have you to shunt your blog up somewhere. This may also be compatible with GitHub pages, but I'm not sure. Give it a shot.
|
26
|
+
|
27
|
+
## What kind of license is OakTree under?
|
28
|
+
|
29
|
+
How kind of you to ask. OakTree is licensed under the WTFPL-2:
|
30
|
+
|
31
|
+
Copyright (C) 2012 Noel Cower <ncower@gmail.com>
|
32
|
+
|
33
|
+
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
34
|
+
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
35
|
+
|
36
|
+
0. You just DO WHAT THE FUCK YOU WANT TO.
|
37
|
+
|
38
|
+
If you have any questions about licensing, please direct them to the incinerator, because the license (also found in the _COPYING_ file that should have accompanied this repository) already answered those questions.
|
39
|
+
|
40
|
+
## Anything else I should know?
|
41
|
+
|
42
|
+
No, but I'll reiterate this point again: **OakTree is unfinished, very unstable, and very much a work in progress.** If you attempt to make use of it, there's a good chance I cannot do anything to make your life better or help you, because you've already sealed your fate.
|
43
|
+
|
44
|
+
## Why is it called 'OakTree?'
|
45
|
+
|
46
|
+
Because it's easy to type and `oak` is even easier to type. There is a possibility that the entire project will just be renamed to 'oak' just to make me happy and remove four characters that I would otherwise have to type.
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rubygems/package_task'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/clean'
|
4
|
+
|
5
|
+
spec = Gem::Specification.load 'oaktree.gemspec'
|
6
|
+
|
7
|
+
desc 'Run tests'
|
8
|
+
Rake::TestTask.new { |task|
|
9
|
+
task.libs << 'test'
|
10
|
+
files = FileList['test/**/test_*.rb'].to_a
|
11
|
+
task.test_files = files
|
12
|
+
}
|
13
|
+
|
14
|
+
desc "Build #{spec.name} #{spec.version.to_s}"
|
15
|
+
Gem::PackageTask.new(spec) { |task|
|
16
|
+
task.need_zip = true
|
17
|
+
}
|
18
|
+
|
19
|
+
CLEAN.include 'pkg'
|
20
|
+
|
21
|
+
task :default => :test
|
data/bin/oak
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'oaktree'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
# Constants
|
7
|
+
|
8
|
+
APP_NAME = 'oak'
|
9
|
+
|
10
|
+
GEM_NAME = 'oaktree'
|
11
|
+
|
12
|
+
HELP_TEXT = <<EOH
|
13
|
+
#{APP_NAME} <command> ...
|
14
|
+
|
15
|
+
COMMANDS
|
16
|
+
* -n, newpost [title]
|
17
|
+
Creates a new post. If no title is given, the post's title is "Untitled".
|
18
|
+
The new post is placed in the blog's source/ directory
|
19
|
+
|
20
|
+
* -i, init [dir]
|
21
|
+
Creates a 'blog_spec' file and required directories in the given directory
|
22
|
+
if blog_spec doesn't already exist. If no directory is provided, oak uses
|
23
|
+
the working directory.
|
24
|
+
|
25
|
+
* -s, sync
|
26
|
+
Builds HTML files for changed posts. This does not rebuild files following
|
27
|
+
template changes. If you've made changes to templates, you should use the
|
28
|
+
rebuild command.
|
29
|
+
|
30
|
+
* -r, rebuild
|
31
|
+
Builds HTML files for all posts, regardless of whether they've been changed.
|
32
|
+
|
33
|
+
* -v, version
|
34
|
+
Shows the version of oak and the oaktree gem.
|
35
|
+
|
36
|
+
* -h, help
|
37
|
+
Shows this help text.
|
38
|
+
|
39
|
+
EOH
|
40
|
+
|
41
|
+
BLOG_TEMPLATE = <<EOT
|
42
|
+
<!DOCTYPE html>
|
43
|
+
<html lang="en">
|
44
|
+
<head>
|
45
|
+
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
46
|
+
<title>{{blog_title}}{{#post}} » {{title}}{{/post}}</title>
|
47
|
+
</head>
|
48
|
+
<body>
|
49
|
+
|
50
|
+
<div id="header">
|
51
|
+
<h1 id="blog-title"><a href="{{blog_url}}index.html">{{blog_title}}</a></h1>
|
52
|
+
<p>
|
53
|
+
<span class="blog-description">{{blog_description}}<br/></span>
|
54
|
+
<span class="blog-byline">By {{blog_author}}</span>
|
55
|
+
</p>
|
56
|
+
</div><!-- title -->
|
57
|
+
|
58
|
+
<div id="posts">
|
59
|
+
{{#posts}}
|
60
|
+
<div class="post">
|
61
|
+
<div class="post-header">
|
62
|
+
<h2 class="post-title">
|
63
|
+
<a href="{{url}}">{{#source_link?}}→ {{/source_link?}}{{title}}</a>
|
64
|
+
</h2>
|
65
|
+
<span class="post-time">{{#time}}%-d %B %Y{{/time}}</span>
|
66
|
+
</div><!-- post-header -->
|
67
|
+
<div class="post-body">
|
68
|
+
{{{content}}}
|
69
|
+
</div><!-- post-body -->
|
70
|
+
</div><!-- post: {{title}} -->
|
71
|
+
{{/posts}}
|
72
|
+
</div><!-- posts -->
|
73
|
+
|
74
|
+
{{#paged?}}
|
75
|
+
<div id="nav-bar">
|
76
|
+
{{#has_previous?}}<a href="{{previous_url}}">←
|
77
|
+
{{#archive}}{{#previous_archive}}{{#date}}%B %Y{{/date}}{{/previous_archive}}{{/archive}}{{^archive}}older{{/archive}}
|
78
|
+
</a>{{/has_previous?}}
|
79
|
+
{{#has_next?}}{{#has_previous?}} — {{/has_previous?}}{{/has_next?}}
|
80
|
+
{{#has_next?}}<a href="{{next_url}}">
|
81
|
+
{{#archive}}{{#next_archive}}{{#date}}%B %Y{{/date}}{{/next_archive}}{{/archive}}{{^archive}}newer{{/archive}}
|
82
|
+
→</a>{{/has_next?}}
|
83
|
+
|
84
|
+
</div>
|
85
|
+
{{/paged?}}
|
86
|
+
|
87
|
+
<div id="archives">
|
88
|
+
<h4>Archives</h4>
|
89
|
+
<ul>
|
90
|
+
{{#archives}}
|
91
|
+
<li>
|
92
|
+
{{^open?}}<a href="{{permalink}}">{{/open?}}
|
93
|
+
{{#date}}%B %Y{{/date}}
|
94
|
+
{{^open?}}</a>{{/open?}}
|
95
|
+
</li>
|
96
|
+
{{/archives}}
|
97
|
+
</ul>
|
98
|
+
</div><!-- archives -->
|
99
|
+
|
100
|
+
<div class="copyright">
|
101
|
+
Copyright © {{#today}}%Y{{/today}} {{blog_author}}. All rights reserved.
|
102
|
+
Made with <a href="https://github.com/nilium/oaktree">OakTree</a>.
|
103
|
+
</div><!-- copyright -->
|
104
|
+
|
105
|
+
</body>
|
106
|
+
</html>
|
107
|
+
EOT
|
108
|
+
|
109
|
+
RSS_FEED_TEMPLATE = <<EOT
|
110
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
111
|
+
<rss version="2.0">
|
112
|
+
<channel>
|
113
|
+
<title>{{blog_title}}</title>
|
114
|
+
<link>{{blog_url}}</link>
|
115
|
+
<description>{{blog_description}}</description>
|
116
|
+
<pubDate>{{#today}}%a, %d %b %Y %T %z{{/today}}</pubDate>
|
117
|
+
<docs>http://www.rssboard.org/rss-2-0-1</docs>
|
118
|
+
{{#posts}}<item>
|
119
|
+
<title>{{title}}</title>
|
120
|
+
<link>{{permalink}}</link>
|
121
|
+
<description><![CDATA[{{{content}}}]]></description>
|
122
|
+
</item>{{/posts}}
|
123
|
+
</channel>
|
124
|
+
</rss>
|
125
|
+
EOT
|
126
|
+
|
127
|
+
# Commands
|
128
|
+
|
129
|
+
# Generate a new blog_spec and create 'source' and 'public' directories under
|
130
|
+
# the given directory.
|
131
|
+
def init_blog directory
|
132
|
+
if ! (File.exists?(directory) && File.directory?(directory)) && directory != '.'
|
133
|
+
FileUtils.mkdir_p directory
|
134
|
+
end
|
135
|
+
|
136
|
+
Dir.chdir(directory) { |path|
|
137
|
+
if File.exists? 'blog_spec' then
|
138
|
+
raise "blog_spec already exists in #{directory}"
|
139
|
+
end
|
140
|
+
|
141
|
+
Dir.mkdir 'source'
|
142
|
+
Dir.mkdir 'public'
|
143
|
+
Dir.mkdir 'template'
|
144
|
+
|
145
|
+
File.open('blog_spec', 'w') { |io| io.write OakTree::Specification.new.export_string }
|
146
|
+
File.open('template/blog.mustache', 'w') { |io| io.write BLOG_TEMPLATE }
|
147
|
+
File.open('template/rss_feed.mustache', 'w') { |io| io.write RSS_FEED_TEMPLATE }
|
148
|
+
}
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
# Show the version
|
153
|
+
def show_version
|
154
|
+
version = OakTree::VERSION
|
155
|
+
|
156
|
+
puts "#{GEM_NAME} version #{version} (ruby #{RUBY_VERSION} #{RUBY_PLATFORM} #{RUBY_RELEASE_DATE})"
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
# Show the help text for oak
|
161
|
+
def show_help
|
162
|
+
puts HELP_TEXT
|
163
|
+
show_version
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
# Generate a new post file for the blog
|
168
|
+
def new_post spec, title
|
169
|
+
title = title.gsub(/[\n\t]+/, '').strip
|
170
|
+
|
171
|
+
today = DateTime.now
|
172
|
+
|
173
|
+
titleslug = title.gsub(/[^_\w\s]/, '').strip.gsub(/\s+/, '_').downcase
|
174
|
+
timeslug = today.strftime '%Y-%m-%d'
|
175
|
+
|
176
|
+
file = "#{spec.sources_root}#{timeslug}_#{titleslug}.md"
|
177
|
+
|
178
|
+
head = <<HEADSTR
|
179
|
+
title: #{title}
|
180
|
+
time: #{today.strftime '%Y-%m-%d %H:%M:%S %z'}
|
181
|
+
----
|
182
|
+
|
183
|
+
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
|
184
|
+
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
|
185
|
+
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
186
|
+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
|
187
|
+
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
|
188
|
+
culpa qui officia deserunt mollit anim id est laborum.
|
189
|
+
HEADSTR
|
190
|
+
|
191
|
+
IO.write file, head
|
192
|
+
|
193
|
+
return file
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
def sync_changes spec, force
|
198
|
+
raise "No blog_spec found" if spec.nil?
|
199
|
+
|
200
|
+
blog = OakTree.new spec
|
201
|
+
blog.generate force
|
202
|
+
end
|
203
|
+
|
204
|
+
# Command selector majigger
|
205
|
+
def dispatch_command cmd, *args
|
206
|
+
|
207
|
+
|
208
|
+
spec = (File.exists? 'blog_spec') ? (OakTree::Specification.from_file 'blog_spec') : nil
|
209
|
+
|
210
|
+
case cmd
|
211
|
+
|
212
|
+
when 'newpost', '-n'
|
213
|
+
raise "Not in a blog directory" unless File.exists? 'blog_spec'
|
214
|
+
|
215
|
+
open_editor = false
|
216
|
+
title = ''
|
217
|
+
|
218
|
+
until args.empty?
|
219
|
+
arg = args.shift
|
220
|
+
|
221
|
+
# finnagling with arguments
|
222
|
+
if arg =~ /^(--[^=]+)=(.*)$/
|
223
|
+
arg = $1
|
224
|
+
args.unshift $2
|
225
|
+
end
|
226
|
+
|
227
|
+
if arg =~ /^(-[t])(.+)$/
|
228
|
+
arg = $1
|
229
|
+
args.unshift $2
|
230
|
+
end
|
231
|
+
|
232
|
+
case arg
|
233
|
+
when '-t', '--time'
|
234
|
+
output_dir = args.shift
|
235
|
+
break unless title.empty?
|
236
|
+
when '-e', '--edit'
|
237
|
+
open_editor = true
|
238
|
+
else
|
239
|
+
title = "#{title} #{arg}"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
title = 'Untitled' if title.empty?
|
244
|
+
title = args.join ' ' unless args.empty?
|
245
|
+
file = new_post(spec, title)
|
246
|
+
|
247
|
+
if open_editor
|
248
|
+
editor = ENV['EDITOR'] || 'open'
|
249
|
+
%x["#{editor}" "#{file}"]
|
250
|
+
end
|
251
|
+
|
252
|
+
when 'init', '-i'
|
253
|
+
directory = '.'
|
254
|
+
directory = args[0] unless args.empty?
|
255
|
+
init_blog directory
|
256
|
+
|
257
|
+
when 'version', '-v'
|
258
|
+
show_version
|
259
|
+
|
260
|
+
when 'help', '-h'
|
261
|
+
show_help
|
262
|
+
|
263
|
+
when 'rebuild', '-r'
|
264
|
+
sync_changes spec, true
|
265
|
+
|
266
|
+
when 'sync', '-s'
|
267
|
+
sync_changes spec, false
|
268
|
+
|
269
|
+
else
|
270
|
+
puts "Unrecognized command '#{cmd}'\n\n"
|
271
|
+
|
272
|
+
show_help
|
273
|
+
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
# Main logic
|
280
|
+
|
281
|
+
if ARGV.empty? then
|
282
|
+
show_help
|
283
|
+
else
|
284
|
+
dispatch_command ARGV[0], *ARGV[1..ARGV.length]
|
285
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'kramdown'
|
2
|
+
require 'kramdown/element'
|
3
|
+
|
4
|
+
class OakTree
|
5
|
+
|
6
|
+
module Kramdown
|
7
|
+
|
8
|
+
class OakHtml < ::Kramdown::Converter::Html
|
9
|
+
|
10
|
+
def convert_footnote el, indent
|
11
|
+
if @options[:auto_id_prefix]
|
12
|
+
el.options[:name] = @options[:auto_id_prefix] + el.options[:name]
|
13
|
+
end
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def convert_div el, indent
|
18
|
+
"<#{el.type}#{html_attributes el.attr}>\n#{inner el, indent+1}\n</#{el.type}>"
|
19
|
+
end
|
20
|
+
|
21
|
+
def footnote_content
|
22
|
+
return '' if @footnotes.empty?
|
23
|
+
|
24
|
+
block = ::Kramdown::Element.new(:div, nil, {'class' => 'footnotes'})
|
25
|
+
block.children << (list = ::Kramdown::Element.new(:ol))
|
26
|
+
|
27
|
+
@footnotes.each { |fn_name, fn_elem|
|
28
|
+
item = ::Kramdown::Element.new(:li, nil, {'id' => "fn:#{fn_name}"})
|
29
|
+
# because we'll end up manipulating a child, we may as well do a deep copy
|
30
|
+
item.children = Marshal.load(Marshal.dump(fn_elem.children))
|
31
|
+
|
32
|
+
# basically what kramdown already does here
|
33
|
+
last = (last = item.children.last).type == :p ? last : (::Kramdown::Element.new(:p))
|
34
|
+
# yes, I'm using rev, shut up
|
35
|
+
last.children << (anchor = ::Kramdown::Element.new(:a, nil, {'href' => "#fnref:#{fn_name}", 'rev' => 'footnote'}))
|
36
|
+
anchor.children << ::Kramdown::Element.new(:raw, '↩')
|
37
|
+
|
38
|
+
list.children << item
|
39
|
+
}
|
40
|
+
|
41
|
+
convert(block, 2)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'digest'
|
3
|
+
require 'psych'
|
4
|
+
|
5
|
+
class OakTree ; end
|
6
|
+
|
7
|
+
|
8
|
+
# Contains the contents of a single post under the sources/ directory. Unlike
|
9
|
+
# past versions of PostData, this does not synchronize with the source file
|
10
|
+
# every time a member is accessed. It's assumed that what you got when you
|
11
|
+
# loaded the post is what you wanted and that any further changes must be
|
12
|
+
# explicitly synchronized.
|
13
|
+
#
|
14
|
+
class OakTree::PostData
|
15
|
+
|
16
|
+
attr_accessor :source_name
|
17
|
+
attr_accessor :source_path
|
18
|
+
# The path to the HTML file that's written when compiling the post.
|
19
|
+
attr_accessor :public_path
|
20
|
+
attr_accessor :title
|
21
|
+
attr_accessor :link
|
22
|
+
attr_accessor :permalink
|
23
|
+
attr_reader :time
|
24
|
+
# The post's slug, a filesystem- and URL-friendly string.
|
25
|
+
attr_reader :slug
|
26
|
+
# The post's body -- that is, the actual content of the blog post.
|
27
|
+
attr_accessor :content
|
28
|
+
# The time when the file was last read. Used to determine if sync_changes will
|
29
|
+
# reload the file. Defaults to the Unix epoch.
|
30
|
+
attr_accessor :last_read_time
|
31
|
+
# The kind of post this is. Expected to be either :post or :static, though
|
32
|
+
# the value is arbitrary.
|
33
|
+
attr_reader :kind
|
34
|
+
# The post's status. Either :published or :unpublished, though only :published
|
35
|
+
# holds meaning -- other values are assumed to be unpublished.
|
36
|
+
attr_reader :status
|
37
|
+
attr_accessor :hash
|
38
|
+
attr_accessor :spec
|
39
|
+
|
40
|
+
protected :source_path=, :public_path=, :title=, :link=, :content=,
|
41
|
+
:last_read_time=, :hash=, :spec=, :permalink=, :source_name=
|
42
|
+
|
43
|
+
# Loads a new post from the source file using the given Specification. The
|
44
|
+
# source file should be the full filename of the source file, but without any
|
45
|
+
# other path components.
|
46
|
+
#
|
47
|
+
def initialize(source_name, spec)
|
48
|
+
set_post_defaults
|
49
|
+
|
50
|
+
self.spec = spec
|
51
|
+
|
52
|
+
self.hash = nil
|
53
|
+
self.last_read_time = Time.at(0).to_datetime
|
54
|
+
|
55
|
+
self.source_name = source_name
|
56
|
+
self.source_path = File.absolute_path(source_name,
|
57
|
+
spec.sources_root).freeze()
|
58
|
+
|
59
|
+
raise "File doesn't exist: #{@source_path}" if ! File.exists? @source_path
|
60
|
+
|
61
|
+
sync_changes true
|
62
|
+
end # initialize
|
63
|
+
|
64
|
+
# Returns the default 'kind' a post should be. Typically means :post, though
|
65
|
+
# it may change in the future.
|
66
|
+
#
|
67
|
+
def self.default_kind
|
68
|
+
:post
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the default 'status' a post should have. Typically :published, but
|
72
|
+
# this may change in the future.
|
73
|
+
#
|
74
|
+
def self.default_status
|
75
|
+
:published
|
76
|
+
end
|
77
|
+
|
78
|
+
# The regexp for identifying the line that separates the post head from the
|
79
|
+
# post body. This is typically three or more hyphens.
|
80
|
+
def self.metadata_separator
|
81
|
+
/^-{3,}\s*$/
|
82
|
+
end
|
83
|
+
|
84
|
+
# Synchronizes changes between the post's file and the post data object. If
|
85
|
+
# 'forced' is true (or non-false/nil), it will reload the file regardless of
|
86
|
+
# whether it's considered necessary.
|
87
|
+
#
|
88
|
+
# A "necessary" reload is when the file's hash changes or the file's last
|
89
|
+
# modification time is more recent than what the post data last read.
|
90
|
+
#
|
91
|
+
def sync_changes(forced = false)
|
92
|
+
raise "Source file does not exist." unless File.exists? @source_path
|
93
|
+
|
94
|
+
source_differs = !! forced
|
95
|
+
source_mtime = File.mtime(@source_path).to_datetime()
|
96
|
+
source_contents = File.open(@source_path, 'r') { |io| io.read }
|
97
|
+
source_hash = Digest::SHA1.hexdigest(source_contents).freeze()
|
98
|
+
|
99
|
+
if ! forced
|
100
|
+
# Check that the source differs from what was last checked.
|
101
|
+
source_differs = (hash != source_hash)
|
102
|
+
public_exists = (@public_path && File.exists?(@public_path))
|
103
|
+
|
104
|
+
# Check if the public file is older than the current source file.
|
105
|
+
if ! source_differs && public_exists
|
106
|
+
public_mtime = File.mtime(@public_path).to_datetime()
|
107
|
+
|
108
|
+
source_differs = true if public_mtime < source_mtime
|
109
|
+
end
|
110
|
+
|
111
|
+
if ! source_differs && @last_read_time < source_mtime
|
112
|
+
source_differs = true
|
113
|
+
end
|
114
|
+
end # ! forced
|
115
|
+
|
116
|
+
return if ! source_differs
|
117
|
+
|
118
|
+
self.last_read_time = source_mtime
|
119
|
+
|
120
|
+
# Reset the post's members to an unloaded state
|
121
|
+
set_post_defaults
|
122
|
+
self.hash = source_hash
|
123
|
+
|
124
|
+
source_split = source_contents.partition(self.class.metadata_separator)
|
125
|
+
|
126
|
+
load_header source_split[0]
|
127
|
+
self.content = source_split[2]
|
128
|
+
|
129
|
+
self
|
130
|
+
end # sync_changes
|
131
|
+
|
132
|
+
protected
|
133
|
+
|
134
|
+
# The regular expression used to fix post slugs. Basically matches groups of
|
135
|
+
# non-alphanumeric characters. This is used to replace slug-unfriendly chunks
|
136
|
+
# of new slugs with slug word separators.
|
137
|
+
#
|
138
|
+
def self.slug_fix_regexp
|
139
|
+
%r{(?:[^[:alnum:]] | [[:space:]])+}x
|
140
|
+
end
|
141
|
+
|
142
|
+
# Sets the default values for the post's data-related instance variables, so
|
143
|
+
# anything loaded during synchronization gets reset by this. Includes the
|
144
|
+
# post title, date, status, etc.
|
145
|
+
#
|
146
|
+
def set_post_defaults
|
147
|
+
self.public_path = nil
|
148
|
+
self.title = nil
|
149
|
+
self.link = nil
|
150
|
+
self.permalink = nil
|
151
|
+
self.time = nil
|
152
|
+
self.slug = ''.freeze()
|
153
|
+
|
154
|
+
self.kind = self.class.default_kind
|
155
|
+
self.status = self.class.default_status
|
156
|
+
end # set_post_defaults
|
157
|
+
|
158
|
+
# Reads the header from the header_source string into the post data. Currently
|
159
|
+
# uses Psych to parse the header as YAML.
|
160
|
+
#
|
161
|
+
def load_header(header_source)
|
162
|
+
begin
|
163
|
+
header_hash = Psych.load(header_source)
|
164
|
+
rescue Psych::SyntaxError => ex
|
165
|
+
puts "Failed to parse header for #{@source_name}"
|
166
|
+
raise
|
167
|
+
end
|
168
|
+
|
169
|
+
header_hash.each {
|
170
|
+
|key, value|
|
171
|
+
setter_sym = "#{key.to_s}=".to_sym
|
172
|
+
if self.respond_to?(setter_sym, true)
|
173
|
+
self.send setter_sym, value
|
174
|
+
else
|
175
|
+
raise "Invalid key/value for header: #{key} => #{value}."
|
176
|
+
end
|
177
|
+
}
|
178
|
+
|
179
|
+
self.slug = title if ! slug || slug.empty?
|
180
|
+
|
181
|
+
# Set the public HTML path and the permalink now that we have enough info
|
182
|
+
# about the post.
|
183
|
+
root = spec.blog_root
|
184
|
+
url = spec.base_url
|
185
|
+
|
186
|
+
link_path = String.new(spec.post_path)
|
187
|
+
link_path << @time.strftime(spec.date_path_format) if kind == :post
|
188
|
+
link_path << slug
|
189
|
+
|
190
|
+
self.public_path = "#{root}public/#{link_path}/index.html".freeze()
|
191
|
+
self.permalink = "#{url}#{link_path}"
|
192
|
+
|
193
|
+
nil
|
194
|
+
end # load_header
|
195
|
+
|
196
|
+
# Assigns a new time to the post -- the time must be a DateTime object or a
|
197
|
+
# String capable of being parsed by DateTime.parse.
|
198
|
+
def time=(new_time)
|
199
|
+
@time = if new_time
|
200
|
+
case new_time.class
|
201
|
+
when DateTime ; new_time
|
202
|
+
when String ; DateTime.parse new_time
|
203
|
+
else new_time.to_datetime
|
204
|
+
end
|
205
|
+
else
|
206
|
+
Time.at(0).to_datetime
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Sets the post slug, ensuring it's formatted properly.
|
211
|
+
def slug=(new_slug)
|
212
|
+
slug_temp = new_slug
|
213
|
+
if slug_temp
|
214
|
+
slug_temp = String.new(new_slug)
|
215
|
+
slug_temp.strip!
|
216
|
+
|
217
|
+
unless slug_temp.empty?
|
218
|
+
slug_temp.downcase!
|
219
|
+
slug_temp.gsub!(self.class.slug_fix_regexp, @spec.slug_separator)
|
220
|
+
end
|
221
|
+
else
|
222
|
+
slug_temp = ''
|
223
|
+
end
|
224
|
+
|
225
|
+
@slug = slug_temp.freeze()
|
226
|
+
end # slug=
|
227
|
+
|
228
|
+
def kind=(new_kind)
|
229
|
+
@kind = new_kind.to_sym
|
230
|
+
end
|
231
|
+
|
232
|
+
def status=(new_status)
|
233
|
+
@status = new_status.to_sym
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|