lua2html.lua

-- Lua HTMLifier. v0.5

-- 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
--  Repackage this as a package table, taking into account the suggestion
--  about stylesheets.
--  
--  See the TODO in lparse for the possibility of changing lparse to a
--  coroutine-based generator.

-- This file returns an object constructor, but the object is a
-- function. This is probably not a great idea, but it works for now.
-- It was designed for the case where the basic style sheet is unlikely
-- to change between calls, which saves a lot of setup. Probably the 
-- solution is to abstract a stylesheet object, and attach that as
-- a parameter, which would need to get passed through the case vectors.

-- Constructor:
--   highlighter = new "lua2html" (ncolour, tagmap)
--
--    ncolour is the number of scope depth colours in the style sheet.
--    tagmap is a mapping from particular tags to html, which can be used to
--      insert mathematical symbols or whatever. Every entry in the tag map
--      must be fully html escaped.
--
-- Usage:
--   err, warn, ntokens = highlighter(file, tag, tagdata)
--
--    file is an output file, or at least an object with a :write method which
--      takes a string argument. (I think I've removed all the multiple argument
--      calls.)
--
--    tag, tagData    the result of calling lparse.parse
--
--    err, warn       error / warning encountered (booleans)
--
--    ntokens         number of tokens in the input vector
--
--  The actual output is presented through repeated calls to file:write.
--  Only the html'ised body is sent; you must previously send the
--  header (including stylesheet) and subsequently send the trailer.

local string = import "string"
local strfind, format = string.find, string.format
local table = import "table"
local tinsert, tconcat = table.insert, table.concat
local math = import "math"
local mod = math.mod

local lparse = import "lparse"
local query = import "libquery"
local entify = query.entify
local util = import "libutil"
local tableFrom, setFrom = util.tableFrom, util.setFrom

-- Here is an example overdone tagmap:

-- This is used to translate content. Do not put anything in here for which
-- there is not a clear link between tag (as opposed to tagData) and content.
-- Anything which is in here needs to be fully escaped.

--[[
  local fancyTag2content = tableFrom [[
    - => −     * => ×      / => ÷     ^ => ↑    .. => ‥
  ... => &#x2026;    <= => &#x2264;     >= => &#x2265;    ~= => &#x2260;    == => &#x2261;
    = => &#x2190;     < => &lt;          > => &gt;

    and => &#x2227;    not => &#x2241;    or => &#x2228;   nil => &#x2205;
    true => &#x2713; false => &#x2717;
  ]]
]]  

-- Map tags to classes. 
local tag2class = tableFrom[[
  + => op       - => op       * => op       / => op       ^ => op      .. => op
 ~= => comp    <= => comp    >= => comp     < => comp     > => comp    == => comp
  ( => paren    ) => paren    [ => brack    ] => brack    { => brace    } => brace
  = => assign   . => index    : => index    , => punc     ; => punc   ... => ellip

     and => wordop     not => wordop      or => wordop  
   false => const      nil => const     true => const
      do => block      end => block      for => block       if => block
  repeat => block    until => block    while => block function => block
doclause => clause    else => clause  elseif => clause      in => clause
    then => clause
   break => stmt     local => stmt    return => stmt
]]

-- error and warning tags
local tag2error = setFrom[[missing skip invalid]]
local tag2warn = setFrom[[badGlobal unused]]

return function(ncolour, tagmap)
  
  -- set defaults
  local tag2content = tableFrom [[
    doclause => do
    > => &gt;        >= => &gt;=         < => &lt;        <= => &lt;=
  ]]
  
  if tagmap then for k, v in tagmap do tag2content[k] = v end end

  local function formatVar(tag, var, idx)
    if tag == "varset" then tag = "var def" end -- ugly
    local htmltag, href, class = "span", "", {tag, n = 1}
    if var.scope.depth ~= 0 then
      tinsert(class, "depth"..mod(var.scope.depth, ncolour))
    end
    if var.def == "STDLIB" then tinsert(class, "stdlib")
     elseif var.def == idx then
       htmltag, href = "a", format(" name='a%i' id='a%i'", idx, idx)
     elseif var.def then
       htmltag, href = "a", format(" href='#a%i'", var.def)
    end
    if var.badGlobal then tinsert(class, "badGlobal")
     elseif not var.used and var.name ~= "_" and not var.global then
                                            tinsert(class, "unused")
     elseif var.closed then if var.set then tinsert(class, "vclosure")
                                       else tinsert(class, "cclosure")
                            end
    end
    return format("<%s%s class='%s'>%s</%s>",
                           htmltag, href, tconcat(class, " "), var.name, htmltag)
  end
  
  local function formatKeydef(t, td, idx)
    return format("<a name='a%i' id='a%i' class='key def'>%s</a>",
                         idx, idx, lparse.textFor(t, td))
  end
  
  local function formatMeantequal()
    return "<span class='comp'>=</span><span class='missing'>=</span>"
  end
  
  local function formatMissing(_, tagdata)
    return format("%s<span class='missing'>%s</span>",
                         strfind(tagdata, "^[.()]") and "" or " ", entify(tagdata))
  end
  
  local function formatEscape(_, tagdata)
    local _, _, f, r = strfind(tagdata, "(.)(.*)")
    return format("<span class='escape1'>%s</span><span class='escape'>%s</span>", 
                          entify(f),  entify(r))
  end
                           
  local function defaultFormat(tag, tagdata)
    local scope, class, text = lparse.scopeFor(tag, tagdata), tag2class[tag] or tag,
                               tag2content[tag] or entify(lparse.textFor(tag, tagdata))
    if scope then
      return format("<span class='%s depth%i'>%s</span>",
                     class, mod(scope.depth, ncolour), text)
     else 
      return format("<span class='%s'>%s</span>", class, text)
    end
  end
  
  local actionTab = setmetatable({
    missing = formatMissing,
    var = formatVar,
    varset = formatVar,
    keydef = formatKeydef,
    meantequal = formatMeantequal,
    escape = formatEscape,
    whitespace = function(_, w) return w end,
    EOF = function() return "" end
  }, {
    __index = function() return defaultFormat end
  })
  
  -- returns (boolean)err, (boolean)warn, (int)#tokens
  return function(outf, tagVec, tagData)
    outf:write "<pre>"
    local i, n = 1, tagVec.n
    local err, warn
    while i <= n do
      local tag = tagVec[i]
      err = err or tag2error[tag]
      warn = warn or tag2warn[tag]
      if tag == "skip" then
        outf:write "<span class='skip'><span class='skipped'>"
        for j = i + 1, n do
          if tagVec[j] == "endskip" then
            i = j
            outf:write "</span></span>"
            break
          end
          outf:write(tagData[j])
        end
       else
        outf:write(actionTab[tagVec[i]](tagVec[i], tagData[i], i))
      end
      i = i + 1
    end
    outf:write "</pre>\n"
    return err, warn, n
  end

end

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