tnt.lua

-- TNT, the Lua highlighter
--
-- copyright 2004 by Rici Lake. Permission is granted to use this code under
-- the same terms and conditions as found in the Lua copyright notice at
-- http://www.lua.org/license.html.
--
-- I'd appreciate a credit if you use this code, but I don't insist. 
--
-- This is prerelease software, no interfaces are guaranteed.
-- Use at your own risk, etc.

-- TODO
--  Separate command line and CGI versions and clean things up.

if not import then error "import not loaded; read the INSTALL document" end

local VERSION = "TNT/0.5"
local COPYRIGHT = "Copyright (C) 2004 Rici Lake"
_COPYRIGHT = _COPYRIGHT or "Copyright (C) 1994-2003 Tecgraf, PUC-Rio"

local defaultCSSFile = "tnt.css"

local globals = {}
for k in _G do globals[k] = true end

local string = import "string"
local strfind, gfind, gsub, format, strlen =
      string.find, string.gfind, string.gsub, string.format, string.len

local lua2htmlFactory = new "lua2html"
local lparse = import "lparse"
local lexer = new "blex"("", "")
local query = import "libquery"

local header = [[
<html><head><title>%s</title><style type="text/css">%s</style></head><body style="background-color:#fff7ef"><h1>%s</h1>
]]

