util.template: Optimized to be almost as fast as manual stanza building.
[prosody.git] / util / template.lua
1
2 local t_insert = table.insert;
3 local st = require "util.stanza";
4 local lxp = require "lxp";
5 local setmetatable = setmetatable;
6 local pairs = pairs;
7 local error = error;
8 local s_gsub = string.gsub;
9
10 local print = print;
11
12 module("template")
13
14 local function process_stanza(stanza, ops)
15         -- process attrs
16         for key, val in pairs(stanza.attr) do
17                 if val:match("{[^}]*}") then
18                         t_insert(ops, {stanza.attr, key, val});
19                 end
20         end
21         -- process children
22         local i = 1;
23         while i <= #stanza do
24                 local child = stanza[i];
25                 if child.name then
26                         process_stanza(child, ops);
27                 elseif child:match("^{[^}]*}$") then -- text
28                         t_insert(ops, {stanza, i, child:match("^{([^}]*)}$"), true});
29                 elseif child:match("{[^}]*}") then -- text
30                         t_insert(ops, {stanza, i, child});
31                 end
32                 i = i + 1;
33         end
34 end
35
36 local parse_xml = (function()
37         local ns_prefixes = {
38                 ["http://www.w3.org/XML/1998/namespace"] = "xml";
39         };
40         local ns_separator = "\1";
41         local ns_pattern = "^([^"..ns_separator.."]*)"..ns_separator.."?(.*)$";
42         return function(xml)
43                 local handler = {};
44                 local stanza = st.stanza("root");
45                 function handler:StartElement(tagname, attr)
46                         local curr_ns,name = tagname:match(ns_pattern);
47                         if name == "" then
48                                 curr_ns, name = "", curr_ns;
49                         end
50                         if curr_ns ~= "" then
51                                 attr.xmlns = curr_ns;
52                         end
53                         for i=1,#attr do
54                                 local k = attr[i];
55                                 attr[i] = nil;
56                                 local ns, nm = k:match(ns_pattern);
57                                 if nm ~= "" then
58                                         ns = ns_prefixes[ns]; 
59                                         if ns then 
60                                                 attr[ns..":"..nm] = attr[k];
61                                                 attr[k] = nil;
62                                         end
63                                 end
64                         end
65                         stanza:tag(name, attr);
66                 end
67                 function handler:CharacterData(data)
68                         data = data:gsub("^%s*", ""):gsub("%s*$", "");
69                         stanza:text(data);
70                 end
71                 function handler:EndElement(tagname)
72                         stanza:up();
73                 end
74                 local parser = lxp.new(handler, "\1");
75                 local ok, err, line, col = parser:parse(xml);
76                 if ok then ok, err, line, col = parser:parse(); end
77                 --parser:close();
78                 if ok then
79                         return stanza.tags[1];
80                 else
81                         return ok, err.." (line "..line..", col "..col..")";
82                 end
83         end;
84 end)();
85
86 local stanza_mt = st.stanza_mt;
87 local function fast_st_clone(stanza, lookup)
88         local stanza_attr = stanza.attr;
89         local stanza_tags = stanza.tags;
90         local tags, attr = {}, {};
91         local clone = { name = stanza.name, attr = attr, tags = tags, last_add = {} };
92         for k,v in pairs(stanza_attr) do attr[k] = v; end
93         lookup[stanza_attr] = attr;
94         for i=1,#stanza_tags do
95                 local child = stanza_tags[i];
96                 local new = fast_st_clone(child, lookup);
97                 tags[i] = new;
98                 lookup[child] = new;
99         end
100         for i=1,#stanza do
101                 local child = stanza[i];
102                 clone[i] = lookup[child] or child;
103         end
104         return setmetatable(clone, stanza_mt);
105 end
106
107 local function create_template(text)
108         local stanza, err = parse_xml(text);
109         if not stanza then error(err); end
110         local ops = {};
111         process_stanza(stanza, ops);
112         
113         local template = {};
114         local lookup = {};
115         function template.apply(data)
116                 local newstanza = fast_st_clone(stanza, lookup);
117                 for i=1,#ops do
118                         local op = ops[i];
119                         local t, k, v, g = op[1], op[2], op[3], op[4];
120                         if g then
121                                 lookup[t][k] = data[v];
122                         else
123                                 lookup[t][k] = s_gsub(v, "{([^}]*)}", data);
124                         end
125                 end
126                 return newstanza;
127         end
128         return template;
129 end
130
131 local templates = setmetatable({}, { __mode = 'k' });
132 return function(text)
133         local template = templates[text];
134         if not template then
135                 template = create_template(text);
136                 templates[text] = template;
137         end
138         return template;
139 end;