aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKim Alvefur <zash@zash.se>2022-01-24 23:54:32 +0100
committerKim Alvefur <zash@zash.se>2022-01-24 23:54:32 +0100
commit7e65b1deef086d12cff6a129857a8ba335b4eccf (patch)
treea6a85a91e784247d2a1250448ddf9a1e4a0fc2a4
parent55da054c9b111f4543ad87d62f252e4aec95fcfb (diff)
downloadprosody-7e65b1deef086d12cff6a129857a8ba335b4eccf.tar.gz
prosody-7e65b1deef086d12cff6a129857a8ba335b4eccf.zip
util.xtemplate: Yet another string template library
This one takes a stanza as input Roughly based on util.interpolation
-rw-r--r--teal-src/util/xtemplate.tl101
-rw-r--r--util/xtemplate.lua86
2 files changed, 187 insertions, 0 deletions
diff --git a/teal-src/util/xtemplate.tl b/teal-src/util/xtemplate.tl
new file mode 100644
index 00000000..b3bdc400
--- /dev/null
+++ b/teal-src/util/xtemplate.tl
@@ -0,0 +1,101 @@
+-- render(template, stanza) --> string
+-- {path} --> stanza:find(path)
+-- {{ns}name/child|each({ns}name){sub-template}}
+
+--[[
+template ::= "{" path ("|" name ("(" args ")")? (template)? )* "}"
+path ::= defined by util.stanza
+name ::= %w+
+args ::= anything with balanced ( ) pairs
+]]
+
+local s_gsub = string.gsub;
+local s_match = string.match;
+local s_sub = string.sub;
+local t_concat = table.concat;
+
+local st = require "util.stanza";
+
+local type escape_t = function (string) : string
+local type filter_t = function (string, string | st.stanza_t, string) : string | st.stanza_t, boolean
+local type filter_coll = { string : filter_t }
+
+local function render(template : string, root : st.stanza_t, escape : escape_t, filters : filter_coll) : string
+ escape = escape or st.xml_escape;
+
+ return (s_gsub(template, "%b{}", function(block : string) : string
+ local inner = s_sub(block, 2, -2);
+ local path, pipe, pos = s_match(inner, "^([^|]+)(|?)()");
+ if not path is string then return end
+ local value : string | st.stanza_t
+ if path == "." then
+ value = root;
+ elseif path == "#" then
+ value = root:get_text();
+ else
+ value = root:find(path);
+ end
+ local is_escaped = false;
+
+ while pipe == "|" do
+ local func, args, tmpl, p = s_match(inner, "^(%w+)(%b())(%b{})()", pos as integer);
+ if not func then func, args, p = s_match(inner, "^(%w+)(%b())()", pos as integer); end
+ if not func then func, tmpl, p = s_match(inner, "^(%w+)(%b{})()", pos as integer); end
+ if not func then func, p = s_match(inner, "^(%w+)()", pos as integer); end
+ if not func then break end
+ if tmpl then tmpl = s_sub(tmpl, 2, -2); end
+ if args then args = s_sub(args, 2, -2); end
+
+ if func == "each" and tmpl and st.is_stanza(value) then
+ if not args then value, args = root, path; end
+ local ns, name = s_match(args, "^(%b{})(.*)$");
+ if ns then ns = s_sub(ns, 2, -2); else name, ns = args, nil; end
+ if ns == "" then ns = nil; end
+ if name == "" then name = nil; end
+ local out, i = {}, 1;
+ for c in (value as st.stanza_t):childtags(name, ns) do
+ out[i], i = render(tmpl, c, escape, filters), i + 1;
+ end
+ value = t_concat(out);
+ is_escaped = true;
+ elseif func == "and" and tmpl then
+ local condition = value;
+ if args then condition = root:find(args); end
+ if condition then
+ value = render(tmpl, root, escape, filters);
+ is_escaped = true;
+ end
+ elseif func == "or" and tmpl then
+ local condition = value;
+ if args then condition = root:find(args); end
+ if not condition then
+ value = render(tmpl, root, escape, filters);
+ is_escaped = true;
+ end
+ elseif filters and filters[func] then
+ local f = filters[func];
+ if args == nil then
+ value, is_escaped = f(value, tmpl);
+ else
+ value, is_escaped = f(args, value, tmpl);
+ end
+ else
+ error("No such filter function: " .. func);
+ end
+ pipe, pos = s_match(inner, "^(|?)()", p as integer);
+ end
+
+ if value is string then
+ if not is_escaped then value = escape(value); end
+ return value;
+ elseif st.is_stanza(value) then
+ value = value:get_text();
+ if value then
+ return escape(value);
+ end
+ end
+ return "";
+ end));
+end
+
+return { render = render };
diff --git a/util/xtemplate.lua b/util/xtemplate.lua
new file mode 100644
index 00000000..254c8af0
--- /dev/null
+++ b/util/xtemplate.lua
@@ -0,0 +1,86 @@
+local s_gsub = string.gsub;
+local s_match = string.match;
+local s_sub = string.sub;
+local t_concat = table.concat;
+
+local st = require("util.stanza");
+
+local function render(template, root, escape, filters)
+ escape = escape or st.xml_escape;
+
+ return (s_gsub(template, "%b{}", function(block)
+ local inner = s_sub(block, 2, -2);
+ local path, pipe, pos = s_match(inner, "^([^|]+)(|?)()");
+ if not (type(path) == "string") then return end
+ local value
+ if path == "." then
+ value = root;
+ elseif path == "#" then
+ value = root:get_text();
+ else
+ value = root:find(path);
+ end
+ local is_escaped = false;
+
+ while pipe == "|" do
+ local func, args, tmpl, p = s_match(inner, "^(%w+)(%b())(%b{})()", pos);
+ if not func then func, args, p = s_match(inner, "^(%w+)(%b())()", pos); end
+ if not func then func, tmpl, p = s_match(inner, "^(%w+)(%b{})()", pos); end
+ if not func then func, p = s_match(inner, "^(%w+)()", pos); end
+ if not func then break end
+ if tmpl then tmpl = s_sub(tmpl, 2, -2); end
+ if args then args = s_sub(args, 2, -2); end
+
+ if func == "each" and tmpl and st.is_stanza(value) then
+ if not args then value, args = root, path; end
+ local ns, name = s_match(args, "^(%b{})(.*)$");
+ if ns then
+ ns = s_sub(ns, 2, -2);
+ else
+ name, ns = args, nil;
+ end
+ if ns == "" then ns = nil; end
+ if name == "" then name = nil; end
+ local out, i = {}, 1;
+ for c in (value):childtags(name, ns) do out[i], i = render(tmpl, c, escape, filters), i + 1; end
+ value = t_concat(out);
+ is_escaped = true;
+ elseif func == "and" and tmpl then
+ local condition = value;
+ if args then condition = root:find(args); end
+ if condition then
+ value = render(tmpl, root, escape, filters);
+ is_escaped = true;
+ end
+ elseif func == "or" and tmpl then
+ local condition = value;
+ if args then condition = root:find(args); end
+ if not condition then
+ value = render(tmpl, root, escape, filters);
+ is_escaped = true;
+ end
+ elseif filters and filters[func] then
+ local f = filters[func];
+ if args == nil then
+ value, is_escaped = f(value, tmpl);
+ else
+ value, is_escaped = f(args, value, tmpl);
+ end
+ else
+ error("No such filter function: " .. func);
+ end
+ pipe, pos = s_match(inner, "^(|?)()", p);
+ end
+
+ if type(value) == "string" then
+ if not is_escaped then value = escape(value); end
+ return value
+ elseif st.is_stanza(value) then
+ value = value:get_text();
+ if value then return escape(value) end
+ end
+ return ""
+ end))
+end
+
+return { render = render }