build.lua

1
require "lfs"
2
 
3
require "jorvick.templates"
4
require "jorvick.parsers"
5
require "jorvick.util"
6
 
7
-- Logging --
8
function make_msg(prefix, name) return function (fmt, ...) print(prefix, string.format(fmt, ...)); end end
9
msg_info, msg_warn, msg_error = make_msg("II", "info"), make_msg("WW", "warn"), make_msg("EE", "error");
10
-- -- - -- --
11
 
12
posts = {};
13
 
14
 
15
-- Read posts, interpret
16
for post_filename in lfs.dir(posts_dir:gsub("/$", "")) do
17
	-- Ignore dotfiles
18
	if not post_filename:match("^%.") then
19
		msg_info("Processing %s", post_filename);
20
		local post_file, err = io.open(posts_dir..post_filename);
21
		if post_file then
22
			local post_format = post_filename:match("%.(%w+)$") or "text";
23
			local post = { filename = post_filename, path = posts_dir..post_filename, format = post_format };
24
			if not parsers[post_format] then
25
				msg_warn("Post format '%s' not supported", post_format);
26
			else
27
				local inside_header, line_num = nil, 0;
28
				for line in post_file:lines() do
29
					line_num = line_num + 1;
30
					if  line:match("^%-%-%-") then
31
						if not inside_header then
32
							inside_header = true;
33
						else
34
							inside_header = false;
35
							break;
36
						end
37
					elseif not inside_header then
38
						break;
39
					else
40
						local k,v = line:match("^(%w+):%s?(.*)$");
41
						if k and v then
42
							post[k] = v;
43
						else
44
							msg_warn("Couldn't parse header at line %d", line_num);
45
						end
46
					end
47
				end
48
			end	
49
		
50
			local post_data, err = post_file:read("*a");
51
 
52
			-- Parse post_data according to extension, add to post list
53
			--msg_info("Parsing %s...", post_filename) --:gsub("%.[^%.]+$", ""):match("[^/]+"));
54
			post.content = templates.load(parsers[post_format](post_data)):render{ page = post };
55
			post.shorttitle = make_short_title(post.title);
56
			post.url = base_url..post.shorttitle.."/"
57
			post.post = post
58
			post.updated = post.updated or os.date("!%Y-%m-%dT%H:%M:%SZ", lfs.attributes(post.path).modification);
59
			if post.published then
60
				if not post.published:match("^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d%:%d%dZ$") then
61
					msg_error("Post has invalid published date");
62
				end
63
			end
64
			table.insert(posts, post);
65
		else	
66
			msg_error("Failed to open %s, %s", post_filename, err);
67
		end
68
	end
69
end
70
 
71
local layouts = setmetatable({ default = templates.init(layouts_dir.."default.html"); }, 
72
	{ __index = function (t, k) rawset(t, k, templates.init(layouts_dir..k..".html")); return rawget(t, k); end });
73
 
74
table.sort(posts, function (a, b)
75
			do return (a.updated or "") < (b.updated or ""); end
76
			if not a.updated then return true; end
77
			if not b.updated then return false; end
78
			local a_year, a_month, a_day = a.updated:match("(%d%d%d%d)%-(%d%d)%-(%d%d)");
79
			local b_year, b_month, b_day = b.updated:match("(%d%d%d%d)%-(%d%d)%-(%d%d)");
80
			if b_year >= a_year then return true; end
81
			if b_month >= a_month then return true; end
82
			if b_day >= a_day then return true; end
83
			return false;
84
		end);
85
 
86
do
87
	local shorttitles = {};
88
	for _, post in ipairs(posts) do
89
		while shorttitles[post.shorttitle] do
90
			post.shorttitle = post.shorttitle:gsub("%~%d+$", "").."~"..((tonumber(post.shorttitle:match("%~(%d*)$")) or 0) + 1)
91
			post.url = base_url..post.shorttitle.."/"
92
		end
93
		shorttitles[post.shorttitle] = post;
