| 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("&", "&"):gsub("<", "<"), "\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"); |