local M = {} local void = { area = true, base = true, br = true, col = true, embed = true, hr = true, img = true, input = true, link = true, meta = true, param = true, source = true, track = true, wbr = true, } local char_refs = { ["<"] = "<", [">"] = ">", ['"'] = """, ["&"] = "&", } local function escape(x) return (tostring(x):gsub('.', char_refs)) end local function ele(_, name) assert(name:match '^%w+$') return function(...) local params, inner if select('#', ...) == 2 or type((...)) == 'table' then params, inner = ... else inner = ... end coroutine.yield("<", name) if params then coroutine.yield(" ") for k, v in pairs(params) do assert(not k:find '[%s"\'>/=]') coroutine.yield(k, "=\"", escape(v), "\" ") end end coroutine.yield(">") if not void[name] then if type(inner) == 'function' then inner() elseif inner then coroutine.yield(escape(inner)) end coroutine.yield("") else assert(not inner) end end end setmetatable(M, {__index = ele}) local function document(fn) return function() coroutine.yield("") M.html(fn) coroutine.yield("\n") end end function M.render(fn, ...) local output = {} local co = coroutine.create(fn, ...) local retvals while true do retvals = {assert(coroutine.resume(co))} if coroutine.status(co) == 'dead' then break end if retvals[2] == abort then coroutine.yield(unpack(retvals)) end for i = 2, #retvals do table.insert(output, retvals[i]) end end return table.concat(output), table.unpack(retvals, 2) end function M.render_doc(fn, ...) return M.render(document(fn), ...) end function M.text(str) coroutine.yield(escape(str)) end function M.url_encode(str) return tostring(str):gsub('([^A-Za-z0-9%_%.%-%~])', function(v) return ('%%%02x'):format(v:byte()):upper() end) end function M.url_decode(str) return tostring(str):gsub('%%(%x%x)', function(c) return string.char(tonumber(c, 16)) end):gsub('%+', " ") end return M