local RssChannel = {}
RssChannel.new = function(title, link, description, settings)
local settings = settings or {}
local channel = {
title=title,
link=link,
description=description,
}
channel.language = settings.language
channel.copyright = settings.copyright
channel.managingEditor = settings.managingEditor
channel.webMaster = settings.webMaster
channel.lastBuildDate = settings.lastBuildDate
channel.category = settings.category
channel.generator = 'argent v0.1.0'
channel.docs = 'https://www.rssboard.org/rss-specification'
channel.items = {}
setmetatable(
channel,
{
__index=RssChannel,
__tostring=RssChannel.tostring,
}
)
return channel
end
RssChannel.addItem = function(self, tbl)
local to_rfc822 = function(datestring, zone)
if not string.match(datestring, '^%d%d%d%d%-%d%d%-%d%d$') then
error(string.format('%q is not a valid date string', datestring))
end
local year = string.match(datestring, '^%d%d%d%d')
local month = string.match(datestring, '-(%d%d)-')
local day = string.match(datestring, '%d%d$')
local month_names = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'}
month = month_names[tonumber(month)]
return string.format('%s %s %s 00:00:00 %s', day, month, year, zone)
end
if not (tbl.title or tbl.description) then
error('items must specify either a title or description!')
end
if not tbl.pubDate then
error('items must specify a pubDate!')
else
tbl.pubDate = to_rfc822(tbl.pubDate, 'Z')
end
table.insert(self.items, tbl)
end
local tag_builder = function(source_table, indent_level)
local str = ''
local indent = string.rep(' ', indent_level)
local add_tag = function(tag)
if source_table[tag] then
str = str ..
string.format('%s<%s>%s%s>\n',
indent, tag, source_table[tag], tag)
end
end
local content = function()
return str
end
return add_tag, content
end
local render_item = function(item_tbl)
local add_tag, content = tag_builder(item_tbl, 2)
add_tag('title')
add_tag('link')
add_tag('description')
add_tag('author')
add_tag('category')
add_tag('comments')
add_tag('guid')
add_tag('pubDate')
local indent = string.rep(' ', 1)
return string.format('%s- \n%s%s
',
indent, content(), indent)
end
RssChannel.tostring = function(self)
local add_tag, content = tag_builder(self, 1)
add_tag('title')
add_tag('link')
add_tag('description')
add_tag('language')
add_tag('copyright')
add_tag('managingEditor')
add_tag('webMaster')
add_tag('pubDate')
add_tag('lastBuildDate')
add_tag('category')
add_tag('generator')
add_tag('docs')
items_str = ''
for _, item in ipairs(self.items) do
items_str = items_str .. render_item(item) .. '\n'
end
return string.format(
'\n\n%s\n%s\n',
content(), items_str)
end
RssChannel.save = function(self, filename)
file = io.open(filename, 'w')
file:write(self:tostring())
file:close()
end
return function(config)
local fmt = string.format
--------------------------------
--
-- basics
--
--------------------------------
function Set(array, transform)
if type(array) ~= 'table' then return nil end
local transform = transform or function(x) return x end
local set = {}
for _, element in ipairs(array) do
set[transform(element)] = true
end
return set
end
function set_tostring(set)
local str = '[empty]'
if not set then return str end
for element in pairs(set) do
if str == '[empty]' then
str = tostring(element)
else
str = str .. '; '.. tostring(element)
end
end
return str
end
function add_end_slash(str)
if type(str) ~= 'string' then return nil end
if string.match(str, '/$') then return str end
return str .. '/'
end
function strip_end_slash(str)
if string.match(str, '/$') then return string.sub(str, 1, #str-1) end
return str
end
function load_layouts(directory, parent, layouts)
local directory = add_end_slash(directory) or ''
local parent = parent or ''
local layouts = layouts or {}
local fullpath = parent..directory
local dirs, files = argent.scanDirectory(argent.config.layout_directory..fullpath)
for _, file in ipairs(files) do
if string.match(file, '%.lua$') then
local basename = string.gsub(file, '%.lua$', '')
local id = string.gsub(fullpath..basename, '/', '.')
argent.log('debug', fmt('loading layout %q', id))
success, result = pcall(loadfile(argent.config.layout_directory..fullpath..file), 0, 1)
if not success then
argent.log('error', result)
argent.log('warn', fmt('layout %q will not be available', id))
else
layouts[id] = result
end
end
end
for _, dir in ipairs(dirs) do
if dir ~= '.' and dir ~= '..' then
argent.log('debug', fmt('loading layouts from %q', parent..directory))
load_layouts(dir, parent..directory, layouts)
end
end
return layouts
end
function setup(config)
argent.log('debug', 'begin setup')
if not config.site_name then
error('site_name MUST be set!')
end
argent.config = {
site_name = config.site_name,
site_address = add_end_slash(config.site_address) or nil,
site_description = config.site_description or '',
site_language = config.site_language,
site_copyright = config.site_copyright,
site_directory = add_end_slash(config.site_directory) or 'site/',
output_directory = add_end_slash(config.output_directory) or 'public/',
layout_directory = add_end_slash(config.layout_directory) or nil,
plugin_directory = add_end_slash(config.plugin_directory) or nil,
exclude = Set(config.exclude, strip_end_slash) or {},
include = Set(config.include, strip_end_slash) or {},
keep = Set(config.keep, strip_end_slash) or {},
noprocess = Set(config.noprocess, strip_end_slash) or {},
rss_include = Set(config.rss_include, strip_end_slash) or nil,
webmaster_email = config.webmaster_email,
}
argent.log('info', fmt('site directory: %s', argent.config.site_directory))
argent.log('info', fmt('output directory: %s', argent.config.output_directory))
argent.log('info', fmt('layout directory: %s', tostring(argent.config.layout_directory)))
argent.log('info', fmt('plugin directory: %s', tostring(argent.config.plugin_directory)))
argent.log('info', 'exclusions: '..set_tostring(argent.config.exclude))
argent.log('info', 'inclusions: '..set_tostring(argent.config.include))
argent.log('info', 'files to keep: '..set_tostring(argent.config.keep))
argent.log('info', 'noprocess: '..set_tostring(argent.config.noprocess))
argent.log('info', 'rss files: '..set_tostring(argent.config.rss_include))
if argent.config.plugin_directory then
package.path =
add_end_slash(argent.currentWorkingDirectory())
..argent.config.plugin_directory..'?.lua;'..package.path
end
if argent.config.layout_directory then
argent.layouts = load_layouts()
else
argent.layouts = {}
end
argent.log('info', fmt('available layouts: %s', set_tostring(argent.layouts)))
if argent.config.rss_include then
if not argent.config.site_address then
error('rss_include is set, but site_address is not!')
end
argent.rss_channel = RssChannel.new(
argent.config.site_name,
argent.config.site_address,
argent.config.site_description,
{ language = argent.config.site_language,
copyright = argent.config.site_copyright,
webMaster = argent.config.webmaster_email,
}
)
end
argent.log('debug', 'end setup')
end
function is_dotfile(filename)
if string.match(filename, '^%.') then return true end
return false
end
function directory_exists(directory, parent)
local directory = directory or ''
local parent = parent or ''
local dirs = argent.scanDirectory(argent.config.output_directory..parent)
for _, dir in ipairs(dirs) do
if strip_end_slash(directory) == dir then return true end
end
return false
end
function output_directory_exists()
local dirs = argent.scanDirectory('./')
for _, dir in ipairs(dirs) do
if add_end_slash(dir) == argent.config.output_directory then
return true
end
end
return false
end
--------------------------------
--
-- obliterating
--
--------------------------------
function obliterate_file(file, parent)
if argent.config.keep[file] then
argent.log('debug', fmt('retaining file %q', parent..file))
return true
end
argent.log('debug', fmt('removing file %q', parent..file))
os.remove(argent.config.output_directory..parent..file)
return false
end
function obliterate_dir(dir, parent)
if argent.config.keep[strip_end_slash(dir)] then
argent.log('debug', fmt('retaining directory %q', parent..dir))
return true
end
argent.log('debug', fmt('obliterating files in %q', parent..dir))
return obliterate(dir, parent)
end
function obliterate(dirname, parent)
local dirname = add_end_slash(dirname) or ''
local parent = parent or ''
if argent.config.keep[dirname] then return true end
local keep_self = (dirname == '')
local dirs, files = argent.scanDirectory(argent.config.output_directory..parent..dirname)
for _, file in ipairs(files) do
keep_self = obliterate_file(file, parent..dirname) or keep_self
end
for _, dir in ipairs(dirs) do
if dir ~= '.' and dir ~= '..' then
keep_self = obliterate_dir(dir, parent..dirname) or keep_self
end
end
if not keep_self then
os.remove(argent.config.output_directory..parent..dirname)
return false
else
return true
end
end
--------------------------------
--
-- processing
--
--------------------------------
function should_include(filename, parent)
for pattern in pairs(argent.config.include) do
if string.match(filename, pattern) or string.match(parent..filename, pattern) then
return true
end
end
return false
end
function should_ignore(filename, parent)
if is_dotfile(filename) and not should_include(filename, parent) then
return false
end
for pattern in pairs(argent.config.exclude) do
if string.match(filename, pattern)
or string.match(parent..filename, pattern) then
return true
end
end
return false
end
function should_process(filename, parent)
if not string.match(filename, '%.lua$') then
return false
end
if should_ignore(filename, parent) then
return false
end
for pattern in pairs(argent.config.noprocess) do
if string.match(filename, pattern)
or string.match(parent..filename, pattern) then
return false
end
end
return true
end
function process_file(filename, parent)
if should_ignore(filename, parent) then
argent.log('debug', fmt('will not process file %q', parent..filename))
return
end
if should_process(filename, parent) then
argent.log('debug', fmt('processing %q as lua file', parent..filename))
process_lua_file(filename, parent)
else
argent.log('debug', fmt('processing %q as regular file', parent..filename))
argent.copyFile(
argent.config.site_directory..parent..filename,
argent.config.output_directory..parent..filename
)
end
end
function process_lua_file(filename, parent)
local success, result = pcall(loadfile(argent.config.site_directory..parent..filename))
if not success then
argent.log('error', result)
argent.log('warn', fmt('%q will not be processed for output!', parent..filename))
return
end
if not type(result) == 'table' then
argent.log('error', fmt('%q returned %q instead of %q', filename, type(result), 'table'))
argent.log('warn', fmt('%q will not result in file output!', filename))
return
end
local html
if result.html then
html = result.html
elseif result.markdown then
html = argent.markdown(result.markdown)
else
argent.log('error', fmt('%q did not specify any content!', filename))
argent.log('warn', fmt('%q will not result in file output!', filename))
return
end
if not result.title then
result.title = string.gsub(filename, '%.lua$', '')
argent.log('warn', fmt('%q did not specify a title; using default title of %q',
filename, result.title))
end
local layout = function(html, tbl) return html end
if result.layout then
if not argent.layouts[result.layout] then
argent.log('error', fmt('%q requested nonexistent layout %q', filename, result.layout))
argent.log('warn', fmt('%q will not result in file output!', filename))
return
else
layout = argent.layouts[result.layout]
end
else -- no layout specified
argent.log('warn', fmt(
'%q did not specify a layout; output will be naked content!',
filename))
end
local output_data = layout(html, result)
local output_name = string.gsub(filename, '%.lua$', '.html')
local output_file = io.open(argent.config.output_directory..parent..output_name, 'w')
output_file:write(output_data)
output_file:close()
if argent.rss_channel then
for pattern in pairs(argent.config.rss_include) do
if string.match(filename, pattern)
or string.match(parent..filename, pattern)
then -- add to the RSS feed!
if not result.date then
argent.log(
'warn',
fmt(
'%q did not specify a date; it will not be included in rss.xml!',
parent..filename
)
)
return
end
argent.rss_channel:addItem{
title=result.title,
link=argent.config.site_address..parent..output_name,
description = result.description,
pubDate = result.date,
}
end
end
end
end
function process_dir(directory, parent)
if should_ignore(directory, parent) then
argent.log('debug', fmt('will not process directory %q', parent..directory))
return
end
if not directory_exists(directory, parent) then
argent.createDirectory(argent.config.output_directory..parent..directory)
end
process(directory, parent)
end
function process(directory, parent)
local directory = add_end_slash(directory) or ''
local parent = parent or ''
local dirs, files = argent.scanDirectory(argent.config.site_directory..parent..directory)
for _, file in ipairs(files) do
process_file(file, parent..directory)
end
for _, dir in ipairs(dirs) do
if dir ~= '.' and dir ~= '..' then
process_dir(dir, parent..directory)
end
end
end
--------------------------------
--
-- putting it all together
--
--------------------------------
setup(config)
if not output_directory_exists() then
argent.createDirectory(argent.config.output_directory)
else
obliterate()
end
process()
if argent.rss_channel then
argent.rss_channel:save(argent.config.output_directory..'rss.xml')
end
end