| 1 | local st = require "util.stanza"; |
| 2 | local lxp = require "lxp"; |
| 3 | local setmetatable = setmetatable; |
| 4 | local pairs = pairs; |
| 5 | local ipairs = ipairs; |
| 6 | local error = error; |
| 7 | local loadstring = loadstring; |
| 8 | local debug = debug; |
| 9 | |
| 10 | module("template") |
| 11 | |
| 12 | local parse_xml = (function() |
| 13 | local ns_prefixes = { |
| 14 | ["http://www.w3.org/XML/1998/namespace"] = "xml"; |
| 15 | }; |
| 16 | local ns_separator = "\1"; |
| 17 | local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$"; |
| 18 | return function(xml) |
| 19 | local handler = {}; |
| 20 | local stanza = st.stanza("root"); |
| 21 | function handler:StartElement(tagname, attr) |
| 22 | local curr_ns,name = tagname:match(ns_pattern); |
| 23 | if name == "" then |
| 24 | curr_ns, name = "", curr_ns; |
| 25 | end |
| 26 | if curr_ns ~= "" then |
| 27 | attr.xmlns = curr_ns; |
| 28 | end |
| 29 | for i=1,#attr do |
| 30 | local k = attr[i]; |
| 31 | attr[i] = nil; |
| 32 | local ns, nm = k:match(ns_pattern); |
| 33 | if nm ~= "" then |
| 34 | ns = ns_prefixes[ns]; |
| 35 | if ns then |
| 36 | attr[ns..":"..nm] = attr[k]; |
| 37 | attr[k] = nil; |
| 38 | end |
| 39 | end |
| 40 | end |
| 41 | stanza:tag(name, attr); |
| 42 | end |
| 43 | function handler:CharacterData(data) |
| 44 | data = data:gsub("^%s*", ""):gsub("%s*$", ""); |
| 45 | stanza:text(data); |
| 46 | end |
| 47 | function handler:EndElement(tagname) |
| 48 | stanza:up(); |
| 49 | end |
| 50 | local parser = lxp.new(handler, "\1"); |
| 51 | local ok, err, line, col = parser:parse(xml); |
| 52 | if ok then ok, err, line, col = parser:parse(); end |
| 53 | --parser:close(); |
| 54 | if ok then |
| 55 | return stanza.tags[1]; |
| 56 | else |
| 57 | return ok, err.." (line "..line..", col "..col..")"; |
| 58 | end |
| 59 | end; |
| 60 | end)(); |
| 61 | |
| 62 | local function create_string_string(str) |
| 63 | str = ("%q"):format(str); |
| 64 | str = str:gsub("{([^}]*)}", function(s) |
| 65 | return '"..(data["'..s..'"]or"").."'; |
| 66 | end); |
| 67 | return str; |
| 68 | end |
| 69 | local function create_attr_string(attr, xmlns) |
| 70 | local str = '{'; |
| 71 | for name,value in pairs(attr) do |
| 72 | if name ~= "xmlns" or value ~= xmlns then |
| 73 | str = str..("[%q]=%s;"):format(name, create_string_string(value)); |
| 74 | end |
| 75 | end |
| 76 | return str..'}'; |
| 77 | end |
| 78 | local function create_clone_string(stanza, lookup, xmlns) |
| 79 | if not lookup[stanza] then |
| 80 | local s = ('setmetatable({name=%q,attr=%s,tags={'):format(stanza.name, create_attr_string(stanza.attr, xmlns)); |
| 81 | -- add tags |
| 82 | for i,tag in ipairs(stanza.tags) do |
| 83 | s = s..create_clone_string(tag, lookup, stanza.attr.xmlns)..";"; |
| 84 | end |
| 85 | s = s..'};'; |
| 86 | -- add children |
| 87 | for i,child in ipairs(stanza) do |
| 88 | if child.name then |
| 89 | s = s..create_clone_string(child, lookup, stanza.attr.xmlns)..";"; |
| 90 | else |
| 91 | s = s..create_string_string(child)..";" |
| 92 | end |
| 93 | end |
| 94 | s = s..'}, stanza_mt)'; |
| 95 | s = s:gsub('%.%.""', ""):gsub('([=;])""%.%.', "%1"):gsub(';"";', ";"); -- strip empty strings |
| 96 | local n = #lookup + 1; |
| 97 | lookup[n] = s; |
| 98 | lookup[stanza] = "_"..n; |
| 99 | end |
| 100 | return lookup[stanza]; |
| 101 | end |
| 102 | local stanza_mt = st.stanza_mt; |
| 103 | local function create_cloner(stanza, chunkname) |
| 104 | local lookup = {}; |
| 105 | local name = create_clone_string(stanza, lookup, ""); |
| 106 | local f = "local setmetatable,stanza_mt=...;return function(data)"; |
| 107 | for i=1,#lookup do |
| 108 | f = f.."local _"..i.."="..lookup[i]..";"; |
| 109 | end |
| 110 | f = f.."return "..name..";end"; |
| 111 | local f,err = loadstring(f, chunkname); |
| 112 | if not f then error(err); end |
| 113 | return f(setmetatable, stanza_mt); |
| 114 | end |
| 115 | |
| 116 | local template_mt = { __tostring = function(t) return t.name end }; |
| 117 | local function create_template(templates, text) |
| 118 | local stanza, err = parse_xml(text); |
| 119 | if not stanza then error(err); end |
| 120 | |
| 121 | local info = debug.getinfo(3, "Sl"); |
| 122 | info = info and ("template(%s:%d)"):format(info.short_src:match("[^\\/]*$"), info.currentline) or "template(unknown)"; |
| 123 | |
| 124 | local template = setmetatable({ apply = create_cloner(stanza, info), name = info, text = text }, template_mt); |
| 125 | templates[text] = template; |
| 126 | return template; |
| 127 | end |
| 128 | |
| 129 | local templates = setmetatable({}, { __mode = 'k', __index = create_template }); |
| 130 | return function(text) |
| 131 | return templates[text]; |
| 132 | end; |