Change config format

- Change config format and parsing
- Fix support for multiple capsules
- Include scp as a transfer option
- Fix more bugs
main
mio 2022-08-05 05:09:47 +00:00
parent e1400e1ed8
commit baead4e5f1
8 changed files with 706 additions and 280 deletions

6
.gitignore vendored
View File

@ -1,4 +1,2 @@
log gemwriter
config.lua gemwriter.luastatic.c
config.ini
template.ini

View File

@ -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

85
env.lua 100644
View File

@ -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

View File

@ -1,245 +1,382 @@
local conf = require("config") local env = require("env")
local tpl = require("template") local lang = require("lang.en")
local util = require("util") local util = require("util")
local writer = {} local writer = {}
writer.docs = {}
writer.app = {
exec_name = "gw",
last_updated = "2022-07-24",
name = "gemwriter",
version = "0.1",
}
writer.conf = {} writer.conf = {}
writer.defaults = { writer.posts = {}
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.entries = {} writer.docs.gen_config = [[
Generate a default config directory.
writer.msg = { ]]
add_entry = "Created ", function writer.gen_config()
help = writer.app.exec_name .. [[ [options] [slug] writer.conf.config_dir = util.replace_shell_vars(env.defaults.config_dir)
util.make_dir(writer.conf.config_dir)
Options: for name, file in pairs(env.defaults.config_files) do
-- Check each file individually anyway to avoid overwriting existing
page [slug] Add a new page with the given name -- files
post [slug] Add a new entry with the given name if util.read_file(writer.conf.config_dir .. "/" .. file) == "" then
index Generate an index page and feed of entries if file == env.defaults.config_files.config then
publish Index and copy posts to a remote server (requires rsync) util.write_file(writer.conf.config_dir .. "/" .. file,
help Show this help message env.defaults_toml)
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]
else else
writer.conf[k] = config[k] util.write_file(writer.conf.config_dir .. "/" .. file, lang[name])
end end
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 end
function writer.replace_vars(str, vars, vals) writer.docs.add_gemtext = [[
local text = str Given a title and mode, create a new gemtext file. Supported modes: page,
for k, v in pairs(vars) do post.
text = string.gsub(text, v, vals[k]) ]]
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 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 end
function writer.add_entry(slug) writer.docs.get_posts = [[
local text = tpl.log_gmi Get a list of gemlog posts and return the file names and their contents in a
text = string.gsub(text, tpl.vars.entry.date, table.
os.date(writer.conf.entry_date_format)) ]]
text = string.gsub(text, tpl.vars.entry.author, writer.conf.author) function writer.get_posts()
local ls = util.ls_grep(writer.conf.cap.gemlog_dir, writer.conf.post_ext)
local entry_name = slug .. writer.conf.entry_ext local posts = {}
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 = {}
local n = 0 local n = 0
local files_list = util.split_str(ls) local files_list = util.split_str(ls)
for f = 1, #files_list do 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 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]) } "/" .. files_list[f]) }
end end
end end
return entries return posts
end 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 meta = {}
local entries = writer.get_entries() local posts = writer.get_posts()
for e = 1, #entries do for e = 1, #posts do
meta[e] = {} meta[e] = {}
if string.find(entries[e][2], "# ") == nil then if string.find(posts[e][2], "# ") == nil then
meta[e]["title"] = entries[e][1] meta[e]["title"] = posts[e][1]
meta[e]["content"] = entries[e][2] meta[e]["content"] = posts[e][2]
else else
meta[e]["title"] = util.extract_str(entries[e][2], "# ", "\n") meta[e]["title"] = util.extract_str(posts[e][2], "# ", "\n")
hi1, hi2 = string.find(entries[e][2], "# ") hi1, hi2 = string.find(posts[e][2], "# ")
meta[e]["content"] = string.sub(entries[e][2], hi1, meta[e]["content"] = string.sub(posts[e][2], hi1,
string.len(entries[e][2])) string.len(posts[e][2]))
end end
if string.find(entries[e][2], "author: ") == nil then if string.find(posts[e][2], "author: ") == nil then
meta[e]["author"] = writer.conf.author meta[e]["author"] = writer.conf.cap.author
else else
meta[e]["author"] = util.extract_str(entries[e][2], "author: ", "\n") meta[e]["author"] = util.extract_str(posts[e][2], "author: ", "\n")
end end
if string.find(entries[e][2], "date: ") == nil then if string.find(posts[e][2], "date: ") == nil then
meta[e]["date"] = util.extract_file_date(writer.conf.output_dir, meta[e]["date"] = util.extract_file_date(writer.conf.cap.gemlog_dir,
entries[e][1]) posts[e][1])
if (meta[e]["date"] == "") or (meta[e]["date"] == nil) then 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 end
else else
meta[e]["date"] = util.extract_str(entries[e][2], "date: ", "\n") meta[e]["date"] = util.extract_str(posts[e][2], "date: ", "\n")
end 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"] meta[e]["summary"] = meta[e]["title"]
else else
meta[e]["summary"] = util.extract_str(entries[e][2], "summary: ", "\n") meta[e]["summary"] = util.extract_str(posts[e][2], "summary: ", "\n")
end end
if string.find(entries[e][2], "tags: ") == nil then if string.find(posts[e][2], "tags: ") == nil then
meta[e]["tags"] = "" meta[e]["tags"] = ""
else else
meta[e]["tags"] = util.extract_str(entries[e][2], "tags: ", "\n") meta[e]["tags"] = util.extract_str(posts[e][2], "tags: ", "\n")
end end
meta[e]["url"] = writer.conf.url .. "/" .. entries[e][1] meta[e]["url"] = writer.conf.cap.log_url .. "/" .. posts[e][1]
end end
return meta return meta
end end
writer.docs.gen_index_page = [[
Generate a gemlog index page listing all gemlog posts.
]]
function writer.gen_index_page() function writer.gen_index_page()
local index_text = writer.replace_vars(tpl.index_gmi, tpl.vars.log, local index_text = util.replace_vars(lang.index, lang.tpl_vars.log,
writer.conf) writer.conf.cap)
local entries_text = "" local posts_text = ""
-- Reverse insert links to log entries, newest first -- Reverse insert links to log posts, newest first
for e = #writer.entries, 1, -1 do for e = #writer.posts, 1, -1 do
local entry_date = util.split_str(writer.entries[e]["date"], "(.*)[T]")[1] local post_date = util.split_str(writer.posts[e]["date"], "(.*)[T]")[1]
-- Get entry filename from the url -- If post date cannot be extracted, revert to current date
local fi1, fi2 = string.find(writer.entries[e]["url"], if post_date == nil then
writer.conf.entry_file_pattern) post_date = os.date(writer.conf.post_date_format)
entries_text = entries_text .. "\n=> " ..
string.sub(writer.entries[e]["url"], fi1 + 1, fi2) .. " " ..
entry_date .. " " .. writer.entries[e]["title"]
end end
index_text = string.gsub(index_text, tpl.vars.index.entries, entries_text) -- Get post filename from the url
util.write_file(writer.conf.output_dir .. "/" .. writer.conf.index_file, local fi1, fi2 = string.find(writer.posts[e]["url"],
index_text) 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 end
writer.docs.gen_atom_feed = [[
Generate an Atom feed of gemlog posts.
]]
function writer.gen_atom_feed() function writer.gen_atom_feed()
local feed_meta = { local feed_meta = {
date = os.date(writer.conf.entry_date_format), date = os.date(writer.conf.post_date_format),
url = writer.conf.url .. "/" .. writer.conf.atom_file, url = writer.conf.cap.url .. "/" .. writer.conf.cap.atom_feed,
} }
local feed_text = writer.replace_vars(tpl.atom_header, tpl.vars.log, local feed_text = util.replace_vars(lang.atom_header, lang.tpl_vars.log,
writer.conf) writer.conf.cap)
feed_text = writer.replace_vars(feed_text, tpl.vars.feed, feed_meta) feed_text = util.replace_vars(feed_text, lang.tpl_vars.feed, feed_meta)
local feed_entry = "" local feed_post = ""
-- Reverse insert log entries, newest first -- Reverse insert log posts, newest first
for e = #writer.entries, 1, -1 do for e = #writer.posts, 1, -1 do
feed_entry = tpl.atom_entry feed_post = lang.atom_entry
feed_entry = writer.replace_vars(feed_entry, tpl.vars.entry, -- Escape html entities in post contents. "%" is also a special character
writer.entries[e]) -- for string.gsub() and similar functions, and will cause an error if not
feed_text = feed_text .. feed_entry -- 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 end
feed_text = feed_text .. tpl.atom_footer feed_text = feed_text .. lang.atom_footer
util.write_file(writer.conf.output_dir .. "/" .. writer.conf.atom_file, util.write_file(writer.conf.cap.gemlog_dir .. "/" ..
feed_text) writer.conf.cap.atom_feed, feed_text)
end end
writer.docs.publish = [[
Transfer a capsule's contents to a remote location.
]]
function writer.publish() function writer.publish()
os.execute(writer.conf.rsync_exec .. " " .. writer.conf.rsync_options .. if writer.conf.cap.transfer_mode == "rsync" then
" " .. writer.conf.output_dir .. " " .. writer.conf.rsync_remote) 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 end
local cli = {} 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) function cli.handle_args(args)
if (args[1] ~= "help") and (args[1] ~= "version") then if (args[1] ~= lang.opts.help) and (args[1] ~= lang.opts.version) then
writer.load_config(conf) writer.load_config(args[2])
end end
if (args[1] == "index") or (args[1] == "publish") then if (args[1] == lang.opts.index) or (args[1] == lang.opts.publish) then
writer.entries = writer.get_entries_meta() writer.posts = writer.get_posts_meta()
if writer.conf.gen_index_page then writer.gen_index_page() end if writer.conf.cap.gen_index_page then writer.gen_index_page() end
if writer.conf.gen_atom_feed then writer.gen_atom_feed() end if writer.conf.cap.gen_atom_feed then writer.gen_atom_feed() end
end end
if args[1] == "post" then if args[1] == lang.opts.config then
local slug = args[2] writer.load_config()
if slug == "" then print(lang.msgs.load_config .. writer.conf.config_dir)
slug = writer.conf.entry_slug
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 else
slug = os.date("%Y-%m-%d") .. "-" .. slug file_name = writer.add_gemtext(args[2], args[1])
end end
writer.add_entry(slug) print(lang.msgs.add_gemtext .. file_name)
print(writer.msg.add_entry .. slug .. writer.conf.entry_ext)
elseif args[1] == "page" then elseif args[1] == lang.opts.index then
local slug = args[2] print(lang.msgs.index)
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] == "index" then elseif args[1] == lang.opts.publish then
print(writer.msg.index)
elseif args[1] == "publish" then
writer.publish() writer.publish()
print(writer.msg.publish) print(lang.msgs.publish)
elseif args[1] == "help" then elseif args[1] == lang.opts.help then
print(writer.msg.help) print(env.app.exec_name .. lang.msgs.help)
elseif args[1] == "version" then elseif args[1] == lang.opts.version then
print(writer.app.name .. " " .. writer.app.version .. print(env.app.name .. " " .. env.app.version ..
" (" .. writer.app.last_updated .. ")") " (" .. env.app.last_updated .. ")")
end end
end end

145
lang/en.lua 100644
View File

@ -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

63
readme.md 100644
View File

@ -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

View File

@ -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
View File

@ -12,10 +12,114 @@ function util.split_str(str, sep)
end 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) function util.extract_str(full_str, find_str, end_str)
fi1, fi2 = string.find(full_str, find_str, 1) local fi1, fi2 = string.find(full_str, find_str, 1)
ei1, ei2 = string.find(full_str, end_str, fi2) 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) 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 = { "%%", "&#37;" },
less_than = { "<", "&#60;" },
greater_than = { ">", "&#62;" },
left_bracket = { "%[", "&#91;" },
right_bracket = { "%]", "&#93;" },
}
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 end
@ -30,18 +134,18 @@ end
function util.read_file(file) function util.read_file(file)
local fh = io.open(file, "r") local fh = io.open(file, "r")
local text = "" local text = ""
io.input(fh) if fh ~= nil then
text = io.read("*a") text = fh:read("*a")
io.close(fh) fh:close()
end
return text return text
end end
function util.write_file(file, str) function util.write_file(file, str)
local fh = io.open(file, "w") local fh = io.open(file, "w")
io.output(fh) fh:write(str)
io.write(str) fh:close()
io.close(fh)
end end
return util return util