#!/usr/bin/env texlua local LIBRARY_MODE = (... == "comment2tex") local M = {} M.styles = { bash = { comment = "##", language = "bash" }, lua = { comment = "---", language = "{[5.3]Lua}" }, } M.wrappers = { lstlisting = { begin = "\\begin{lstlisting}[language=@LANG@,@CONT@numbers=left]", finish = "\\end{lstlisting}", }, plain = { begin = "\\ctxlisting%", finish = "\\endctxlisting", }, } M.defaults = { style = "bash", wrapper = "lstlisting", comment = nil, language = nil, begin = nil, finish = nil, } function M.new_opts(over) local o = {} for k, v in pairs(M.defaults) do o[k] = v end if over then for k, v in pairs(over) do if v ~= nil then o[k] = v end end end return o end function M.resolve(o) local style = M.styles[o.style] if not style then error("comment2tex: unknown style: " .. tostring(o.style) .. " (expected bash or lua)") end local wrapper = M.wrappers[o.wrapper] if not wrapper then error("comment2tex: unknown wrapper: " .. tostring(o.wrapper) .. " (expected lstlisting or plain)") end o.comment = o.comment or style.comment o.language = o.language or style.language o.begin = o.begin or wrapper.begin o.finish = o.finish or wrapper.finish return o end function M.convert(o, lines, emit) local prefix = o.comment local plen = #prefix local in_code = false local block_count = 0 local function open_code() if not in_code then block_count = block_count + 1 local cont = block_count == 1 and "" or "firstnumber=last," local line = o.begin:gsub("@LANG@", o.language):gsub("@CONT@", cont) emit(line) in_code = true end end local function close_code() if in_code then emit(o.finish) in_code = false end end for _, line in ipairs(lines) do if line:sub(1, plen) == prefix then close_code() emit((line:sub(plen + 1):gsub("^ ", ""))) else open_code() emit(line) end end close_code() end function M.read_lines(path) local fh, err = io.open(path, "r") if not fh then error("comment2tex: cannot open input: " .. tostring(err)) end local data = fh:read("*a") fh:close() local lines = {} for line in (data .. "\n"):gmatch("(.-)\n") do lines[#lines + 1] = line end if data:sub(-1) == "\n" then lines[#lines] = nil end return lines end function M.convert_file(path, o) o = M.resolve(o or M.new_opts()) local out = {} M.convert(o, M.read_lines(path), function(line) out[#out + 1] = line end) return table.concat(out, "\n") .. "\n" end function M.write_file(infile, outfile, o) local text = M.convert_file(infile, o) local fh, err = io.open(outfile, "w") if not fh then error("comment2tex: cannot open output: " .. tostring(err)) end fh:write(text) fh:close() return outfile end function M.write(style, wrapper, infile, outfile) return M.write_file(infile, outfile, M.new_opts{ style = style, wrapper = wrapper }) end local function usage(stream) stream:write([[ Usage: comment2tex.lua [options] Convert a source file with embedded LaTeX doc-comments to LaTeX. Options: -s, --style NAME preset: bash (##) or lua (---) [default: bash] -w, --wrapper NAME listing wrapper: lstlisting or plain [default: lstlisting] -c, --comment PREFIX doc-comment prefix marking a doc line -l, --language LANG listing language for code blocks -b, --begin TEMPLATE listing begin template (@LANG@, @CONT@) -e, --end TEMPLATE listing end template -o, --output FILE write LaTeX here instead of stdout -h, --help show this help Templates substitute @LANG@ with the language and @CONT@ with "firstnumber=last," on continuation blocks (empty on the first). ]]) end local function die(msg) io.stderr:write("comment2tex: " .. msg .. "\n") os.exit(1) end function M.main(argv) local over = {} local input local i = 1 local function value(flag) i = i + 1 local v = argv[i] if v == nil then die("missing value for " .. flag) end return v end while i <= #argv do local a = argv[i] if a == "-h" or a == "--help" then usage(io.stdout); return 0 elseif a == "-s" or a == "--style" then over.style = value(a) elseif a == "-w" or a == "--wrapper" then over.wrapper = value(a) elseif a == "-c" or a == "--comment" then over.comment = value(a) elseif a == "-l" or a == "--language" then over.language = value(a) elseif a == "-b" or a == "--begin" then over.begin = value(a) elseif a == "-e" or a == "--end" then over.finish = value(a) elseif a == "-o" or a == "--output" then over.output = value(a) elseif a == "--" then input = argv[i + 1]; break elseif a:sub(1, 1) == "-" and a ~= "-" then die("unknown option: " .. a) elseif input == nil then input = a else die("unexpected argument: " .. a) end i = i + 1 end if not input then usage(io.stderr); return 1 end local ok, err = pcall(function() local o = M.resolve(M.new_opts(over)) local text = M.convert_file(input, o) if over.output then M.write_file(input, over.output, o) else io.stdout:write(text) end end) if not ok then die(tostring(err):gsub("^comment2tex: ", "")) end return 0 end if not LIBRARY_MODE then os.exit(M.main(arg)) end return M