Change config format
- Change config format and parsing - Fix support for multiple capsules - Include scp as a transfer option - Fix more bugspull/1/head
parent
e1400e1ed8
commit
baead4e5f1
|
@ -1,4 +1,2 @@
|
|||
log
|
||||
config.lua
|
||||
config.ini
|
||||
template.ini
|
||||
gemwriter
|
||||
gemwriter.luastatic.c
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
local config = {
|
||||
author = "Gem",
|
||||
title = "gemlog",
|
||||
subtitle = "a Gemini log",
|
||||
url = "gemini://",
|
||||
output_dir = "/path/to/local/log",
|
||||
rsync_remote = "/path/to/remote/log",
|
||||
}
|
||||
|
||||
return config
|
|
@ -0,0 +1,85 @@
|
|||
local env = {}
|
||||
|
||||
-- App information
|
||||
env.app = {
|
||||
name = "gemwriter",
|
||||
exec_name = "gemwriter",
|
||||
version = "0.2",
|
||||
last_updated = "2022-08-05",
|
||||
}
|
||||
|
||||
env.defaults = {
|
||||
config_dir = "$HOME/.config/" .. env.app.exec_name,
|
||||
config_files = {
|
||||
atom_entry = "atom.entry.xml",
|
||||
atom_header = "atom.header.xml",
|
||||
config = "config.toml",
|
||||
index = "index.gmi",
|
||||
page = "page.gmi",
|
||||
post = "post.gmi",
|
||||
},
|
||||
post_slug = "untitled",
|
||||
post_slug_date_format = "%Y-%m-%d",
|
||||
-- Atom feed date format
|
||||
post_date_format = "%Y-%m-%dT%H:%M:%SZ",
|
||||
post_ext = ".gmi",
|
||||
post_file_pattern = "[%/](%d%d%d%d%-%d%d%-%d%d)(.*)[%.](%a*)",
|
||||
page_slug = os.date("%Y%m%d-%H%M%S"),
|
||||
}
|
||||
|
||||
-- Configurable settings
|
||||
env.toml_vars = {
|
||||
general = "general",
|
||||
capsules = "capsules",
|
||||
}
|
||||
env.general = {
|
||||
app_lang = "en",
|
||||
main_capsule = "main",
|
||||
}
|
||||
env.capsules = {
|
||||
main = {
|
||||
author = "Gem",
|
||||
title = "Gemlog",
|
||||
subtitle = "A Gemini log",
|
||||
url = "gemini://domain.tld",
|
||||
log_url = "gemini://domain.tld/log",
|
||||
capsule_dir = env.defaults.config_dir .. "/capsule",
|
||||
gemlog_dir = env.defaults.config_dir .. "/capsule/log",
|
||||
gen_index_page = true,
|
||||
index_page = "index.gmi",
|
||||
gen_atom_feed = true,
|
||||
atom_feed = "atom.xml",
|
||||
transfer_mode = "scp",
|
||||
scp_exec = "/usr/bin/scp",
|
||||
scp_target = "/path/to/remote/capsule",
|
||||
rsync_exec = "/usr/bin/rsync",
|
||||
rsync_options = "-avz",
|
||||
rsync_dest = "/path/to/remote/capsule",
|
||||
},
|
||||
}
|
||||
env.defaults_toml = [[
|
||||
[]] .. env.toml_vars.general .. [[]
|
||||
app_lang = "]] .. env.general.app_lang .. [["
|
||||
main_capsule = "]] .. env.general.main_capsule .. [["
|
||||
|
||||
[]] .. env.toml_vars.capsules .. "." .. env.general.main_capsule .. [[]
|
||||
author = "]] .. env.capsules.main.author .. [["
|
||||
title = "]] .. env.capsules.main.title .. [["
|
||||
subtitle = "]] .. env.capsules.main.subtitle .. [["
|
||||
url = "]] .. env.capsules.main.url .. [["
|
||||
log_url = "]] .. env.capsules.main.log_url .. [["
|
||||
capsule_dir = "]] .. env.capsules.main.capsule_dir .. [["
|
||||
gemlog_dir = "]] .. env.capsules.main.gemlog_dir .. [["
|
||||
gen_index_page = "]] .. tostring(env.capsules.main.gen_index_page) .. [["
|
||||
index_page = "]] .. env.capsules.main.index_page .. [["
|
||||
gen_atom_feed = "]] .. tostring(env.capsules.main.gen_atom_feed) .. [["
|
||||
atom_feed = "]] .. env.capsules.main.atom_feed .. [["
|
||||
transfer_mode = "]] .. env.capsules.main.transfer_mode .. [["
|
||||
scp_exec = "]] .. env.capsules.main.scp_exec .. [["
|
||||
scp_target = "]] .. env.capsules.main.scp_target .. [["
|
||||
rsync_exec = "]] .. env.capsules.main.rsync_exec .. [["
|
||||
rsync_options = "]] .. env.capsules.main.rsync_options .. [["
|
||||
rsync_dest = "]] .. env.capsules.main.rsync_dest .. [["
|
||||
]]
|
||||
|
||||
return env
|
465
gemwriter.lua
465
gemwriter.lua
|
@ -1,245 +1,382 @@
|
|||
local conf = require("config")
|
||||
local tpl = require("template")
|
||||
local env = require("env")
|
||||
local lang = require("lang.en")
|
||||
local util = require("util")
|
||||
|
||||
|
||||
local writer = {}
|
||||
|
||||
writer.app = {
|
||||
exec_name = "gw",
|
||||
last_updated = "2022-07-24",
|
||||
name = "gemwriter",
|
||||
version = "0.1",
|
||||
}
|
||||
|
||||
writer.docs = {}
|
||||
writer.conf = {}
|
||||
writer.defaults = {
|
||||
atom_file = "atom.xml",
|
||||
author = "Gem",
|
||||
-- Atom feed date format
|
||||
entry_date_format = "%Y-%m-%dT%X+00:00",
|
||||
entry_ext = ".gmi",
|
||||
entry_slug = os.date("%Y-%m-%d-%H%M%S"),
|
||||
entry_file_pattern = "[%/](%d%d%d%d%-%d%d%-%d%d)(.*)[%.](%a*)",
|
||||
gen_atom_feed = true,
|
||||
gen_index_page = true,
|
||||
index_file = "index.gmi",
|
||||
page_slug = "untitled-" .. os.date("%Y%m%d%H%M%S"),
|
||||
title = "gemlog",
|
||||
subtitle = "a Gemini log",
|
||||
url = "",
|
||||
output_dir = "log",
|
||||
rsync_exec = "/usr/bin/rsync",
|
||||
rsync_options = "-avz",
|
||||
rsync_remote = "",
|
||||
}
|
||||
writer.posts = {}
|
||||
|
||||
writer.entries = {}
|
||||
|
||||
writer.msg = {
|
||||
add_entry = "Created ",
|
||||
help = writer.app.exec_name .. [[ [options] [slug]
|
||||
|
||||
Options:
|
||||
|
||||
page [slug] Add a new page with the given name
|
||||
post [slug] Add a new entry with the given name
|
||||
index Generate an index page and feed of entries
|
||||
publish Index and copy posts to a remote server (requires rsync)
|
||||
help Show this help message
|
||||
version Print version info
|
||||
]],
|
||||
index = "Created index page and feed.",
|
||||
publish = "Published log.",
|
||||
}
|
||||
|
||||
|
||||
function writer.load_config(config)
|
||||
for k, v in pairs(writer.defaults) do
|
||||
if config[k] == nil then
|
||||
writer.conf[k] = writer.defaults[k]
|
||||
writer.docs.gen_config = [[
|
||||
Generate a default config directory.
|
||||
]]
|
||||
function writer.gen_config()
|
||||
writer.conf.config_dir = util.replace_shell_vars(env.defaults.config_dir)
|
||||
util.make_dir(writer.conf.config_dir)
|
||||
for name, file in pairs(env.defaults.config_files) do
|
||||
-- Check each file individually anyway to avoid overwriting existing
|
||||
-- files
|
||||
if util.read_file(writer.conf.config_dir .. "/" .. file) == "" then
|
||||
if file == env.defaults.config_files.config then
|
||||
util.write_file(writer.conf.config_dir .. "/" .. file,
|
||||
env.defaults_toml)
|
||||
else
|
||||
writer.conf[k] = config[k]
|
||||
util.write_file(writer.conf.config_dir .. "/" .. file, lang[name])
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Create capsule and gemlog directories
|
||||
util.make_dir(env.capsules.main.capsule_dir)
|
||||
util.make_dir(env.capsules.main.gemlog_dir)
|
||||
end
|
||||
|
||||
|
||||
writer.docs.parse_config = [[
|
||||
Read a config file in TOML format and load the values into a table.
|
||||
]]
|
||||
function writer.parse_config(config_file)
|
||||
local lines = util.split_lines(config_file)
|
||||
local config_group = env.toml_vars.general
|
||||
local line_cap = ""
|
||||
writer.conf.capsules = {}
|
||||
for l = 1, #lines do
|
||||
-- Config group
|
||||
if string.sub(lines[l], 1, 1) == "[" then
|
||||
for tv, label in pairs(env.toml_vars) do
|
||||
if string.find(lines[l], label) ~= nil then config_group = label end
|
||||
end
|
||||
if config_group == env.toml_vars.capsules then
|
||||
local _, lb_sep = string.find(lines[l], "%[" .. config_group .. "%.")
|
||||
local rb_sep, _ = string.find(lines[l], "%]")
|
||||
if lb_sep ~= nil then
|
||||
line_cap = string.sub(lines[l], lb_sep + 1, rb_sep - 1)
|
||||
writer.conf.capsules[line_cap] = {}
|
||||
end
|
||||
end
|
||||
|
||||
-- Config variables (ignore comments)
|
||||
elseif (string.sub(lines[l], 1, 1) ~= "#") and
|
||||
(string.find(lines[l], "=") ~= nil) then
|
||||
local eq_sep, _ = string.find(lines[l], " = ")
|
||||
local qt_sep, _ = string.find(lines[l], "\"")
|
||||
local k = string.sub(lines[l], 1, eq_sep - 1)
|
||||
local val = string.sub(lines[l], eq_sep + 4, string.len(lines[l]) - 1)
|
||||
if qt_sep == nil then
|
||||
val = string.sub(lines[l], eq_sep + 3, string.len(lines[l]))
|
||||
end
|
||||
-- Load settings by key and their values to the config table
|
||||
if config_group == env.toml_vars.general then
|
||||
writer.conf[k] = val
|
||||
elseif config_group == env.toml_vars.capsules then
|
||||
-- Convert boolean values
|
||||
if (val == "\"true\"") or (val == "\"false\"") then
|
||||
writer.conf.capsules[line_cap][k] = util.to_bool(val)
|
||||
else
|
||||
writer.conf.capsules[line_cap][k] = val
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If the main capsule id is not the app default, update the id
|
||||
if (writer.conf.cap_id == env.general.main_capsule) and
|
||||
(writer.conf.main_capsule ~= env.general.main_capsule) then
|
||||
writer.conf.cap_id = writer.conf.main_capsule
|
||||
end
|
||||
-- Add an alias to the selected capsule's table
|
||||
writer.conf.cap = writer.conf.capsules[writer.conf.cap_id]
|
||||
|
||||
-- Fill in the remaining defaults
|
||||
for k, v in pairs(env.defaults) do
|
||||
if writer.conf[k] == nil then
|
||||
writer.conf[k] = env.defaults[k]
|
||||
end
|
||||
end
|
||||
|
||||
-- Replace shell variables in paths to make them usable
|
||||
for c_id, _ in pairs(writer.conf.capsules) do
|
||||
writer.conf.capsules[c_id]["capsule_dir"] = util.replace_shell_vars(
|
||||
writer.conf.capsules[c_id]["capsule_dir"])
|
||||
writer.conf.capsules[c_id]["gemlog_dir"] = util.replace_shell_vars(
|
||||
writer.conf.capsules[c_id]["gemlog_dir"])
|
||||
-- Create capsule and gemlog directories
|
||||
util.make_dir(writer.conf.capsules[c_id]["capsule_dir"])
|
||||
util.make_dir(writer.conf.capsules[c_id]["gemlog_dir"])
|
||||
end
|
||||
|
||||
-- Set lang
|
||||
lang = require("lang." .. writer.conf.app_lang)
|
||||
end
|
||||
|
||||
|
||||
writer.docs.load_config = [[
|
||||
Check whether there is an existing config and load it if found. Exit if a
|
||||
capsule is not found.
|
||||
]]
|
||||
function writer.load_config(cap_id)
|
||||
writer.conf.config_dir = util.replace_shell_vars(env.defaults.config_dir)
|
||||
local config_file = util.read_file(writer.conf.config_dir .. "/" ..
|
||||
env.defaults.config_files.config)
|
||||
local config_new = false
|
||||
|
||||
-- No config found
|
||||
if (config_file == "") and (not config_new) then
|
||||
writer.gen_config()
|
||||
config_new = true
|
||||
end
|
||||
|
||||
-- Config found, read the configuration and load the values to an
|
||||
-- associative table, writer.conf.
|
||||
if (config_file ~= "") and (not config_new) then
|
||||
-- Check capsule id exists, abort if no valid capsule found
|
||||
if (cap_id == nil) or (string.find(cap_id, " ") ~= nil) then
|
||||
writer.conf.cap_id = env.general.main_capsule
|
||||
else
|
||||
writer.conf.cap_id = cap_id
|
||||
end
|
||||
local cap_label = "%[" .. env.toml_vars.capsules .. "%." ..
|
||||
tostring(cap_id) .. "%]"
|
||||
if (string.find(config_file, cap_label) == nil) and
|
||||
(writer.conf.cap_id ~= env.general.main_capsule) then
|
||||
print(lang.errs.invalid_cap_id)
|
||||
os.exit()
|
||||
end
|
||||
|
||||
writer.parse_config(config_file)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function writer.replace_vars(str, vars, vals)
|
||||
local text = str
|
||||
for k, v in pairs(vars) do
|
||||
text = string.gsub(text, v, vals[k])
|
||||
writer.docs.add_gemtext = [[
|
||||
Given a title and mode, create a new gemtext file. Supported modes: page,
|
||||
post.
|
||||
]]
|
||||
function writer.add_gemtext(title, mode)
|
||||
local file_name = title
|
||||
local post_title = title
|
||||
if ((title == "") or (title == nil)) and (mode == "post") then
|
||||
file_name = os.date(writer.conf.post_slug_date_format) .. "-" ..
|
||||
writer.conf.post_slug .. writer.conf.post_ext
|
||||
post_title = writer.conf.post_slug
|
||||
elseif ((title == "") or (title == nil)) and (mode == "page") then
|
||||
file_name = writer.conf.page_slug .. writer.conf.post_ext
|
||||
post_title = writer.conf.page_slug
|
||||
else
|
||||
-- Strip punctuation from the title and convert to file name
|
||||
local delim = "delim-" .. tostring(math.random(90000000000))
|
||||
file_name = string.gsub(file_name, " ", delim)
|
||||
if mode == "post" then
|
||||
file_name = os.date(writer.conf.post_slug_date_format) .. "-" ..
|
||||
string.gsub(file_name, "%p", "") .. writer.conf.post_ext
|
||||
else
|
||||
file_name = string.gsub(file_name, "%p", "") .. writer.conf.post_ext
|
||||
end
|
||||
return text
|
||||
file_name = string.lower(string.gsub(file_name, delim, "-"))
|
||||
end
|
||||
|
||||
local text = lang.post
|
||||
local file_path = writer.conf.cap.gemlog_dir .. "/" .. file_name
|
||||
if mode == "page" then
|
||||
text = lang.page
|
||||
file_path = writer.conf.cap.capsule_dir .. "/" .. file_name
|
||||
end
|
||||
text = string.gsub(text, lang.tpl_vars.post.date,
|
||||
os.date(writer.conf.post_date_format))
|
||||
text = string.gsub(text, lang.tpl_vars.post.author, writer.conf.cap.author)
|
||||
text = string.gsub(text, lang.tpl_vars.post.title, post_title)
|
||||
|
||||
util.write_file(file_path, text)
|
||||
return file_name
|
||||
end
|
||||
|
||||
|
||||
function writer.add_entry(slug)
|
||||
local text = tpl.log_gmi
|
||||
text = string.gsub(text, tpl.vars.entry.date,
|
||||
os.date(writer.conf.entry_date_format))
|
||||
text = string.gsub(text, tpl.vars.entry.author, writer.conf.author)
|
||||
|
||||
local entry_name = slug .. writer.conf.entry_ext
|
||||
os.execute("test -d " .. writer.conf.output_dir .. " || mkdir -p " ..
|
||||
writer.conf.output_dir)
|
||||
util.write_file(writer.conf.output_dir .. "/" .. entry_name, text)
|
||||
end
|
||||
|
||||
|
||||
function writer.get_entries()
|
||||
local ls_cmd = io.popen("ls " .. writer.conf.output_dir .. " | grep " ..
|
||||
writer.conf.entry_ext)
|
||||
local ls = ls_cmd:read("*a")
|
||||
local entries = {}
|
||||
writer.docs.get_posts = [[
|
||||
Get a list of gemlog posts and return the file names and their contents in a
|
||||
table.
|
||||
]]
|
||||
function writer.get_posts()
|
||||
local ls = util.ls_grep(writer.conf.cap.gemlog_dir, writer.conf.post_ext)
|
||||
local posts = {}
|
||||
local n = 0
|
||||
local files_list = util.split_str(ls)
|
||||
for f = 1, #files_list do
|
||||
if files_list[f] ~= writer.conf.index_file then
|
||||
if files_list[f] ~= writer.conf.cap.index_page then
|
||||
n = n + 1
|
||||
entries[n] = { files_list[f], util.read_file(writer.conf.output_dir ..
|
||||
posts[n] = { files_list[f], util.read_file(writer.conf.cap.gemlog_dir ..
|
||||
"/" .. files_list[f]) }
|
||||
end
|
||||
end
|
||||
return entries
|
||||
return posts
|
||||
end
|
||||
|
||||
|
||||
function writer.get_entries_meta()
|
||||
writer.docs.get_posts_meta = [[
|
||||
Extract gemlog posts metadata and return them as an associative table.
|
||||
]]
|
||||
function writer.get_posts_meta()
|
||||
local meta = {}
|
||||
local entries = writer.get_entries()
|
||||
for e = 1, #entries do
|
||||
local posts = writer.get_posts()
|
||||
for e = 1, #posts do
|
||||
meta[e] = {}
|
||||
if string.find(entries[e][2], "# ") == nil then
|
||||
meta[e]["title"] = entries[e][1]
|
||||
meta[e]["content"] = entries[e][2]
|
||||
if string.find(posts[e][2], "# ") == nil then
|
||||
meta[e]["title"] = posts[e][1]
|
||||
meta[e]["content"] = posts[e][2]
|
||||
else
|
||||
meta[e]["title"] = util.extract_str(entries[e][2], "# ", "\n")
|
||||
hi1, hi2 = string.find(entries[e][2], "# ")
|
||||
meta[e]["content"] = string.sub(entries[e][2], hi1,
|
||||
string.len(entries[e][2]))
|
||||
meta[e]["title"] = util.extract_str(posts[e][2], "# ", "\n")
|
||||
hi1, hi2 = string.find(posts[e][2], "# ")
|
||||
meta[e]["content"] = string.sub(posts[e][2], hi1,
|
||||
string.len(posts[e][2]))
|
||||
end
|
||||
if string.find(entries[e][2], "author: ") == nil then
|
||||
meta[e]["author"] = writer.conf.author
|
||||
if string.find(posts[e][2], "author: ") == nil then
|
||||
meta[e]["author"] = writer.conf.cap.author
|
||||
else
|
||||
meta[e]["author"] = util.extract_str(entries[e][2], "author: ", "\n")
|
||||
meta[e]["author"] = util.extract_str(posts[e][2], "author: ", "\n")
|
||||
end
|
||||
if string.find(entries[e][2], "date: ") == nil then
|
||||
meta[e]["date"] = util.extract_file_date(writer.conf.output_dir,
|
||||
entries[e][1])
|
||||
if string.find(posts[e][2], "date: ") == nil then
|
||||
meta[e]["date"] = util.extract_file_date(writer.conf.cap.gemlog_dir,
|
||||
posts[e][1])
|
||||
if (meta[e]["date"] == "") or (meta[e]["date"] == nil) then
|
||||
meta[e]["date"] = os.date(writer.conf.entry_date_format)
|
||||
meta[e]["date"] = os.date(writer.conf.post_date_format)
|
||||
end
|
||||
else
|
||||
meta[e]["date"] = util.extract_str(entries[e][2], "date: ", "\n")
|
||||
meta[e]["date"] = util.extract_str(posts[e][2], "date: ", "\n")
|
||||
end
|
||||
if string.find(entries[e][2], "summary: ") == nil then
|
||||
if string.find(posts[e][2], "summary: ") == nil then
|
||||
meta[e]["summary"] = meta[e]["title"]
|
||||
else
|
||||
meta[e]["summary"] = util.extract_str(entries[e][2], "summary: ", "\n")
|
||||
meta[e]["summary"] = util.extract_str(posts[e][2], "summary: ", "\n")
|
||||
end
|
||||
if string.find(entries[e][2], "tags: ") == nil then
|
||||
if string.find(posts[e][2], "tags: ") == nil then
|
||||
meta[e]["tags"] = ""
|
||||
else
|
||||
meta[e]["tags"] = util.extract_str(entries[e][2], "tags: ", "\n")
|
||||
meta[e]["tags"] = util.extract_str(posts[e][2], "tags: ", "\n")
|
||||
end
|
||||
meta[e]["url"] = writer.conf.url .. "/" .. entries[e][1]
|
||||
meta[e]["url"] = writer.conf.cap.log_url .. "/" .. posts[e][1]
|
||||
end
|
||||
return meta
|
||||
end
|
||||
|
||||
|
||||
writer.docs.gen_index_page = [[
|
||||
Generate a gemlog index page listing all gemlog posts.
|
||||
]]
|
||||
function writer.gen_index_page()
|
||||
local index_text = writer.replace_vars(tpl.index_gmi, tpl.vars.log,
|
||||
writer.conf)
|
||||
local entries_text = ""
|
||||
-- Reverse insert links to log entries, newest first
|
||||
for e = #writer.entries, 1, -1 do
|
||||
local entry_date = util.split_str(writer.entries[e]["date"], "(.*)[T]")[1]
|
||||
-- Get entry filename from the url
|
||||
local fi1, fi2 = string.find(writer.entries[e]["url"],
|
||||
writer.conf.entry_file_pattern)
|
||||
entries_text = entries_text .. "\n=> " ..
|
||||
string.sub(writer.entries[e]["url"], fi1 + 1, fi2) .. " " ..
|
||||
entry_date .. " " .. writer.entries[e]["title"]
|
||||
local index_text = util.replace_vars(lang.index, lang.tpl_vars.log,
|
||||
writer.conf.cap)
|
||||
local posts_text = ""
|
||||
-- Reverse insert links to log posts, newest first
|
||||
for e = #writer.posts, 1, -1 do
|
||||
local post_date = util.split_str(writer.posts[e]["date"], "(.*)[T]")[1]
|
||||
-- If post date cannot be extracted, revert to current date
|
||||
if post_date == nil then
|
||||
post_date = os.date(writer.conf.post_date_format)
|
||||
end
|
||||
index_text = string.gsub(index_text, tpl.vars.index.entries, entries_text)
|
||||
util.write_file(writer.conf.output_dir .. "/" .. writer.conf.index_file,
|
||||
index_text)
|
||||
-- Get post filename from the url
|
||||
local fi1, fi2 = string.find(writer.posts[e]["url"],
|
||||
writer.conf.post_file_pattern)
|
||||
posts_text = posts_text .. "\n=> " ..
|
||||
string.sub(writer.posts[e]["url"], fi1 + 1, fi2) .. " " ..
|
||||
post_date .. " " .. writer.posts[e]["title"]
|
||||
end
|
||||
index_text = string.gsub(index_text, lang.tpl_vars.index.posts,
|
||||
posts_text)
|
||||
util.write_file(writer.conf.cap.gemlog_dir .. "/" ..
|
||||
writer.conf.cap.index_page, index_text)
|
||||
end
|
||||
|
||||
|
||||
writer.docs.gen_atom_feed = [[
|
||||
Generate an Atom feed of gemlog posts.
|
||||
]]
|
||||
function writer.gen_atom_feed()
|
||||
local feed_meta = {
|
||||
date = os.date(writer.conf.entry_date_format),
|
||||
url = writer.conf.url .. "/" .. writer.conf.atom_file,
|
||||
date = os.date(writer.conf.post_date_format),
|
||||
url = writer.conf.cap.url .. "/" .. writer.conf.cap.atom_feed,
|
||||
}
|
||||
local feed_text = writer.replace_vars(tpl.atom_header, tpl.vars.log,
|
||||
writer.conf)
|
||||
feed_text = writer.replace_vars(feed_text, tpl.vars.feed, feed_meta)
|
||||
local feed_entry = ""
|
||||
-- Reverse insert log entries, newest first
|
||||
for e = #writer.entries, 1, -1 do
|
||||
feed_entry = tpl.atom_entry
|
||||
feed_entry = writer.replace_vars(feed_entry, tpl.vars.entry,
|
||||
writer.entries[e])
|
||||
feed_text = feed_text .. feed_entry
|
||||
local feed_text = util.replace_vars(lang.atom_header, lang.tpl_vars.log,
|
||||
writer.conf.cap)
|
||||
feed_text = util.replace_vars(feed_text, lang.tpl_vars.feed, feed_meta)
|
||||
local feed_post = ""
|
||||
-- Reverse insert log posts, newest first
|
||||
for e = #writer.posts, 1, -1 do
|
||||
feed_post = lang.atom_entry
|
||||
-- Escape html entities in post contents. "%" is also a special character
|
||||
-- for string.gsub() and similar functions, and will cause an error if not
|
||||
-- escaped.
|
||||
writer.posts[e]["content"] =
|
||||
util.replace_html_entities(writer.posts[e]["content"])
|
||||
feed_post = util.replace_vars(feed_post, lang.tpl_vars.post,
|
||||
writer.posts[e])
|
||||
feed_text = feed_text .. feed_post
|
||||
end
|
||||
feed_text = feed_text .. tpl.atom_footer
|
||||
util.write_file(writer.conf.output_dir .. "/" .. writer.conf.atom_file,
|
||||
feed_text)
|
||||
feed_text = feed_text .. lang.atom_footer
|
||||
util.write_file(writer.conf.cap.gemlog_dir .. "/" ..
|
||||
writer.conf.cap.atom_feed, feed_text)
|
||||
end
|
||||
|
||||
|
||||
writer.docs.publish = [[
|
||||
Transfer a capsule's contents to a remote location.
|
||||
]]
|
||||
function writer.publish()
|
||||
os.execute(writer.conf.rsync_exec .. " " .. writer.conf.rsync_options ..
|
||||
" " .. writer.conf.output_dir .. " " .. writer.conf.rsync_remote)
|
||||
if writer.conf.cap.transfer_mode == "rsync" then
|
||||
os.execute(writer.conf.cap.rsync_exec .. " " ..
|
||||
writer.conf.cap.rsync_options .. " " .. writer.conf.cap.capsule_dir ..
|
||||
"/ " .. writer.conf.cap.rsync_dest .. "/")
|
||||
else
|
||||
os.execute(writer.conf.cap.scp_exec .. " -r " ..
|
||||
writer.conf.cap.capsule_dir .. "/* " .. writer.conf.cap.scp_target)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
local cli = {}
|
||||
cli.docs = {}
|
||||
|
||||
|
||||
cli.docs.handle_args = [[
|
||||
Match command-line options to the respective application functions, passing
|
||||
any additional arguments to the functions.
|
||||
]]
|
||||
function cli.handle_args(args)
|
||||
if (args[1] ~= "help") and (args[1] ~= "version") then
|
||||
writer.load_config(conf)
|
||||
if (args[1] ~= lang.opts.help) and (args[1] ~= lang.opts.version) then
|
||||
writer.load_config(args[2])
|
||||
end
|
||||
|
||||
if (args[1] == "index") or (args[1] == "publish") then
|
||||
writer.entries = writer.get_entries_meta()
|
||||
if writer.conf.gen_index_page then writer.gen_index_page() end
|
||||
if writer.conf.gen_atom_feed then writer.gen_atom_feed() end
|
||||
if (args[1] == lang.opts.index) or (args[1] == lang.opts.publish) then
|
||||
writer.posts = writer.get_posts_meta()
|
||||
if writer.conf.cap.gen_index_page then writer.gen_index_page() end
|
||||
if writer.conf.cap.gen_atom_feed then writer.gen_atom_feed() end
|
||||
end
|
||||
|
||||
if args[1] == "post" then
|
||||
local slug = args[2]
|
||||
if slug == "" then
|
||||
slug = writer.conf.entry_slug
|
||||
if args[1] == lang.opts.config then
|
||||
writer.load_config()
|
||||
print(lang.msgs.load_config .. writer.conf.config_dir)
|
||||
|
||||
elseif (args[1] == lang.opts.post) or (args[1] == lang.opts.page) then
|
||||
local file_name = ""
|
||||
if args[3] ~= nil then
|
||||
file_name = writer.add_gemtext(args[3], args[1])
|
||||
else
|
||||
slug = os.date("%Y-%m-%d") .. "-" .. slug
|
||||
file_name = writer.add_gemtext(args[2], args[1])
|
||||
end
|
||||
writer.add_entry(slug)
|
||||
print(writer.msg.add_entry .. slug .. writer.conf.entry_ext)
|
||||
print(lang.msgs.add_gemtext .. file_name)
|
||||
|
||||
elseif args[1] == "page" then
|
||||
local slug = args[2]
|
||||
if slug == "" then slug = writer.conf.page_slug end
|
||||
writer.add_entry(slug)
|
||||
print(writer.msg.add_entry .. slug .. writer.conf.entry_ext)
|
||||
elseif args[1] == lang.opts.index then
|
||||
print(lang.msgs.index)
|
||||
|
||||
elseif args[1] == "index" then
|
||||
print(writer.msg.index)
|
||||
|
||||
elseif args[1] == "publish" then
|
||||
elseif args[1] == lang.opts.publish then
|
||||
writer.publish()
|
||||
print(writer.msg.publish)
|
||||
print(lang.msgs.publish)
|
||||
|
||||
elseif args[1] == "help" then
|
||||
print(writer.msg.help)
|
||||
elseif args[1] == lang.opts.help then
|
||||
print(env.app.exec_name .. lang.msgs.help)
|
||||
|
||||
elseif args[1] == "version" then
|
||||
print(writer.app.name .. " " .. writer.app.version ..
|
||||
" (" .. writer.app.last_updated .. ")")
|
||||
elseif args[1] == lang.opts.version then
|
||||
print(env.app.name .. " " .. env.app.version ..
|
||||
" (" .. env.app.last_updated .. ")")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
local en = {}
|
||||
|
||||
-- Templates
|
||||
en.tpl_vars = {
|
||||
post = {
|
||||
author = "{{ post_author }}",
|
||||
content = "{{ post_content }}",
|
||||
date = "{{ post_date }}",
|
||||
summary = "{{ post_summary }}",
|
||||
tags = "{{ post_tags }}",
|
||||
title = "{{ post_title }}",
|
||||
url = "{{ post_url }}",
|
||||
},
|
||||
feed = {
|
||||
date = "{{ feed_date }}",
|
||||
url = "{{ feed_url }}",
|
||||
},
|
||||
index = {
|
||||
posts = "{{ posts }}",
|
||||
},
|
||||
log = {
|
||||
author = "{{ log_author }}",
|
||||
subtitle = "{{ log_subtitle }}",
|
||||
title = "{{ log_title }}",
|
||||
log_url = "{{ log_url }}",
|
||||
},
|
||||
}
|
||||
|
||||
en.atom_header = [[<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<id>{{ log_url }}</id>
|
||||
<title>{{ log_title }}</title>
|
||||
<subtitle>{{ log_subtitle }}</subtitle>
|
||||
<updated>{{ feed_date }}</updated>
|
||||
<author>
|
||||
<name>{{ log_author }}</name>
|
||||
</author>
|
||||
<link href="{{ log_url }}" rel="alternate"/>
|
||||
<link href="{{ feed_url }}" rel="self" type="application/atom+xml"/>
|
||||
]]
|
||||
|
||||
en.atom_entry = [[
|
||||
<entry>
|
||||
<id>{{ post_url }}</id>
|
||||
<title>
|
||||
<![CDATA[{{ post_title }}]] .. "]]>" .. [[
|
||||
|
||||
</title>
|
||||
<updated>{{ post_date }}</updated>
|
||||
<author>
|
||||
<name>{{ post_author }}</name>
|
||||
</author>
|
||||
<link href="{{ post_url }}" rel="alternate"/>
|
||||
<summary>
|
||||
<![CDATA[{{ post_summary }}]] .. "]]>" .. [[
|
||||
|
||||
</summary>
|
||||
<content>
|
||||
<![CDATA[{{ post_content }}]] .. "]]>" .. [[
|
||||
|
||||
</content>
|
||||
</entry>
|
||||
]]
|
||||
|
||||
en.atom_footer = [[</feed>]]
|
||||
|
||||
en.post = [[---
|
||||
date: {{ post_date }}
|
||||
---
|
||||
|
||||
# {{ post_title }}
|
||||
|
||||
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
=> gemini:// link
|
||||
=> gemini:// link (img)
|
||||
=> https:// link (https)]]
|
||||
|
||||
en.index = [[
|
||||
# {{ log_title }}
|
||||
{{ posts }}
|
||||
]]
|
||||
|
||||
en.page = [[
|
||||
# {{ post_title }}
|
||||
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
|
||||
List:
|
||||
|
||||
*
|
||||
*
|
||||
*
|
||||
|
||||
```
|
||||
Preformatted text
|
||||
```
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
=> gemini:// link
|
||||
=> gemini:// link (img)
|
||||
=> https:// link (https)]]
|
||||
|
||||
-- App command options and messages output
|
||||
en.opts = {
|
||||
config = "config",
|
||||
page = "page",
|
||||
post = "post",
|
||||
index = "index",
|
||||
publish = "publish",
|
||||
help = "help",
|
||||
version = "version",
|
||||
}
|
||||
|
||||
en.msgs = {
|
||||
add_gemtext = "Created ",
|
||||
help = [[ [options] [capsule] [title]
|
||||
|
||||
Options:
|
||||
|
||||
config Generate a config directory
|
||||
page [capsule] [title] Add a new page with the given title
|
||||
post [capsule] [title] Add a new gemlog post with the given title
|
||||
index Generate an index page and feed of posts
|
||||
publish Index and copy posts remotely using scp
|
||||
help Show this help message
|
||||
version Print version info]],
|
||||
index = "Created index page and feed.",
|
||||
load_config = [[Created config files. Please edit them with the correct
|
||||
details before proceeding. The config files can be found at:
|
||||
]],
|
||||
publish = "Published capsule.",
|
||||
}
|
||||
|
||||
en.errs = {
|
||||
invalid_cap_id = "Error: unknown capsule id.",
|
||||
}
|
||||
|
||||
return en
|
|
@ -0,0 +1,63 @@
|
|||
# Gemwriter
|
||||
|
||||
A little command-line helper for publishing [Gemini] sites or "capsules".
|
||||
|
||||
|
||||
```
|
||||
Options:
|
||||
|
||||
config Generate a config directory
|
||||
page [capsule] [title] Add a new page with the given title
|
||||
post [capsule] [title] Add a new gemlog post with the given title
|
||||
index Generate an index page and feed of posts
|
||||
publish Index and copy posts remotely using scp
|
||||
help Show this help message
|
||||
version Print version info
|
||||
```
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
- Linux or Unix-based OS
|
||||
- [Lua] 5.4 (other versions >= 5.1 will probably be fine but are untested)
|
||||
- [scp] or [rsync], to transfer files remotely
|
||||
|
||||
|
||||
## Build
|
||||
|
||||
- Install Lua and [luastatic].
|
||||
|
||||
- Clone this repository and change into the directory. Run:
|
||||
|
||||
```
|
||||
luastatic gemwriter.lua env.lua util.lua lang/en.lua /usr/lib/liblua.so -I/usr/include -o gemwriter
|
||||
```
|
||||
|
||||
The paths to `liblua.so` and the development headers (i.e.
|
||||
`/usr/include/lua.h`) may need to be adjusted for your distribution.
|
||||
|
||||
- Move the `gemwriter` executable to a location in your `$PATH`.
|
||||
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Generate a new config: `gemwriter config`
|
||||
|
||||
2. Edit `~/.config/gemwriter/config.toml` with the correct details about your
|
||||
capsule and gemlog.
|
||||
|
||||
3. Create a new gemlog post: `gemwriter post "Hello World!"`
|
||||
|
||||
4. Publish your capsule: `gemwriter publish`
|
||||
|
||||
|
||||
## License
|
||||
|
||||
BSD-3-Clause
|
||||
|
||||
|
||||
[Gemini]: https://gemini.circumlunar.space/
|
||||
[Lua]: https://www.lua.org/
|
||||
[scp]: https://www.openssh.com/
|
||||
[rsync]: https://rsync.samba.org/
|
||||
[luastatic]: https://github.com/ers35/luastatic
|
96
template.lua
96
template.lua
|
@ -1,96 +0,0 @@
|
|||
local tpl = {}
|
||||
|
||||
tpl.vars = {
|
||||
entry = {
|
||||
author = "{{ entry_author }}",
|
||||
content = "{{ entry_content }}",
|
||||
date = "{{ entry_date }}",
|
||||
summary = "{{ entry_summary }}",
|
||||
tags = "{{ entry_tags }}",
|
||||
title = "{{ entry_title }}",
|
||||
url = "{{ entry_url }}",
|
||||
},
|
||||
feed = {
|
||||
date = "{{ feed_date }}",
|
||||
url = "{{ feed_url }}",
|
||||
},
|
||||
index = {
|
||||
entries = "{{ entries }}",
|
||||
},
|
||||
log = {
|
||||
author = "{{ log_author }}",
|
||||
subtitle = "{{ log_subtitle }}",
|
||||
title = "{{ log_title }}",
|
||||
url = "{{ log_url }}",
|
||||
},
|
||||
}
|
||||
|
||||
tpl.atom_header = [[<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<id>{{ log_url }}</id>
|
||||
<title>{{ log_title }}</title>
|
||||
<subtitle>{{ log_subtitle }}</subtitle>
|
||||
<updated>{{ feed_date }}</updated>
|
||||
<author>
|
||||
<name>{{ log_author }}</name>
|
||||
</author>
|
||||
<link href="{{ log_url }}" rel="alternate"/>
|
||||
<link href="{{ feed_url }}" rel="self" type="application/atom+xml"/>]]
|
||||
|
||||
tpl.atom_entry = [[ <entry>
|
||||
<id>{{ entry_url }}</id>
|
||||
<title>
|
||||
<![CDATA[{{ entry_title }}]] .. "]]>" .. [[
|
||||
|
||||
</title>
|
||||
<updated>{{ entry_date }}</updated>
|
||||
<author>
|
||||
<name>{{ entry_author }}</name>
|
||||
</author>
|
||||
<link href="{{ entry_url }}" rel="alternate"/>
|
||||
<summary>
|
||||
<![CDATA[{{ entry_summary }}]] .. "]]>" .. [[
|
||||
|
||||
</summary>
|
||||
<content>
|
||||
<![CDATA[{{ entry_content }}]] .. "]]>" .. [[
|
||||
|
||||
</content>
|
||||
</entry>
|
||||
]]
|
||||
|
||||
tpl.atom_footer = [[</feed>]]
|
||||
|
||||
tpl.log_gmi = [[---
|
||||
date: {{ entry_date }}
|
||||
---
|
||||
|
||||
# {{ entry_title }}
|
||||
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
|
||||
List:
|
||||
|
||||
*
|
||||
*
|
||||
*
|
||||
|
||||
```
|
||||
Preformatted text
|
||||
```
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
=> gemini:// link
|
||||
=> gemini:// link (img)
|
||||
=> https:// link (https)]]
|
||||
|
||||
tpl.index_gmi = [[
|
||||
# {{ log_title }}
|
||||
{{ entries }}
|
||||
]]
|
||||
|
||||
return tpl
|
120
util.lua
120
util.lua
|
@ -12,10 +12,114 @@ function util.split_str(str, sep)
|
|||
end
|
||||
|
||||
|
||||
function util.split_lines(str)
|
||||
local tbl = {}
|
||||
local si = 1
|
||||
for i = 1, #str do
|
||||
if string.find(string.sub(str, i, i), "\n") ~= nil then
|
||||
tbl[#tbl + 1] = string.sub(str, si, i - 1)
|
||||
si = i + 1
|
||||
end
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
|
||||
function util.extract_str(full_str, find_str, end_str)
|
||||
fi1, fi2 = string.find(full_str, find_str, 1)
|
||||
ei1, ei2 = string.find(full_str, end_str, fi2)
|
||||
local fi1, fi2 = string.find(full_str, find_str, 1)
|
||||
local ei1, ei2 = string.find(full_str, end_str, fi2)
|
||||
if end_str == "\n" then
|
||||
return string.sub(full_str, fi2 + 1, ei1 - 2)
|
||||
else
|
||||
return string.sub(full_str, fi2 + 1, ei1 - 1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function util.to_bool(str)
|
||||
local bool = { ["true"] = true, ["false"] = false }
|
||||
return bool(str)
|
||||
end
|
||||
|
||||
|
||||
function util.table_keys(tbl)
|
||||
local keys = {}
|
||||
local n = 0
|
||||
for k, v in pairs(tbl) do
|
||||
n = n + 1
|
||||
keys[n] = k
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
|
||||
function util.has_key(tbl, str)
|
||||
local keys = util.table_keys(tbl)
|
||||
for k = 1, #keys do
|
||||
if str == keys[k] then
|
||||
do return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function util.replace_vars(str, vars, vals)
|
||||
local text = str
|
||||
for k, v in pairs(vars) do
|
||||
text = string.gsub(text, v, vals[k])
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
|
||||
function util.replace_html_entities(str)
|
||||
local html_ents = {
|
||||
percent = { "%%", "%" },
|
||||
less_than = { "<", "<" },
|
||||
greater_than = { ">", ">" },
|
||||
left_bracket = { "%[", "[" },
|
||||
right_bracket = { "%]", "]" },
|
||||
}
|
||||
local text = str
|
||||
for e, c in pairs(html_ents) do
|
||||
text = string.gsub(text, c[1], c[2])
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
|
||||
function util.make_dir(dir)
|
||||
os.execute("test -d " .. dir .. " || mkdir -p " .. dir)
|
||||
end
|
||||
|
||||
|
||||
function util.ls_grep(dir, str)
|
||||
local ls_cmd = io.popen("ls " .. dir .. " | grep " .. str)
|
||||
return ls_cmd:read("*a")
|
||||
end
|
||||
|
||||
|
||||
function util.get_shell_var(str)
|
||||
local var = os.getenv(str)
|
||||
if var == "" then
|
||||
return nil
|
||||
else
|
||||
return var
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function util.replace_shell_vars(str)
|
||||
-- Replace common shell variable paths
|
||||
if string.find(str, "%$") ~= nil then
|
||||
local shell_vars = { "HOME", "USER" }
|
||||
for v = 1, #shell_vars do
|
||||
str = string.gsub(str, "$" .. shell_vars[v],
|
||||
util.get_shell_var(shell_vars[v]))
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
|
||||
|
@ -30,18 +134,18 @@ end
|
|||
function util.read_file(file)
|
||||
local fh = io.open(file, "r")
|
||||
local text = ""
|
||||
io.input(fh)
|
||||
text = io.read("*a")
|
||||
io.close(fh)
|
||||
if fh ~= nil then
|
||||
text = fh:read("*a")
|
||||
fh:close()
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
|
||||
function util.write_file(file, str)
|
||||
local fh = io.open(file, "w")
|
||||
io.output(fh)
|
||||
io.write(str)
|
||||
io.close(fh)
|
||||
fh:write(str)
|
||||
fh:close()
|
||||
end
|
||||
|
||||
return util
|
||||
|
|
Reference in New Issue