94
	end
95
end
96
 
97
msg_info("%s post%s processed", #posts, #posts ~= 1 and "s" or "");
98
 
99
msg_info(" - - - ");
100
 
101
local unpublished, removed_posts = 0, {};
102
for n, post in ipairs(posts) do
103
	if post.content and (not post.layout or layouts[post.layout]) then
104
		lfs.mkdir(output_dir..post.shorttitle);
105
		local outfile, err = io.open(output_dir..post.shorttitle.."/index.html", "w+");
106
		if outfile then
107
			local layout = layouts[post.layout or "default"];
108
			
109
			-- Link to the previous and next published posts
110
			local i = 1;
111
			repeat
112
				post.next = posts[n+i];
113
				i = i + 1;
114
			until (not post.next) or post.next.published;
115
			i = 1;
116
			repeat
117
				post.previous = posts[n-i];
118
				i = i + 1;
119
			until (not post.previous) or post.previous.published;
120
			
121
			post.id = n;
122
			outfile:write(layout:render(post));
123
			outfile:close();
124
			if not post.published then
125
				-- Not published yet, hide it
126
				--msg_info("Hiding unpublished post");
127
				unpublished = unpublished + 1;
128
				table.insert(removed_posts, n);
129
			end
130
		else
131
			msg_error("Failed to write HTML: %s", err);
132
		end
133
		if post.published then
134
			msg_info("Published: %s", post.shorttitle);
135
		else
136
			msg_info("Generated: %s", post.shorttitle);
137
		end
138
	end
139
end
140
 
141
local removed = 0;
142
for _, n in ipairs(removed_posts) do
143
	table.remove(posts, n-removed);
144
	removed = removed + 1;
145
end
146
 
147
-- Do main page
148
local main_tpl = templates.init(layouts_dir.."/main.html");
149
 
150
main_tpl:saverender(output_dir.."index.html", { title = blog_title, posts = posts });
151
 
152
msg_info("%s post%s published, %d post%s unpublished", #posts, #posts ~= 1 and "s" or "", unpublished, unpublished ~= 1 and "s" or "");
153
 
154
msg_info(" - - - ");
155
 
156
-- Do feed
157
local atom = io.open(output_dir.."feed/atom.xml.new", "w+");
158
atom:write[[<?xml version="1.0" encoding="UTF-8"?>
159
	<feed xmlns="http://www.w3.org/2005/Atom">
160
]]
161
atom:write("\t<id>", base_url, "</id>\n");
162
atom:write("\t<title>", blog_title, "</title>\n");
163
atom:write("\t<subtitle>", blog_subtitle, "</subtitle>\n");
164
atom:write("\t<link href='", base_url, "feed/atom.xml' rel='self' />\n");
165
atom:write("\t<link href='", base_url, "' />\n");
166
 
167
atom:write("\t<updated>", os.date "!%Y-%m-%dT%H:%M:%SZ", "</updated>\n");
168
 
169
for n = #posts,#posts-10,-1 do
170
	local post = posts[n];
171
	if post.published then
172
		atom:write("<entry>\n");
173
		atom:write("\t<author>\n\t\t<name>", post.author or default_author or "Unknown Author", "</name>\n\t</author>\n");
174
		atom:write("\t<id>", post.url, "</id>\n");
175
		atom:write("\t<title>", post.title, "</title>\n");
176
		atom:write("\t<published>", post.published, "</published>\n");
177
		atom:write("\t<updated>", post.updated, "</updated>\n");
178
		atom:write("\t<link href='", post.url, "' />\n");
179
		atom:write("\t<content type='html'>\n", post.content:gsub("&", "&amp;"):gsub("<", "&lt;"), "\t</content>\n");
180
		atom:write("</entry>\n");
181
	end
182
end
183
atom:write("</feed>");
184
atom:close();
185
os.rename("feed/atom.xml.new", "feed/atom.xml");
186
 
187
msg_info("ATOM feed written to feed/atom.xml");