Merge 0.10->trunk
[prosody.git] / util / interpolation.lua
1 -- Simple template language
2 --
3 -- The new() function takes a pattern and an escape function and returns
4 -- a render() function.  Both are required.
5 --
6 -- The function render() takes a string template and a table of values.
7 -- Sequences like {name} in the template string are substituted
8 -- with values from the table, optionally depending on a modifier
9 -- symbol.
10 --
11 -- Variants are:
12 -- {name} is substituted for values["name"] and is escaped using the
13 -- second argument to new_render().  To disable the escaping, use {name!}.
14 -- {name.item} can be used to access table items.
15 -- To renter lists of items: {name# item number {idx} is {item} }
16 -- Or key-value pairs: {name% t[ {idx} ] = {item} }
17 -- To show a defaults for missing values {name? sub-template } can be used,
18 -- which renders a sub-template if values["name"] is false-ish.
19 -- {name& sub-template } does the opposite, the sub-template is rendered
20 -- if the selected value is anything but false or nil.
21
22 local type, tostring = type, tostring;
23 local pairs, ipairs = pairs, ipairs;
24 local s_sub, s_gsub, s_match = string.sub, string.gsub, string.match;
25 local t_concat = table.concat;
26
27 local function new_render(pat, escape, funcs)
28         -- assert(type(pat) == "string", "bad argument #1 to 'new_render' (string expected)");
29         -- assert(type(escape) == "function", "bad argument #2 to 'new_render' (function expected)");
30         local function render(template, values)
31                 -- assert(type(template) == "string", "bad argument #1 to 'render' (string expected)");
32                 -- assert(type(values) == "table", "bad argument #2 to 'render' (table expected)");
33                 return (s_gsub(template, pat, function (block)
34                         block = s_sub(block, 2, -2);
35                         local name, opt, e = s_match(block, "^([%a_][%w_.]*)(%p?)()");
36                         if not name then return end
37                         local value = values[name];
38                         if not value and name:find(".", 2, true) then
39                                 value = values;
40                                 for word in name:gmatch"[^.]+" do
41                                         value = value[word];
42                                         if not value then break; end
43                                 end
44                         end
45                         if funcs then
46                                 while value ~= nil and opt == '|' do
47                                         local f;
48                                         f, opt, e = s_match(block, "^([%a_][%w_.]*)(%p?)()", e);
49                                         f = funcs[f];
50                                         if f then value = f(value); end
51                                 end
52                         end
53                         if opt == '#' or opt == '%' then
54                                 if type(value) ~= "table" then return ""; end
55                                 local iter = opt == '#' and ipairs or pairs;
56                                 local out, i, subtpl = {}, 1, s_sub(block, e);
57                                 local subvalues = setmetatable({}, { __index = values });
58                                 for idx, item in iter(value) do
59                                         subvalues.idx = idx;
60                                         subvalues.item = item;
61                                         out[i], i = render(subtpl, subvalues), i+1;
62                                 end
63                                 return t_concat(out);
64                         elseif opt == '&' then
65                                 if not value then return ""; end
66                                 return render(s_sub(block, e), values);
67                         elseif opt == '?' and not value then
68                                 return render(s_sub(block, e), values);
69                         elseif value ~= nil then
70                                 if type(value) ~= "string" then
71                                         value = tostring(value);
72                                 end
73                                 if opt ~= '!' then
74                                         return escape(value);
75                                 end
76                                 return value;
77                         end
78                 end));
79         end
80         return render;
81 end
82
83 return {
84         new = new_render;
85 };