util/interpolation.lua
author Kim Alvefur <zash@zash.se>
Thu, 28 Mar 2024 15:26:57 +0100
changeset 13472 98806cac64c3
parent 11311 f2e276bb4ef8
permissions -rw-r--r--
MUC: Switch to official XEP-0317 namespace for Hats (including compat) (thanks nicoco)

-- Simple template language
--
-- The new() function takes a pattern and an escape function and returns
-- a render() function.  Both are required.
--
-- The function render() takes a string template and a table of values.
-- Sequences like {name} in the template string are substituted
-- with values from the table, optionally depending on a modifier
-- symbol.
--
-- Variants are:
-- {name} is substituted for values["name"] and is escaped using the
-- second argument to new_render().  To disable the escaping, use {name!}.
-- {name.item} can be used to access table items.
-- To renter lists of items: {name# item number {idx} is {item} }
-- Or key-value pairs: {name% t[ {idx} ] = {item} }
-- To show a defaults for missing values {name? sub-template } can be used,
-- which renders a sub-template if values["name"] is false-ish.
-- {name& sub-template } does the opposite, the sub-template is rendered
-- if the selected value is anything but false or nil.

local type, tostring = type, tostring;
local pairs, ipairs = pairs, ipairs;
local s_sub, s_gsub, s_match = string.sub, string.gsub, string.match;
local t_concat = table.concat;

local function new_render(pat, escape, funcs)
	-- assert(type(pat) == "string", "bad argument #1 to 'new_render' (string expected)");
	-- assert(type(escape) == "function", "bad argument #2 to 'new_render' (function expected)");
	local function render(template, values)
		-- assert(type(template) == "string", "bad argument #1 to 'render' (string expected)");
		-- assert(type(values) == "table", "bad argument #2 to 'render' (table expected)");
		return (s_gsub(template, pat, function (block)
			block = s_sub(block, 2, -2);
			local name, raw, opt, e = s_match(block, "^([%a_][%w_.]*)(!?)(%p?)()");
			if not name then return end
			local value = values[name];
			if not value and name:find(".", 2, true) then
				value = values;
				for word in name:gmatch"[^.]+" do
					value = value[word];
					if not value then break; end
				end
			end
			if funcs then
				while opt == '|' do
					local f;
					f, raw, opt, e = s_match(block, "^([%a_][%w_.]*)(!?)(%p?)()", e);
					f = funcs[f];
					if value ~= nil and f then value = f(value); end
				end
			end
			if opt == '#' or opt == '%' then
				if type(value) ~= "table" then return ""; end
				local iter = opt == '#' and ipairs or pairs;
				local out, i, subtpl = {}, 1, s_sub(block, e);
				local subvalues = setmetatable({}, { __index = values });
				for idx, item in iter(value) do
					subvalues.idx = idx;
					subvalues.item = item;
					out[i], i = render(subtpl, subvalues), i+1;
				end
				return t_concat(out);
			elseif opt == '&' then
				if not value then return ""; end
				return render(s_sub(block, e), values);
			elseif opt == '~' then
				if value then return ""; end
				return render(s_sub(block, e), values);
			elseif opt == '?' and not value then
				return render(s_sub(block, e), values);
			elseif value ~= nil then
				if type(value) ~= "string" then
					value = tostring(value);
				end
				if raw ~= '!' then
					return escape(value);
				end
				return value;
			end
		end));
	end
	return render;
end

return {
	new = new_render;
};