sequoia-man 0.3.2

Man page generation for Sequoia PGP CLIs
Documentation
-- Tweaks our manual pages during conversion to html by pandoc.
--
-- See https://pandoc.org/lua-filters.html#debugging-lua-filters on
-- how to debug and develop these filters.

-- Note: you must set the environment variable BINARY to the name of
-- the primary executable, e.g., 'sq' or `sq-git`.
BINARY = os.getenv("BINARY")
if BINARY == nil or BINARY == "" then
  error("BINARY environment variable should be set to the binary's name, e.g., sq")
end

-- Overwrites the title.
--
-- It just says 'sq` which looks odd.
function Meta(m)
   m.title = "Sequoia PGP Manual Pages"
   return m
end

-- Turns subcommand headers into links.
function Header (h)
   -- Given a sequence of subcommands, return the appropriate file
   -- name.
   local function subcommand_to_link (s)
      t = ""
      for i = 1, #s do
         if s[i].t == 'Space' then
            t = t .. '-'
         else
            t = t ..  s[i].text
         end
      end
      return t .. ".1.html"
   end

   if h.c[1].t == 'Str' and h.c[1].text == BINARY then
      h.c = pandoc.Link(h.c, subcommand_to_link(h.c))
      return h
   end
end

-- Transform the entries in the "SYNOPSIS" section into links.
local function rewrite_synopsis (inlines)
   -- Returns true if we're looking at a synopsis entry.
   --
   -- Matches on the following sequence:
   --
   --      [1] LineBreak {}
   --      [2] Strong {
   --        content: Inlines[7] {
   --          [1] Str "sq"
   --          [2] Space
   --          [3] Str "key"
   --          [4] Space
   --          [5] Str "userid"
   --          [6] Space
   --          [7] Str "add"
   --        }
   --      }
   --      [3] Space
   --      [4] Str "["
   local function synopsis_p (s)
      return s[1] and s[1].t == 'LineBreak'
         and s[2] and s[2].t == 'Strong'
         and s[2].c[1].text == BINARY and (not s[2].c[2] or s[2].c[2].t == 'Space')
         and s[3] and s[3].t == 'Space'
         and s[4] and s[4].t == 'Str' and s[4].text == '['
   end

   -- Given a sequence of subcommands, return the appropriate file
   -- name.
   local function subcommand_to_link (s)
      t = ""
      for i = 1, #s do
         if s[i].t == 'Space' then
            t = t .. '-'
         else
            t = t ..  s[i].text
         end
      end
      return t .. ".1.html"
   end

   for i = 1, #inlines-4 do
      if synopsis_p(table.pack(table.unpack(inlines, i, i + 3))) then
         inlines[i+1] = pandoc.Link(inlines[i+1], subcommand_to_link(inlines[i+1].c))
      end
   end
end

-- Transforms the entries in the "SEE ALSO" section into links.
local function rewrite_see_also (inlines)
   -- Returns true if we're looking at a see-also entry.
   --
   -- Matches on the following sequence:
   --
   --      [43] Strong {
   --        content: Inlines[1] {
   --          [1] Str "sq-wkd"
   --        }
   --      }
   --      [44] Str "(1),"
   local function see_also_p (a, b)
      return a and a.t == 'Strong'
         and b and b.t == 'Str' and string.sub(b.text, 1, 3) == '(1)'
   end

   for i = 1, #inlines-1 do
      if see_also_p(inlines[i], inlines[i+1]) then
         inlines[i] = pandoc.Link(inlines[i], inlines[i].c[1].text .. ".1.html")
      end
   end
end

-- Transforms text enclosed in backticks into inline code or links.
local function rewrite_inline_code (inlines)
   -- Returns the length of the token stream containing quoted text
   -- starting from the first token, or nil if no quoted text is
   -- found.
   local function inline_quoted (s)
      if s[1] and s[1].t == 'Str' and string.sub(s[1].text, 1, 1) == "`" then
         -- Find the end, i.e. the first token containing the closing
         -- tick (which may be the first token).
         for i = 1, #s do
            if s[i] and s[i].t == 'Str' and string.find(s[i].text, "`", 2) then
               return i
            end
         end
      end
   end

   -- Given a sequence of tokens, returns the text representation.
   local function tokens_to_text (s)
      t = ""
      for i = 1, #s do
         if s[i].t == 'Space' then
            t = t .. ' '
         else
            t = t ..  s[i].text
         end
      end
      return t
   end

   -- Given a string subcommand, return the appropriate file
   -- name.
   local function subcommand_to_link (s)
      return string.gsub(s, " ", "-") .. ".1.html"
   end

   -- Note: as we mutate `inlines` here, we use a while loop to
   -- iterate over it.  Using a for loop doesn't cut it as `#inlines`
   -- is then evaluated only once when entering the loop for the first
   -- time.
   local i = 1
   while i <= #inlines do
      local len = inline_quoted(table.pack(table.unpack(inlines, i)))
      if len then
         -- First, flatten the matched tokens to a text.
         local text = tokens_to_text(table.pack(table.unpack(inlines, i, i+len)))

         -- Then, remove the tokens from the AST.
         for j = math.min(i+len, #inlines), i, -1 do
            inlines:remove(j)
         end

         -- We need to create a replacement node.
         local replacement

         -- See if the quoted text is a reference to a (sub)command.
         local s, e = string.find(text, "`" .. BINARY .. "[^`]*`")
         if s then
            -- If so, we turn it into a link.
            local sub = string.sub(text, s+1, e-1)
            replacement = pandoc.Link(sub, subcommand_to_link(sub))
         else
            s, e = string.find(text, "`[^`]*`")
            replacement = pandoc.Code(string.sub(text, s+1, e-1))
         end

         -- Finally, insert prefix, replacement, and suffix into the
         -- AST.
         inlines:insert(i+0, pandoc.Str(string.sub(text, 1, s-1)))
         inlines:insert(i+1, replacement)
         inlines:insert(i+2, pandoc.Str(string.sub(text, e+1)))
      end

      i = i + 1
   end
end

function Inlines (inlines)
   rewrite_synopsis(inlines)
   rewrite_see_also(inlines)
   rewrite_inline_code(inlines)
   return inlines
end

-- Turns implicit links into actual links.
function Str (s)
   s = s.text
   a, b = string.find(s, "<https://[^>]+>")
   if a and b then
      target = string.sub(s, a+1, b-1)
      return pandoc.Inlines {
         pandoc.Str(string.sub(s, 0, a)),
         pandoc.Link(target, target),
         pandoc.Str(string.sub(s, b)),
      }
   end
end