local function fmt_attrs(attrs) local function fmt_attr(k,v) k = k:gsub("_","-") 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 == "") if selfclosing then return ("<%s%s/>"):format(tag.tname,attrs) else return ("<%s%s>%s"):format(tag.tname,attrs,html(tag.body),tag.tname) end end -- tag(tname,body) or tag(tname,attrs,body) local function tag(tname, a1, a2) 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 -- T.tname(body) or T.tname(attrs, body) 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) == 'number' then return html(tostring(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 o = "" for _,item in ipairs(x) do o = o .. tostring(html(item)) end return safe(o) end end end return { html = html, tag = tag, safe = safe, T = T, }