local function fmt_attrs(attrs) local function fmt_attr(k,v) if v == true then return k else if type(v) == "table" then v = table.concat(v," ") end return ('%s="%s"'):format(k,v) end end local o = "" for k,v in pairs(attrs) do o = o .. " " .. fmt_attr(k,v) end return o end local html local function fmt_tag(tag) local attrs = tag.attrs and fmt_attrs(tag.attrs) or "" local selfclosing = (tag.body == "") local sameline = type(tag.body) == "string" or (type(tag.body) == "table" and tag.body.tag) or (type(tag.body) == "table" and #tag.body <= 1) local maybenl = sameline and "" or "\n" if selfclosing then return ("<%s%s/>"):format(tag.tname,attrs) else return ("<%s%s>%s%s"):format(tag.tname,attrs,maybenl,html(tag.body),tag.tname) end end local function tag(tname, a1, a2) -- tag(tname,body) or tag(tname,attrs,body) local body, attrs if a2 then attrs=a1 body=a2 else attrs=nil body=a1 end return setmetatable({tag=true,tname=tname,attrs=attrs,body=body},{__tostring=fmt_tag}) end -- instead of tag('ul', {x, y, z}) -- you can do T.ul{ x, y, z} local T = setmetatable({}, {__index=function(_,tname) return function (...) return tag(tname, ...) end end}) local function safe(s) -- marks s as safe, doesn't escape it assert(type(s) == "string","can only mark string as safe") return setmetatable({safe=true,s=s},{__tostring=function(x)return x.s end}) end local function escape(s) s=s:gsub("&","&") s=s:gsub("<","<") s=s:gsub(">",">") s=s:gsub('"',""") return safe(s) end html = function (x) if type(x) == "string" then return escape(x) elseif type(x) == "table" then if x.safe then -- safestr. already escaped return x elseif x.tag then -- it's a tag return safe( tostring(x) ) else -- just a regular list local nl = "\n" if #x <= 2 then nl = "" end local o = "" for _,item in ipairs(x) do o = o .. tostring(html(item)) .. nl end return safe(o) end end end return { html = html, tag = tag, safe = safe, T = T, }