diff --git a/.gitignore b/.gitignore index 87b605f..6268f27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -log -config.lua -config.ini -template.ini +gemwriter +gemwriter.luastatic.c diff --git a/config.sample.lua b/config.sample.lua deleted file mode 100644 index f76e33c..0000000 --- a/config.sample.lua +++ /dev/null @@ -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 diff --git a/env.lua b/env.lua new file mode 100644 index 0000000..3a3d08a --- /dev/null +++ b/env.lua @@ -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 diff --git a/gemwriter.lua b/gemwriter.lua index 1fe80dd..5e0d2c2 100644 --- a/gemwriter.lua +++ b/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] - else - writer.conf[k] = config[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 + 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 -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.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 - return text + + -- 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 -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) +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 - 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) + -- 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.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.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 + 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 + + +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 + -- 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, tpl.vars.index.entries, entries_text) - util.write_file(writer.conf.output_dir .. "/" .. writer.conf.index_file, - index_text) + 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 diff --git a/lang/en.lua b/lang/en.lua new file mode 100644 index 0000000..745f421 --- /dev/null +++ b/lang/en.lua @@ -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 = [[ + + {{ log_url }} + {{ log_title }} + {{ log_subtitle }} + {{ feed_date }} + + {{ log_author }} + + + +]] + +en.atom_entry = [[ + + {{ post_url }} + + <![CDATA[{{ post_title }}]] .. "]]>" .. [[ + + + {{ post_date }} + + {{ post_author }} + + + + " .. [[ + + + + " .. [[ + + + +]] + +en.atom_footer = [[]] + +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 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..45a2dfd --- /dev/null +++ b/readme.md @@ -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 diff --git a/template.lua b/template.lua deleted file mode 100644 index f003cf4..0000000 --- a/template.lua +++ /dev/null @@ -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 = [[ - - {{ log_url }} - {{ log_title }} - {{ log_subtitle }} - {{ feed_date }} - - {{ log_author }} - - - ]] - -tpl.atom_entry = [[ - {{ entry_url }} - - <![CDATA[{{ entry_title }}]] .. "]]>" .. [[ - - - {{ entry_date }} - - {{ entry_author }} - - - - " .. [[ - - - - " .. [[ - - - -]] - -tpl.atom_footer = [[]] - -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 diff --git a/util.lua b/util.lua index bc27496..4fae930 100644 --- a/util.lua +++ b/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) - return string.sub(full_str, fi2 + 1, ei1 - 1) + 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