diff options
author | Kim Alvefur <zash@zash.se> | 2022-01-24 23:54:32 +0100 |
---|---|---|
committer | Kim Alvefur <zash@zash.se> | 2022-01-24 23:54:32 +0100 |
commit | 7e65b1deef086d12cff6a129857a8ba335b4eccf (patch) | |
tree | a6a85a91e784247d2a1250448ddf9a1e4a0fc2a4 | |
parent | 55da054c9b111f4543ad87d62f252e4aec95fcfb (diff) | |
download | prosody-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.tl | 101 | ||||
-rw-r--r-- | util/xtemplate.lua | 86 |
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 } |