-- Copyright 2013 by Till Tantau -- -- This file may be distributed an/or modified -- -- 1. under the LaTeX Project Public License and/or -- 2. under the GNU Public License -- -- See the file doc/generic/pgf/licenses/LICENSE for more information -- @release $Header$ --- -- This class offers functions for converting the documentation of Lua -- and other code into \TeX. -- -- @field renderers This array must consist of tables having two -- fields: |test| and |renderer|. The first must be set to a function -- that is called with an information table as parameter and must -- return |true| if the function stored in the |renderer| should be -- called. (Typically, the |test| will test whether the ``head line'' -- following a documentation block has a special form.) local DocumentParser = { renderers = {} } -- Namespace: require 'pgf.manual'.DocumentParser = DocumentParser -- Imports local keys = require 'pgf.gd.interface.InterfaceCore'.keys -- Forwards: local collect_infos, render_infos --- -- Includes the documentation stored in some file in the manual. -- -- @param filename The file from which the documentation is to be read -- @param type The type of the file. function DocumentParser.include(filename, typ) local fullname = assert(kpse.find_file(filename:gsub("%.", "/"), typ or "lua") or kpse.find_file(filename:gsub("%.", "\\"), typ or "lua"), "file " .. filename .. " not found") local file, error = io.open(fullname) -- First, let us read the file into a table to make handling easier: local lines = {} for line in file:lines() do lines [#lines + 1] = line end -- A table storing the current output. The array part contains TeX -- lines, the table part contains information about special table.s local output = {} -- Now, start the main parser loop. local i = 1 while i <= #lines do if lines[i]:match("^%-%-%-") then local infos = collect_infos (lines, i) infos.filename = filename render_infos(infos, output) i = infos.last_line end i = i + 1 end -- Render output: for _,l in ipairs(output) do if type(l) == "string" then tex.print(l) else l() end end end --- -- Add a test and a renderer to the array of renderers. -- -- @param test A test function (see |DocumentParser.renderers|). -- @param renderer A rendering function. function DocumentParser.addRenderer (test, renderer) DocumentParser.renderers [#DocumentParser.renderers + 1] = { test = test, renderer = renderer } end -- Forwards: local print_on_output, print_on_output_escape, print_docline_on_output, open_mode, close_mode, print_lines_on_output local function strip_quotes(s) if s then return string.gsub(s, '^"(.*)"$', "%1") end end local function split(s) local t = {} for line in string.gmatch(s, ".-\n") do t[#t+1] = line end for i=#t,1,-1 do if t[i]:match("^%s*$") then t[i] = nil else return t end end return t end local function process_string(s) if s then local t = split(s.."\n") -- Compute min spaces local min = math.huge for _,l in ipairs(t) do min = math.min(min, string.find(l, "%S") or math.huge) end if min < math.huge then -- Now, trim 'em all! for i=1,#t do t[i] = string.sub(t[i],min,-2) end end return t end end local function process_examples(t) if not t then return nil end if type(t) == "string" then t = {t} end local n = {} for i=1,#t do n[i] = process_string(strip_quotes(t[i])) end return n end -- The standard renderers: -- The function renderer DocumentParser.addRenderer ( function (infos) -- The test return infos.keywords["function"] or infos.head_line:match("^%s*function%s+") or infos.head_line:match("^%s*local%s+function%s+") end, function (infos, output) -- The renderer if infos.keywords["function"] then local k = infos.keywords["function"][1] infos.head_line = k:match("^%s*@(.*)") end local rest = infos.head_line:match("function%s+(.*)") local tab = rest:match("([^(]*[%.%:]).*") local fun = rest:match("[^(]*[%.%:](.-)%s*%(") or rest:match("(.-)%s*%(") local pars = rest:match(".-%((.*)%)") -- Render the head print_on_output_escape(output, [[\begin{luacommand}]]) output[#output+1] = "{" .. (tab or "") .. fun .. "}" print_on_output_escape(output, "{", tab or "", "}", "{", fun, "}", "{", pars, "}") if tab then local table_name = tab:sub(1,-2) local t = output[table_name] or {} t[#t+1] = { link = "pgf/lua/" .. tab .. fun, text = "function " .. tab .. "\\declare{" .. fun .. "} (" .. pars .. ")" } output[table_name] = t end local mode = "text" for _,l in ipairs(infos.doc_lines) do if mode ~= "done" then mode = print_docline_on_output(output, l, mode) end end close_mode(output, mode) print_on_output(output, [[\end{luacommand}]]) end ) -- The table renderer DocumentParser.addRenderer ( function (infos) -- The test return infos.keywords["table"] or infos.head_line:match("=%s*{") end, function (infos, output) -- The renderer if infos.keywords["table"] then local k = infos.keywords["table"][1] infos.head_line = k:match("^%s*@table(.*)") .. "=" end local name = infos.head_line:match("^%s*local%s+(.-)%s*=") or infos.head_line:match("^%s*(.*)%s*=") -- Render the head print_on_output_escape(output, [[\begin{luatable}]], "{", name:match("(.*[%.%:]).*") or "", "}", "{", name:match(".*[%.%:](*-)") or name,"}", "{", infos.filename, "}") local mode = "text" for _,l in ipairs(infos.doc_lines) do mode = print_docline_on_output(output, l, mode) end close_mode(output, mode) output[#output+1] = function () if output[name] then tex.print("\\par\\emph{Alphabetical method summary:}\\par{\\small") table.sort(output[name], function (a,b) return a.text < b.text end) for _,l in ipairs(output[name]) do tex.print("\\texttt{\\hyperlink{" .. l.link .. "}{" .. l.text:gsub("_", "\\_") .. "}}\\par") end tex.print("}") end end print_on_output(output, [[\end{luatable}]]) end ) -- The library renderer DocumentParser.addRenderer ( function (infos) -- The test return infos.keywords["library"] end, function (infos, output) -- The renderer local name = infos.filename:gsub("%.library$",""):gsub("^pgf%.gd%.","") -- Render the head print_on_output_escape(output, "\\begin{lualibrary}{", name, "}") local mode = "text" for _,l in ipairs(infos.doc_lines) do mode = print_docline_on_output(output, l, mode) end close_mode(output, mode) print_on_output(output, "\\end{lualibrary}") end ) -- The section renderer DocumentParser.addRenderer ( function (infos) -- The test return infos.keywords["section"] end, function (infos, output) -- The renderer local mode = "text" for _,l in ipairs(infos.doc_lines) do mode = print_docline_on_output(output, l, mode) end close_mode(output, mode) end ) -- The documentation (plain text) renderer DocumentParser.addRenderer ( function (infos) -- The test return infos.keywords["documentation"] end, function (infos, output) -- The renderer local mode = "text" for _,l in ipairs(infos.doc_lines) do mode = print_docline_on_output(output, l, mode) end close_mode(output, mode) end ) -- The declare renderer DocumentParser.addRenderer ( function (infos) -- The test return infos.keywords["declare"] or infos.head_line:match("declare%s*{") or infos.head_line:match("^%s*key%s*") end, function (infos, output) -- The renderer local key_name if infos.keywords["declare"] then local k = infos.keywords["declare"][1] key_name = k:match("^%s*@declare%s*(.*)") elseif infos.head_line:match("^%s*key%s*") then key_name = infos.head_line:match('^%s*key%s*"(.*)"') or infos.head_line:match("^%s*key%s*'(.*)'") else local l = infos.lines [infos.last_line + 1] key_name = l:match('key%s*=%s*"(.*)"') or l:match("key%s*=%s*'(.*)'") end assert (key_name, "could not determine key") local key = assert (keys[key_name], "unknown key '" .. key_name .. "'") -- Render the head if key.type then print_on_output_escape(output, "\\begin{luadeclare}", "{", key.key, "}", "{\\meta{", key.type, "}}", "{", key.default or "", "}", "{", key.initial or "", "}") else print_on_output_escape(output, "\\begin{luadeclarestyle}", "{", key.key, "}", "{}", "{", key.default or "", "}", "{", key.initial or "", "}") end local mode = "text" print_lines_on_output(output, process_string(strip_quotes(key.summary))) print_lines_on_output(output, process_string(strip_quotes(key.documentation))) if key.examples then local e = process_examples(key.examples) print_on_output(output, "\\par\\smallskip\\emph{Example" .. (((#e>1) and "s") or "") .. "}\\par") for _,example in ipairs(e) do print_on_output(output, "\\begin{codeexample}[]") print_lines_on_output(output, example) print_on_output(output, "\\end{codeexample}") end end print_on_output(output, key.type and "\\end{luadeclare}" or "\\end{luadeclarestyle}") end ) -- The empty line DocumentParser.addRenderer ( function (infos) -- The test return #infos.doc_lines == 1 and infos.doc_lines[1]:match("^%-*%s*$") end, function (infos, output) end ) function print_lines_on_output(output, lines) for _,l in ipairs(lines or {}) do output[#output+1] = l end end function print_on_output(output, ...) local args = {...} if #args > 0 then for i = 1, #args do args[i] = tostring(args[i]) end output[#output+1] = table.concat(args) end end function print_on_output_escape(output, ...) local args = {...} if #args > 0 then for i = 1, #args do args[i] = tostring(args[i]):gsub("_", "\\_") end output[#output+1] = table.concat(args) end end function print_docline_on_output(output, l, mode) if l:match("^%s*@section") then print_on_output(output, "\\", l:match("%s*@section%s*(.*)")) elseif l:match("^%s*@param%s+") then if mode ~= "param" then close_mode (output, mode) mode = open_mode (output, "param") end print_on_output(output, "\\item[\\texttt{", l:match("%s@param%s+(.-)%s"):gsub("_", "\\_"), "}] ", l:match("%s@param%s+.-%s+(.*)")) elseif l:match("^%s*@return%s+") then if mode ~= "return" then close_mode (output, mode) mode = open_mode (output, "returns") end print_on_output(output, "\\item[]", l:match("%s@return%s+(.*)")) elseif l:match("^%s*@see%s+") then if mode ~= "text" then close_mode (output, mode) mode = open_mode (output, "text") end print_on_output(output, "\\par\\emph{See also:} \\texttt{", l:match("%s@see%s+(.*)"):gsub("_", "\\_"), "}") elseif l:match("^%s*@usage%s+") then if mode ~= "text" then close_mode (output, mode) mode = open_mode (output, "text") end print_on_output(output, "\\par\\emph{Usage:} ", l:match("%s@usage%s+(.*)")) elseif l:match("^%s*@field+") then close_mode (output, mode) mode = open_mode (output, "field") print_on_output(output, "{", (l:match("%s@field%s+(.-)%s") or l:match("%s@field%s+(.*)")):gsub("_", "\\_"), "}", l:match("%s@field%s+.-%s+(.*)")) elseif l:match("^%s*@done") or l:match("^%s*@text") then close_mode(output, mode) print_on_output(output, l) mode = "text" elseif l:match("^%s*@library") then -- do nothing elseif l:match("^%s*@function") then -- do nothing elseif l:match("^%s*@end") then close_mode(output, mode) mode = "done" elseif l:match("^%s*@") then error("Unknown mark " .. l) else print_on_output(output, l) end return mode end function open_mode (output, mode) if mode == "param" then print_on_output(output, "\\begin{luaparameters}") elseif mode == "field" then print_on_output(output, "\\begin{luafield}") elseif mode == "returns" then print_on_output(output, "\\begin{luareturns}") end return mode end function close_mode (output, mode) if mode == "param" then print_on_output(output, "\\end{luaparameters}") elseif mode == "field" then print_on_output(output, "\\end{luafield}") elseif mode == "returns" then print_on_output(output, "\\end{luareturns}") end return mode end function collect_infos (lines, i, state) local doc_lines = {} local keywords = {} local function find_keywords(line) local keyword = line:match("^%s*@([^%s]*)") if keyword then local t = keywords[keyword] or {} t[#t+1] = line keywords[keyword] = t end return line end -- Copy triple matches: while lines[i] and lines[i]:match("^%-%-%-") do doc_lines [#doc_lines + 1] = find_keywords(lines[i]:sub(4)) i = i + 1 end -- Continue with double matches: while lines[i] and lines[i]:match("^%-%-") do doc_lines [#doc_lines + 1] = find_keywords(lines[i]:sub(3)) i = i + 1 end local head_line = "" if not keywords["end"] then -- Skip empty lines while lines[i] and lines[i]:match("^%s*$") do i = i + 1 end head_line = lines[i] or "" if lines[i] and lines[i]:match("^%-%-%-") then i = i - 1 end end return { lines = lines, last_line = i, doc_lines = doc_lines, keywords = keywords, head_line = head_line } end function render_infos(infos, state) for _,renderer in ipairs(DocumentParser.renderers) do if renderer.test (infos, state) then renderer.renderer (infos, state) return end end pgf.debug(infos) error("Unknown documentation type") end return DocumentParser