-- sample style stuff
local legend = -- don't muck with the spaces here !
[[<div style="border: 3px double black; padding:6px; margin-left:10%%; margin-right:10%%"><pre>]]
..    [[<span class="preprocessor">#!/she/bang or #preprocessor</span>   ]]   
..    [[<span class="comment">--[[Comment]]       -- Comment</span>
]]..  [[<span class="skip"><span class="skipped">Unparseable text</span></span>               ]]
..    [[<span class="missing">something missing</span>   ]]
..    [[<span class='comp'>=</span><span class='missing'>=</span>  = instead of ==
]]..  [[<span class="badGlobal">undefined global</span>               ]] 
..    [[<span class="var unused">unused variable</span>     ]]
..    [[<span class="vclosure var">mutable upvalue</span>
</pre></div>

]]

local footer = "<hr/><p>Produced by TNT, the Lua-linter. "
                        .. VERSION .. " " .. COPYRIGHT
               .. "</p></body></html>\n"

-- We could, of course, insist that they tell us :)

local function getncolour(style)
  local ncolour = 1
  for i in gfind(style, "%.depth(%d+)") do
    local n = tonumber(i) + 1
    if n > ncolour then ncolour = n end
  end
  return ncolour
end

local stylesheet = new "defaultStyle" ()
local titlepat, outpat
local logf
local htmlify

local function carp(err)
  io.stderr:write(err, "\n")
  os.exit(1)
end

local function inform(msg)
  io.stderr:write(msg, "\n")
  os.exit(0)
end

local function check(ok, err)
  if not ok then carp(err) end
  return ok
end
 
local function openfile(fname, mode)
  return check(io.open(fname, mode or "r"))
end

local function readAndClose(file)
  local rv = file:read "*a"
  file:close()
  return rv
end

local function readAll(fname)
  return readAndClose(openfile(fname))
end

-- Try to load the default style sheet
local function checkstyle()
  stylesheet = stylesheet or readAll(defaultCSSFile) 
  return stylesheet
end
  
-- Very basic, this is. But it could be improved sometime.
local function subst(pat, fname, default)
  if pat then return (gsub(pat, "%?", fname))
         else return default
  end
end

-- conditional formatted logging (only one loglevel for now)
local function nologger() end
local function logger(...)
  io.stderr:write(format(unpack(arg)))
end

-- fname is mostly only used for logging
local function tnt(inf, outf, title, fname)
  htmlify = htmlify or lua2htmlFactory(getncolour(checkstyle()))
  title = query.entify(title)
  -- This read had an assert around it, but that bombs on /dev/null
  -- which is pseudo-legit.
  local now = os.clock()
  local buf = readAndClose(inf) or ""
  lexer.reuse(buf, fname)
  outf:write(query.xhtml10strict)
  outf:write(format(header, title, stylesheet, title))
  local e, w, n = htmlify(outf, lparse.parse(lexer, globals))
  outf:write(footer)
  outf:close()
  local took = os.clock() - now
  logf("%s: \t%i/%i chars/tokens, %9.3f seconds %s\n",
        fname, strlen(buf), n, took, e and ": Errors"
                                         or w and ": Warnings"
                                         or "")
end

local function tntfile(fname)
  local inf
  if fname == "-" then
    inf, fname = io.stdin, "STDIN"
   else
    inf = openfile(fname)
    outpat = outpat or "?.html"
  end
  local ofname = subst(outpat, fname, "-")
  local outf = ofname == "-" and io.stdout or openfile(ofname, "w")
  tnt(inf, outf, subst(titlepat, fname, fname), fname)
end

local function putlegend()
  local ofname = subst(outpat, "legend", "-")
  local outf = ofname == "-" and io.stdout or openfile(ofname, "w")
  outf:write(query.xhtml10strict)
  local title = subst(titlepat, "legend", "TNT legend")
  outf:write(format(header, title, checkstyle(), title))
  outf:write(legend)
  outf:write(footer)
  outf:close()
end

---- CGI handling ----

local function cgitnt(buf, title)
  htmlify = htmlify or lua2htmlFactory(getncolour(checkstyle()))
  title = query.entify(title)
  lexer.reuse(buf, title)
  io.stdout:write "Content-Type: text/html\r\n\r\n"
  io.stdout:write(query.xhtml10strict)
  io.stdout:write(format(header, title, stylesheet, title))
  htmlify(io.stdout, lparse.parse(lexer, globals))
  io.stdout:write(footer)
end

local function cgilegend()
  io.stdout:write "Content-Type: text/html\r\n\r\n"
  io.stdout:write(query.xhtml10strict)
  io.stdout:write(format(header, "TNT legend", checkstyle(), "TNT legend"))
  io.stdout:write(footer)
end

local function docgi()
  local fname, buf
  for k, v in query.qpairs(query.cgiGetQuery()) do
    if k == "fname" then fname = gsub(v, "[^-._/ %w]", "?")
     elseif k == "lua" then buf = gsub(v, "\r", "")
    end
  end
  if buf and buf ~= "" then cgitnt(buf, fname)
   else
    io.stdout:write "Content-Type: text/html\r\n\r\n"
    io.stdout:write(query.xhtml10strict)
    io.stdout:write( [[
<html><head><title>TNT colourises it for you!</title></head>
      <body><h1>Put your Lua code here</h1>
            <form action="]]..os.getenv "SCRIPT_NAME"..[[" method="post">
             <p>Module name: <input type="text" name="fname"></input>
             <textarea name="lua" rows="30" cols="120" style="width: 100%; height: 80%;">-- Paste it here!</textarea>
             <input type="submit" value="send"></input></p>
            </form>
      </body>
</html>
]])
  end
end

---- Option handling ----

local nextarg, putback
do
  local argno = 0
  function nextarg(r)
    if r and r ~= "" then return r
     else
      argno = argno + 1
      return arg[argno]
    end
  end
  -- yuk
  function putback(r)
    if r ~= "" then 
      arg[argno] = "-" .. r
      argno = argno - 1
    end
  end
end

local function putusage(func)
  func(arg[0] .. [[ (option | file)...
OPTIONS
  -t title  Set title
  -o file   Output filename. Default is ?.html
            (stdout if input is stdin)
  -f file   Process FILE. Only necessary if filename starts
            with a -
  -s file   File containing stylesheet
  -v        Verbose (not very)
  -l        Output the style legend
  -V        Output version to stderr and quit
  -?        Output this help and quit
  -         use stdin (default if no files specified)
]])
end

local function usage(x)
  if x then return x
       else putusage(carp)
  end
end

local processed = 0

local optfuncs = {
  t = function(r) titlepat = usage(nextarg(r)) end,
  o = function(r) outpat = usage(nextarg(r)) end,
  f = function(r) tntfile(usage(nextarg(r))) end,
  s = function(r) stylesheet = readAll(usage(nextarg(r)))
                  htmlify = nil -- force a new htmlifier
      end,
  l = function(r) putback(r); processed = processed + 1; putlegend() end,
  v = function(r) putback(r); logf = logger end,
  V = function(r)
        inform(VERSION.."\t"..COPYRIGHT.. "\n"
                .._VERSION.."\t".._COPYRIGHT)
      end,
  ['?'] = function() putusage(inform) end
}

---- Main function ---

local function handler()

  logf = nologger
  for thisarg in nextarg do
    local _, _, shortoption, rest = strfind(thisarg, "^%-(%w)(.*)")
    if shortoption then usage(optfuncs[shortoption])(rest)
                   else processed = processed + 1
                        tntfile(thisarg)
    end
  end
  if processed == 0 then tntfile "-" end
end

if os.getenv "GATEWAY_INTERFACE" then
  local ok, err = pcall(docgi)
  if not ok then
    local _, _, status = strfind(err, ":%d+: (%d%d%d) ")
    if status then
      io.stdout:write ("Status: "..status.."\r\n\r\n")
     else
      io.stdout:write "Status: 500\r\nContent-Type: text/plain\r\n\r\n"
      io.stdout:write(err)
    end
  end
 else
  -- For now, we just die on errors. It would be nicer to skip the file and
  -- go on to the next one, maybe.
  local ok, err = pcall(handler)
  if not ok then carp(err) end
end

Produced by TNT, the Lua-linter. TNT/0.5 Copyright (C) 2004 Rici Lake