util/template.lua

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;