apk-tools/src/genhelp.lua

310 lines
7.4 KiB
Lua

#!/usr/bin/lua5.3
--[[
Utility to convert SCDOC manpages to apk-tools help messages
General:
- Wrangle *apk-applet*(SECTION) links
- Uppercase _underlined_ things as they are "keywords"
- Other format specs like ** to be removed
- For options text, the first sentence (within the first line) is taken as the help text
Main page: apk.8.scd
- SYNOPSIS
- COMMANDS has ## header with a table for commands list
- GLOBAL OPTIONS and COMMIT OPTIONS for option group help
- NOTES
Applet pages: apk-*.8.scd
- Take usage from SYNOPSIS, can have multiple lines like apk-version(8)
- Take DESCRIPTION, take first paragraph, rewrap, and put as section in applet specific help
- From OPTIONS take each option and it's first sentence (within the first line)
--]]
local function splittokens(s)
local res = {}
for w in s:gmatch("%S+") do
res[#res+1] = w
end
return res
end
local function textwrap(text, linewidth)
local spaceleft = linewidth
local res = {}
local line = {}
for _, word in ipairs(splittokens(text)) do
if #word + 1 > spaceleft then
table.insert(res, table.concat(line, ' '))
line = { word }
spaceleft = linewidth - #word
else
table.insert(line, word)
spaceleft = spaceleft - (#word + 1)
end
end
table.insert(res, table.concat(line, ' '))
return res
end
local function upperfirst(s)
return s:sub(1,1):upper() .. s:sub(2):lower()
end
scdoc = {
usage_prefix = "usage: ",
}
scdoc.__index = scdoc
function scdoc:nop(ln)
--print(self.section, ln)
end
function scdoc:SYNOPSIS_text(ln)
table.insert(self.usage, self.usage_prefix .. ln)
self.usage_prefix = " or: "
end
function scdoc:COMMANDS_text(ln)
local ch = ln:sub(1,1)
local a, b = ln:match("^([[|:<]*)%s+(.+)")
if ch == '|' then
self.cur_cmd = { b, "" }
table.insert(self.commands, self.cur_cmd)
elseif ch == ':' and self.cur_cmd then
self.cur_cmd[2] = b
self.cur_cmd = nil
end
end
function scdoc:COMMANDS_subsection(n)
n = n:sub(1,1) .. n:sub(2):lower()
table.insert(self.commands, n)
end
function scdoc:DESCRIPTION_text(ln)
table.insert(self.description, ln)
end
function scdoc:DESCRIPTION_paragraph()
if #self.description > 0 then
self.section_text = self.nop
end
end
function scdoc:OPTIONS_text(ln)
local ch = ln:sub(1,1)
if ch == '-' then
self.cur_opt = { ln, {} }
table.insert(self.options, self.cur_opt)
elseif ch == '\t' then
table.insert(self.cur_opt[2], ln:sub(2))
end
end
function scdoc:NOTES_text(ln)
table.insert(self.notes, ln)
end
function scdoc:parse_default(ln)
if #ln == 0 then
return (self[self.section .. "_paragraph"] or self.nop)(self)
end
s, n = ln:match("^(#*) (.*)")
if s and n then
if #s == 1 then
local optgroup, opts = n:match("^(%S*) ?(OPTIONS)$")
if opts then
if #optgroup == 0 then optgroup = self.applet end
self.options = { name = optgroup }
table.insert(self.optgroup, self.options)
n = opts
end
self.section = n
self.section_text = self[n .. "_text"] or self.nop
self.subsection = nil
else
self.subsection = n
local f = self[self.section.."_subsection"]
if f then f(self, n) end
end
return
end
-- Handle formatting
ln = ln:gsub("apk%-(%S+)%(%d%)", "%1")
ln = ln:gsub("([^\\])%*(.-[^\\])%*", "%1%2")
ln = ln:gsub("^%*(.-[^\\])%*", "%1")
ln = ln:gsub("([^\\])_(.-[^\\])_", function(a,s) return a..s:upper() end)
ln = ln:gsub("^_(.-[^\\])_", function(s) return s:upper() end)
ln = ln:gsub("\\", "")
self:section_text(ln)
end
function scdoc:parse_header(ln)
self.manpage, self.mansection = ln:match("^(%S*)%((%d*)%)")
if self.manpage:find("^apk%-") then
self.applet = self.manpage:sub(5):lower()
else
self.applet = self.manpage:upper()
end
self.parser = self.parse_default
self.section_text = self.nop
end
function scdoc:parse(fn)
self.parser = self.parse_header
for l in io.lines(fn) do
self:parser(l)
end
end
function scdoc:render_options(out, options)
local width = self.width
local nindent = 24
table.insert(out, ("%s options:\n"):format(upperfirst(options.name)))
for _, opt in ipairs(options) do
local indent = (" "):rep(nindent)
k, v = opt[1], opt[2]
if #k > nindent - 4 then
table.insert(out, (" %s\n"):format(k, "", v))
table.insert(out, indent)
else
local fmt = (" %%-%ds "):format(nindent - 4)
table.insert(out, fmt:format(k, v))
end
v = table.concat(v, " ")
local i = v:find("%.%s")
if not i then i = v:find("%.$") end
if i then v = v:sub(1, i-1) end
v = textwrap(v, width - nindent - 1)
table.insert(out, v[1])
table.insert(out, "\n")
for i = 2, #v do
table.insert(out, indent)
table.insert(out, v[i])
table.insert(out, "\n")
end
end
end
function scdoc:render_optgroups(out)
for _, options in ipairs(self.optgroup) do
if #options > 0 then
table.insert(out, options.name .. "\0")
self:render_options(out, options)
if options.name == self.applet then
self:render_footer(out)
end
table.insert(out, "\0")
end
end
end
function scdoc:render_footer(out)
table.insert(out, ("\nFor more information: man %s %s\n"):format(self.mansection, self.manpage))
end
function scdoc:render(out)
local width = self.width
if not self.applet then return end
table.insert(out, self.applet .. "\0")
table.insert(out, table.concat(self.usage, "\n"))
table.insert(out, "\n")
if #self.commands > 0 then
for _, cmd in ipairs(self.commands) do
if type(cmd) == "string" then
table.insert(out, "\n" .. cmd .. ":\n")
else
table.insert(out, (" %-10s %s\n"):format(cmd[1], cmd[2]))
end
end
elseif #self.description > 0 then
table.insert(out, "\nDescription:\n")
for _, ln in ipairs(textwrap(table.concat(self.description, ' '), width - 2)) do
table.insert(out, (" %s\n"):format(ln))
end
end
if #self.notes > 0 then
table.insert(out, "\n")
table.insert(out, table.concat(self.notes, "\n"))
if self.manpage == "apk" then self:render_footer(out)
else table.insert(out, "\n") end
end
table.insert(out, "\0")
end
local do_compress = true
local function compress(data)
if not do_compress then
return data
end
local zlib = require 'zlib'
local level = 9
if type(zlib.version()) == "string" then
-- lua-lzlib interface
return zlib.compress(data, level)
else
-- lua-zlib interface
return zlib.deflate(level)(data, "finish")
end
end
local function dump_compressed_vars(name, data, header)
local width = 16
local cout = compress(data)
if header then print(header) end
if do_compress then print("#define COMPRESSED_HELP") end
print(("static const unsigned int payload_%s_size = %d;"):format(name, #data))
print(("static const unsigned char payload_%s[] = { /* %d bytes */"):format(name, #cout))
for i = 1, #cout do
if i % width == 1 then
io.write("\t")
end
--print(cout:byte(i))
io.write(("0x%02x,"):format(cout:byte(i)))
if i % width == 0 or i == #cout then
io.write("\n")
end
end
print("};")
end
local f = {}
for _, fn in ipairs(arg) do
if fn == '--no-zlib' then
do_compress = false
else
doc = setmetatable({
width = 78,
section = "HEADER",
usage = {},
description = {},
commands = {},
notes = {},
optgroup = {},
}, scdoc)
doc:parse(fn)
table.insert(f, doc)
end
end
table.sort(f, function(a, b) return a.applet < b.applet end)
local out = {}
for _, doc in ipairs(f) do doc:render(out) end
for _, doc in ipairs(f) do doc:render_optgroups(out) end
table.insert(out, "\0")
local help = table.concat(out)
--io.stderr:write(help)
dump_compressed_vars("help", help, "/* Automatically generated by genhelp.lua. Do not modify. */")