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