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
config.lua
config.ini
template.ini
gemwriter
gemwriter.luastatic.c

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

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
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 = { "%%", "&#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
@ -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