aboutsummaryrefslogtreecommitdiffstats
path: root/teal-src/util
diff options
context:
space:
mode:
Diffstat (limited to 'teal-src/util')
-rw-r--r--teal-src/util/compat.d.tl4
-rw-r--r--teal-src/util/crand.d.tl6
-rw-r--r--teal-src/util/dataforms.d.tl52
-rw-r--r--teal-src/util/datamapper.tl381
-rw-r--r--teal-src/util/datetime.d.tl11
-rw-r--r--teal-src/util/encodings.d.tl27
-rw-r--r--teal-src/util/error.d.tl78
-rw-r--r--teal-src/util/format.d.tl4
-rw-r--r--teal-src/util/hashes.d.tl23
-rw-r--r--teal-src/util/hex.d.tl6
-rw-r--r--teal-src/util/http.d.tl9
-rw-r--r--teal-src/util/human/units.d.tl5
-rw-r--r--teal-src/util/id.d.tl9
-rw-r--r--teal-src/util/interpolation.d.tl6
-rw-r--r--teal-src/util/jid.d.tl15
-rw-r--r--teal-src/util/json.d.tl18
-rw-r--r--teal-src/util/jsonpointer.tl46
-rw-r--r--teal-src/util/jsonschema.tl374
-rw-r--r--teal-src/util/net.d.tl13
-rw-r--r--teal-src/util/poll.d.tl31
-rw-r--r--teal-src/util/pposix.d.tl108
-rw-r--r--teal-src/util/random.d.tl4
-rw-r--r--teal-src/util/ringbuffer.d.tl20
-rw-r--r--teal-src/util/signal.d.tl41
-rw-r--r--teal-src/util/smqueue.tl99
-rw-r--r--teal-src/util/stanza.d.tl62
-rw-r--r--teal-src/util/strbitop.d.tl6
-rw-r--r--teal-src/util/table.d.tl6
-rw-r--r--teal-src/util/time.d.tl6
-rw-r--r--teal-src/util/uuid.d.tl8
-rw-r--r--teal-src/util/xtemplate.tl101
31 files changed, 1579 insertions, 0 deletions
diff --git a/teal-src/util/compat.d.tl b/teal-src/util/compat.d.tl
new file mode 100644
index 00000000..da9c6083
--- /dev/null
+++ b/teal-src/util/compat.d.tl
@@ -0,0 +1,4 @@
+local record lib
+ xpcall : function (function, function, ...:any):boolean, any
+end
+return lib
diff --git a/teal-src/util/crand.d.tl b/teal-src/util/crand.d.tl
new file mode 100644
index 00000000..b40cb67e
--- /dev/null
+++ b/teal-src/util/crand.d.tl
@@ -0,0 +1,6 @@
+local record lib
+ bytes : function (n : integer) : string
+ enum sourceid "OpenSSL" "arc4random()" "Linux" end
+ _source : sourceid
+end
+return lib
diff --git a/teal-src/util/dataforms.d.tl b/teal-src/util/dataforms.d.tl
new file mode 100644
index 00000000..9e4170fa
--- /dev/null
+++ b/teal-src/util/dataforms.d.tl
@@ -0,0 +1,52 @@
+local stanza_t = require "util.stanza".stanza_t
+
+local enum form_type
+ "form"
+ "submit"
+ "cancel"
+ "result"
+end
+
+local enum field_type
+ "boolean"
+ "fixed"
+ "hidden"
+ "jid-multi"
+ "jid-single"
+ "list-multi"
+ "list-single"
+ "text-multi"
+ "text-private"
+ "text-single"
+end
+
+local record form_field
+
+ type : field_type
+ var : string -- protocol name
+ name : string -- internal name
+
+ label : string
+ desc : string
+
+ datatype : string
+ range_min : number
+ range_max : number
+
+ value : any -- depends on field_type
+ options : table
+end
+
+local record dataform
+ title : string
+ instructions : string
+ { form_field } -- XXX https://github.com/teal-language/tl/pull/415
+
+ form : function ( dataform, table, form_type ) : stanza_t
+end
+
+local record lib
+ new : function ( dataform ) : dataform
+end
+
+return lib
diff --git a/teal-src/util/datamapper.tl b/teal-src/util/datamapper.tl
new file mode 100644
index 00000000..73b1dfc0
--- /dev/null
+++ b/teal-src/util/datamapper.tl
@@ -0,0 +1,381 @@
+-- Copyright (C) 2021 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- Based on
+-- https://json-schema.org/draft/2020-12/json-schema-core.html
+-- https://json-schema.org/draft/2020-12/json-schema-validation.html
+-- http://spec.openapis.org/oas/v3.0.1#xmlObject
+-- https://github.com/OAI/OpenAPI-Specification/issues/630 (text:true)
+--
+-- XML Object Extensions:
+-- text to refer to the text content at the same time as attributes
+-- x_name_is_value for enum fields where the <tag-name/> is the value
+-- x_single_attribute for <tag attr="this"/>
+--
+-- TODO pointers
+-- TODO cleanup / refactor
+-- TODO s/number/integer/ once we have appropriate math.type() compat
+--
+
+local st = require "util.stanza";
+local json = require"util.json"
+local pointer = require"util.jsonpointer";
+
+local json_type_name = json.json_type_name;
+local json_schema_object = require "util.jsonschema"
+local type schema_t = boolean | json_schema_object
+
+local function toboolean ( s : string ) : boolean
+ if s == "true" or s == "1" then
+ return true
+ elseif s == "false" or s == "0" then
+ return false
+ elseif s then
+ return true
+ end
+end
+
+local function totype(t : json_type_name, s : string) : any
+ if not s then return nil end
+ if t == "string" then
+ return s;
+ elseif t == "boolean" then
+ return toboolean(s)
+ elseif t == "number" or t == "integer" then
+ return tonumber(s)
+ end
+end
+
+local enum value_goes
+ "in_tag_name"
+ "in_text"
+ "in_text_tag"
+ "in_attribute"
+ "in_single_attribute"
+ "in_children"
+ "in_wrapper"
+end
+
+local function resolve_schema(schema : schema_t, root : json_schema_object) : schema_t
+ if schema is json_schema_object then
+ if schema["$ref"] and schema["$ref"]:sub(1, 1) == "#" then
+ return pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t;
+ end
+ end
+ return schema;
+end
+
+local function guess_schema_type(schema : json_schema_object) : json_type_name
+ local schema_types = schema.type
+ if schema_types is json_type_name then
+ return schema_types
+ elseif schema_types ~= nil then
+ error "schema has unsupported 'type' property"
+ elseif schema.properties then
+ return "object"
+ elseif schema.items then
+ return "array"
+ end
+ return "string" -- default assumption
+end
+
+local function unpack_propschema( propschema : schema_t, propname : string, current_ns : string )
+ : json_type_name, value_goes, string, string, string, string, { any }
+ local proptype : json_type_name = "string"
+ local value_where : value_goes = propname and "in_text_tag" or "in_text"
+ local name = propname
+ local namespace : string
+ local prefix : string
+ local single_attribute : string
+ local enums : { any }
+
+ if propschema is json_schema_object then
+ proptype = guess_schema_type(propschema);
+ elseif propschema is string then -- Teal says this can never be a string, but it could before so best be sure
+ error("schema as string is not supported: "..propschema.." {"..current_ns.."}"..propname)
+ end
+
+ if proptype == "object" or proptype == "array" then
+ value_where = "in_children"
+ end
+
+ if propschema is json_schema_object then
+ local xml = propschema.xml
+ if xml then
+ if xml.name then
+ name = xml.name
+ end
+ if xml.namespace and xml.namespace ~= current_ns then
+ namespace = xml.namespace
+ end
+ if xml.prefix then
+ prefix = xml.prefix
+ end
+ if proptype == "array" and xml.wrapped then
+ value_where = "in_wrapper"
+ elseif xml.attribute then
+ value_where = "in_attribute"
+ elseif xml.text then
+ value_where = "in_text"
+ elseif xml.x_name_is_value then
+ value_where = "in_tag_name"
+ elseif xml.x_single_attribute then
+ single_attribute = xml.x_single_attribute
+ value_where = "in_single_attribute"
+ end
+ end
+ if propschema["const"] then
+ enums = { propschema["const"] }
+ elseif propschema["enum"] then
+ enums = propschema["enum"]
+ end
+ end
+
+ if current_ns == "urn:xmpp:reactions:0" and name == "reactions" then
+ assert(proptype=="array")
+ end
+
+ return proptype, value_where, name, namespace, prefix, single_attribute, enums
+end
+
+local parse_object : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any }
+local parse_array : function (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { any }
+
+local function extract_value (s : st.stanza_t, value_where : value_goes, proptype : json.json_type_name, name : string, namespace : string, prefix : string, single_attribute : string, enums : { any }) : string
+ if value_where == "in_tag_name" then
+ local c : st.stanza_t
+ if proptype == "boolean" then
+ c = s:get_child(name, namespace);
+ elseif enums and proptype == "string" then
+ -- XXX O(n²) ?
+ -- Probably better to flip the table and loop over :childtags(nil, ns), should be 2xO(n)
+ -- BUT works first, optimize later
+ for i = 1, #enums do
+ c = s:get_child(enums[i] as string, namespace);
+ if c then break end
+ end
+ else
+ c = s:get_child(nil, namespace);
+ end
+ if c then
+ return c.name;
+ end
+ elseif value_where == "in_attribute" then
+ local attr = name
+ if prefix then
+ attr = prefix .. ':' .. name
+ elseif namespace and namespace ~= s.attr.xmlns then
+ attr = namespace .. "\1" .. name
+ end
+ return s.attr[attr]
+
+ elseif value_where == "in_text" then
+ return s:get_text()
+
+ elseif value_where == "in_single_attribute" then
+ local c = s:get_child(name, namespace)
+ return c and c.attr[single_attribute]
+ elseif value_where == "in_text_tag" then
+ return s:get_child_text(name, namespace)
+ end
+end
+
+function parse_object (schema : schema_t, s : st.stanza_t, root : json_schema_object) : { string : any }
+ local out : { string : any } = {}
+ schema = resolve_schema(schema, root)
+ if schema is json_schema_object and schema.properties then
+ for prop, propschema in pairs(schema.properties) do
+ propschema = resolve_schema(propschema, root)
+
+ local proptype, value_where, name, namespace, prefix, single_attribute, enums = unpack_propschema(propschema, prop, s.attr.xmlns)
+
+ if value_where == "in_children" and propschema is json_schema_object then
+ if proptype == "object" then
+ local c = s:get_child(name, namespace)
+ if c then
+ out[prop] = parse_object(propschema, c, root);
+ end
+ elseif proptype == "array" then
+ local a = parse_array(propschema, s, root);
+ if a and a[1] ~= nil then
+ out[prop] = a;
+ end
+ else
+ error "unreachable"
+ end
+ elseif value_where == "in_wrapper" and propschema is json_schema_object and proptype == "array" then
+ local wrapper = s:get_child(name, namespace);
+ if wrapper then
+ out[prop] = parse_array(propschema, wrapper, root);
+ end
+ else
+ local value : string = extract_value (s, value_where, proptype, name, namespace, prefix, single_attribute, enums)
+
+ out[prop] = totype(proptype, value)
+ end
+ end
+ end
+
+ return out
+end
+
+function parse_array (schema : json_schema_object, s : st.stanza_t, root : json_schema_object) : { any }
+ local itemschema : schema_t = resolve_schema(schema.items, root);
+ local proptype, value_where, child_name, namespace, prefix, single_attribute, enums = unpack_propschema(itemschema, nil, s.attr.xmlns)
+ local attr_name : string
+ if value_where == "in_single_attribute" then -- FIXME this shouldn't be needed
+ value_where = "in_attribute";
+ attr_name = single_attribute;
+ end
+ local out : { any } = {}
+
+ if proptype == "object" then
+ if itemschema is json_schema_object then
+ for c in s:childtags(child_name, namespace) do
+ table.insert(out, parse_object(itemschema, c, root));
+ end
+ else
+ error "array items must be schema object"
+ end
+ elseif proptype == "array" then
+ if itemschema is json_schema_object then
+ for c in s:childtags(child_name, namespace) do
+ table.insert(out, parse_array(itemschema, c, root));
+ end
+ end
+ else
+ for c in s:childtags(child_name, namespace) do
+ local value : string = extract_value (c, value_where, proptype, attr_name or child_name, namespace, prefix, single_attribute, enums)
+
+ table.insert(out, totype(proptype, value));
+ end
+ end
+ return out;
+end
+
+local function parse (schema : json_schema_object, s : st.stanza_t) : table
+ local s_type = guess_schema_type(schema)
+ if s_type == "object" then
+ return parse_object(schema, s, schema)
+ elseif s_type == "array" then
+ return parse_array(schema, s, schema)
+ else
+ error "top-level scalars unsupported"
+ end
+end
+
+local function toxmlstring(proptype : json_type_name, v : any) : string
+ if proptype == "string" and v is string then
+ return v
+ elseif proptype == "number" and v is number then
+ return string.format("%g", v)
+ elseif proptype == "integer" and v is number then -- TODO is integer
+ return string.format("%d", v)
+ elseif proptype == "boolean" then
+ return v and "1" or "0"
+ end
+end
+
+local unparse : function (json_schema_object, table, string, string, st.stanza_t, json_schema_object) : st.stanza_t
+
+local function unparse_property(out : st.stanza_t, v : any, proptype : json_type_name, propschema : schema_t, value_where : value_goes, name : string, namespace : string, current_ns : string, prefix : string, single_attribute : string, root : json_schema_object)
+
+ if value_where == "in_attribute" then
+ local attr = name
+ if prefix then
+ attr = prefix .. ':' .. name
+ elseif namespace and namespace ~= current_ns then
+ attr = namespace .. "\1" .. name
+ end
+
+ out.attr[attr] = toxmlstring(proptype, v)
+ elseif value_where == "in_text" then
+ out:text(toxmlstring(proptype, v))
+ elseif value_where == "in_single_attribute" then
+ assert(single_attribute)
+ local propattr : { string : string } = {}
+
+ if namespace and namespace ~= current_ns then
+ propattr.xmlns = namespace
+ end
+
+ propattr[single_attribute] = toxmlstring(proptype, v)
+ out:tag(name, propattr):up();
+
+ else
+ local propattr : { string : string }
+ if namespace ~= current_ns then
+ propattr = { xmlns = namespace }
+ end
+ if value_where == "in_tag_name" then
+ if proptype == "string" and v is string then
+ out:tag(v, propattr):up();
+ elseif proptype == "boolean" and v == true then
+ out:tag(name, propattr):up();
+ end
+ elseif proptype == "object" and propschema is json_schema_object and v is table then
+ local c = unparse(propschema, v, name, namespace, nil, root);
+ if c then
+ out:add_direct_child(c);
+ end
+ elseif proptype == "array" and propschema is json_schema_object and v is table then
+ if value_where == "in_wrapper" then
+ local c = unparse(propschema, v, name, namespace, nil, root);
+ if c then
+ out:add_direct_child(c);
+ end
+ else
+ unparse(propschema, v, name, namespace, out, root);
+ end
+ else
+ out:text_tag(name, toxmlstring(proptype, v), propattr)
+ end
+ end
+end
+
+function unparse ( schema : json_schema_object, t : table, current_name : string, current_ns : string, ctx : st.stanza_t, root : json_schema_object ) : st.stanza_t
+
+ if root == nil then root = schema end
+
+ if schema.xml then
+ if schema.xml.name then
+ current_name = schema.xml.name
+ end
+ if schema.xml.namespace then
+ current_ns = schema.xml.namespace
+ end
+ -- TODO prefix?
+ end
+
+ local out = ctx or st.stanza(current_name, { xmlns = current_ns })
+
+ local s_type = guess_schema_type(schema)
+ if s_type == "object" then
+
+ for prop, propschema in pairs(schema.properties) do
+ propschema = resolve_schema(propschema, root)
+ local v = t[prop]
+
+ if v ~= nil then
+ local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(propschema, prop, current_ns)
+ unparse_property(out, v, proptype, propschema, value_where, name, namespace, current_ns, prefix, single_attribute, root)
+ end
+ end
+ return out;
+
+ elseif s_type == "array" then
+ local itemschema = resolve_schema(schema.items, root)
+ local proptype, value_where, name, namespace, prefix, single_attribute = unpack_propschema(itemschema, current_name, current_ns)
+ for _, item in ipairs(t as { string }) do
+ unparse_property(out, item, proptype, itemschema, value_where, name, namespace, current_ns, prefix, single_attribute, root)
+ end
+ return out;
+ end
+end
+
+return {
+ parse = parse,
+ unparse = unparse,
+}
diff --git a/teal-src/util/datetime.d.tl b/teal-src/util/datetime.d.tl
new file mode 100644
index 00000000..971e8f9c
--- /dev/null
+++ b/teal-src/util/datetime.d.tl
@@ -0,0 +1,11 @@
+-- TODO s/number/integer/ once Teal gets support for that
+
+local record lib
+ date : function (t : integer) : string
+ datetime : function (t : integer) : string
+ time : function (t : integer) : string
+ legacy : function (t : integer) : string
+ parse : function (t : string) : integer
+end
+
+return lib
diff --git a/teal-src/util/encodings.d.tl b/teal-src/util/encodings.d.tl
new file mode 100644
index 00000000..58aefa5e
--- /dev/null
+++ b/teal-src/util/encodings.d.tl
@@ -0,0 +1,27 @@
+-- TODO many actually return Maybe(String)
+local record lib
+ record base64
+ encode : function (s : string) : string
+ decode : function (s : string) : string
+ end
+ record stringprep
+ nameprep : function (s : string, strict : boolean) : string
+ nodeprep : function (s : string, strict : boolean) : string
+ resourceprep : function (s : string, strict : boolean) : string
+ saslprep : function (s : string, strict : boolean) : string
+ end
+ record idna
+ to_ascii : function (s : string) : string
+ to_unicode : function (s : string) : string
+ end
+ record utf8
+ valid : function (s : string) : boolean
+ length : function (s : string) : integer
+ end
+ record confusable
+ skeleton : function (s : string) : string
+ end
+ version : string
+end
+return lib
+
diff --git a/teal-src/util/error.d.tl b/teal-src/util/error.d.tl
new file mode 100644
index 00000000..05f52405
--- /dev/null
+++ b/teal-src/util/error.d.tl
@@ -0,0 +1,78 @@
+local enum error_type
+ "auth"
+ "cancel"
+ "continue"
+ "modify"
+ "wait"
+end
+
+local enum error_condition
+ "bad-request"
+ "conflict"
+ "feature-not-implemented"
+ "forbidden"
+ "gone"
+ "internal-server-error"
+ "item-not-found"
+ "jid-malformed"
+ "not-acceptable"
+ "not-allowed"
+ "not-authorized"
+ "policy-violation"
+ "recipient-unavailable"
+ "redirect"
+ "registration-required"
+ "remote-server-not-found"
+ "remote-server-timeout"
+ "resource-constraint"
+ "service-unavailable"
+ "subscription-required"
+ "undefined-condition"
+ "unexpected-request"
+end
+
+local record protoerror
+ type : error_type
+ condition : error_condition
+ text : string
+ code : integer
+end
+
+local record error
+ type : error_type
+ condition : error_condition
+ text : string
+ code : integer
+ context : { any : any }
+ source : string
+end
+
+local type compact_registry_item = { string, string, string, string }
+local type compact_registry = { compact_registry_item }
+local type registry = { string : protoerror }
+local type context = { string : any }
+
+local record error_registry_wrapper
+ source : string
+ registry : registry
+ new : function (string, context) : error
+ coerce : function (any, string) : any, error
+ wrap : function (error) : error
+ wrap : function (string, context) : error
+ is_error : function (any) : boolean
+end
+
+local record lib
+ record configure_opt
+ auto_inject_traceback : boolean
+ end
+ new : function (protoerror, context, { string : protoerror }, string) : error
+ init : function (string, string, registry | compact_registry) : error_registry_wrapper
+ init : function (string, registry | compact_registry) : error_registry_wrapper
+ is_error : function (any) : boolean
+ coerce : function (any, string) : any, error
+ from_stanza : function (table, context, string) : error
+ configure : function
+end
+
+return lib
diff --git a/teal-src/util/format.d.tl b/teal-src/util/format.d.tl
new file mode 100644
index 00000000..1ff77c97
--- /dev/null
+++ b/teal-src/util/format.d.tl
@@ -0,0 +1,4 @@
+local record lib
+ format : function (string, ... : any) : string
+end
+return lib
diff --git a/teal-src/util/hashes.d.tl b/teal-src/util/hashes.d.tl
new file mode 100644
index 00000000..cbb06f8e
--- /dev/null
+++ b/teal-src/util/hashes.d.tl
@@ -0,0 +1,23 @@
+local type hash = function (msg : string, hex : boolean) : string
+local type hmac = function (key : string, msg : string, hex : boolean) : string
+local type kdf = function (pass : string, salt : string, i : integer) : string
+
+local record lib
+ sha1 : hash
+ sha256 : hash
+ sha224 : hash
+ sha384 : hash
+ sha512 : hash
+ md5 : hash
+ hmac_sha1 : hmac
+ hmac_sha256 : hmac
+ hmac_sha512 : hmac
+ hmac_md5 : hmac
+ scram_Hi_sha1 : kdf
+ pbkdf2_hmac_sha1 : kdf
+ pbkdf2_hmac_sha256 : kdf
+ equals : function (string, string) : boolean
+ version : string
+ _LIBCRYPTO_VERSION : string
+end
+return lib
diff --git a/teal-src/util/hex.d.tl b/teal-src/util/hex.d.tl
new file mode 100644
index 00000000..3b216a88
--- /dev/null
+++ b/teal-src/util/hex.d.tl
@@ -0,0 +1,6 @@
+local type s2s = function (s : string) : string
+local record lib
+ to : s2s
+ from : s2s
+end
+return lib
diff --git a/teal-src/util/http.d.tl b/teal-src/util/http.d.tl
new file mode 100644
index 00000000..ecbe35c3
--- /dev/null
+++ b/teal-src/util/http.d.tl
@@ -0,0 +1,9 @@
+local record lib
+ urlencode : function (s : string) : string
+ urldecode : function (s : string) : string
+ formencode : function (f : { string : string }) : string
+ formdecode : function (s : string) : { string : string }
+ contains_token : function (field : string, token : string) : boolean
+ normalize_path : function (path : string) : string
+end
+return lib
diff --git a/teal-src/util/human/units.d.tl b/teal-src/util/human/units.d.tl
new file mode 100644
index 00000000..f6568d90
--- /dev/null
+++ b/teal-src/util/human/units.d.tl
@@ -0,0 +1,5 @@
+local lib = record
+ adjust : function (number, string) : number, string
+ format : function (number, string, string) : string
+end
+return lib
diff --git a/teal-src/util/id.d.tl b/teal-src/util/id.d.tl
new file mode 100644
index 00000000..4b6c93b7
--- /dev/null
+++ b/teal-src/util/id.d.tl
@@ -0,0 +1,9 @@
+local record lib
+ short : function () : string
+ medium : function () : string
+ long : function () : string
+ custom : function (integer) : function () : string
+
+end
+return lib
+
diff --git a/teal-src/util/interpolation.d.tl b/teal-src/util/interpolation.d.tl
new file mode 100644
index 00000000..fb653edf
--- /dev/null
+++ b/teal-src/util/interpolation.d.tl
@@ -0,0 +1,6 @@
+local type renderer = function (string, { string : any }) : string
+local type filter = function (string, any) : string
+local record lib
+ new : function (string, string, funcs : { string : filter }) : renderer
+end
+return lib
diff --git a/teal-src/util/jid.d.tl b/teal-src/util/jid.d.tl
new file mode 100644
index 00000000..897318d9
--- /dev/null
+++ b/teal-src/util/jid.d.tl
@@ -0,0 +1,15 @@
+local record lib
+ split : function (string) : string, string, string
+ bare : function (string) : string
+ prepped_split : function (string, boolean) : string, string, string
+ join : function (string, string, string) : string
+ prep : function (string, boolean) : string
+ compare : function (string, string) : boolean
+ node : function (string) : string
+ host : function (string) : string
+ resource : function (string) : string
+ escape : function (string) : string
+ unescape : function (string) : string
+end
+
+return lib
diff --git a/teal-src/util/json.d.tl b/teal-src/util/json.d.tl
new file mode 100644
index 00000000..a1c25004
--- /dev/null
+++ b/teal-src/util/json.d.tl
@@ -0,0 +1,18 @@
+local record lib
+ encode : function (any) : string
+ decode : function (string) : any, string
+
+ enum json_type_name
+ "null"
+ "boolean"
+ "object"
+ "array"
+ "number"
+ "string"
+ "integer"
+ end
+
+ type null_type = (nil)
+ null : null_type
+end
+return lib
diff --git a/teal-src/util/jsonpointer.tl b/teal-src/util/jsonpointer.tl
new file mode 100644
index 00000000..c21e1fbf
--- /dev/null
+++ b/teal-src/util/jsonpointer.tl
@@ -0,0 +1,46 @@
+
+local enum ptr_error
+ "invalid-table"
+ "invalid-path"
+end
+
+local function unescape_token(escaped_token : string) : string
+ local unescaped = escaped_token:gsub("~1", "/"):gsub("~0", "~")
+ return unescaped
+end
+
+local function resolve_json_pointer(ref : table, path : string) : any, ptr_error
+ local ptr_len = #path+1
+ for part, pos in path:gmatch("/([^/]*)()") do
+ local token = unescape_token(part)
+ if not ref is table then
+ return nil
+ end
+ local idx = next(ref)
+ local new_ref : any
+
+ if idx is string then
+ new_ref = ref[token]
+ elseif idx is integer then
+ local i = tonumber(token)
+ if token == "-" then i = #ref + 1 end
+ new_ref = ref[i+1]
+ else
+ return nil, "invalid-table"
+ end
+
+ if pos as integer == ptr_len then
+ return new_ref
+ elseif new_ref is table then
+ ref = new_ref
+ elseif not ref is table then
+ return nil, "invalid-path"
+ end
+
+ end
+ return ref
+end
+
+return {
+ resolve = resolve_json_pointer,
+}
diff --git a/teal-src/util/jsonschema.tl b/teal-src/util/jsonschema.tl
new file mode 100644
index 00000000..160c164c
--- /dev/null
+++ b/teal-src/util/jsonschema.tl
@@ -0,0 +1,374 @@
+-- Copyright (C) 2021 Kim Alvefur
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+-- Based on
+-- https://json-schema.org/draft/2020-12/json-schema-core.html
+-- https://json-schema.org/draft/2020-12/json-schema-validation.html
+--
+
+local json = require"util.json"
+local null = json.null;
+
+local pointer = require "util.jsonpointer"
+
+local type json_type_name = json.json_type_name
+
+-- json_type_name here is non-standard
+local type schema_t = boolean | json_schema_object
+
+local record json_schema_object
+ type json_type_name = json.json_type_name
+ type schema_object = json_schema_object
+
+ type : json_type_name | { json_type_name }
+ enum : { any }
+ const : any
+
+ allOf : { schema_t }
+ anyOf : { schema_t }
+ oneOf : { schema_t }
+
+ ["not"] : schema_t
+ ["if"] : schema_t
+ ["then"] : schema_t
+ ["else"] : schema_t
+
+ ["$ref"] : string
+
+ -- numbers
+ multipleOf : number
+ maximum : number
+ exclusiveMaximum : number
+ minimum : number
+ exclusiveMinimum : number
+
+ -- strings
+ maxLength : integer
+ minLength : integer
+ pattern : string -- NYI
+ format : string
+
+ -- arrays
+ prefixItems : { schema_t }
+ items : schema_t
+ contains : schema_t
+ maxItems : integer
+ minItems : integer
+ uniqueItems : boolean
+ maxContains : integer -- NYI
+ minContains : integer -- NYI
+
+ -- objects
+ properties : { string : schema_t }
+ maxProperties : integer -- NYI
+ minProperties : integer -- NYI
+ required : { string }
+ dependentRequired : { string : { string } }
+ additionalProperties: schema_t
+ patternProperties: schema_t -- NYI
+ propertyNames : schema_t
+
+ -- xml
+ record xml_t
+ name : string
+ namespace : string
+ prefix : string
+ attribute : boolean
+ wrapped : boolean
+
+ -- nonstantard, maybe in the future
+ text : boolean
+ x_name_is_value : boolean
+ x_single_attribute : string
+ end
+
+ xml : xml_t
+
+ -- descriptive
+ title : string
+ description : string
+ deprecated : boolean
+ readOnly : boolean
+ writeOnly : boolean
+
+ -- methods
+ validate : function ( schema_t, any, json_schema_object ) : boolean
+end
+
+-- TODO validator function per schema property
+
+local function simple_validate(schema : json_type_name | { json_type_name }, data : any) : boolean
+ if schema == nil then
+ return true
+ elseif schema == "object" and data is table then
+ return type(data) == "table" and (next(data)==nil or type((next(data, nil))) == "string")
+ elseif schema == "array" and data is table then
+ return type(data) == "table" and (next(data)==nil or type((next(data, nil))) == "number")
+ elseif schema == "integer" then
+ return math.type(data) == schema
+ elseif schema == "null" then
+ return data == null
+ elseif schema is { json_type_name } then
+ for _, one in ipairs(schema as { json_type_name }) do
+ if simple_validate(one, data) then
+ return true
+ end
+ end
+ return false
+ else
+ return type(data) == schema
+ end
+end
+
+local complex_validate : function ( json_schema_object, any, json_schema_object ) : boolean
+
+local function validate (schema : schema_t, data : any, root : json_schema_object) : boolean
+ if schema is boolean then
+ return schema
+ else
+ return complex_validate(schema, data, root)
+ end
+end
+
+function complex_validate (schema : json_schema_object, data : any, root : json_schema_object) : boolean
+
+ if root == nil then
+ root = schema
+ end
+
+ if schema["$ref"] and schema["$ref"]:sub(1,1) == "#" then
+ local referenced = pointer.resolve(root as table, schema["$ref"]:sub(2)) as schema_t
+ if referenced ~= nil and referenced ~= root and referenced ~= schema then
+ if not validate(referenced, data, root) then
+ return false;
+ end
+ end
+ end
+
+ if not simple_validate(schema.type, data) then
+ return false;
+ end
+
+ if schema.type == "object" then
+ if data is table then
+ -- just check that there the keys are all strings
+ for k in pairs(data) do
+ if not k is string then
+ return false
+ end
+ end
+ end
+ end
+
+ if schema.type == "array" then
+ if data is table then
+ -- just check that there the keys are all numbers
+ for i in pairs(data) do
+ if not i is integer then
+ return false
+ end
+ end
+ end
+ end
+
+ if schema["enum"] ~= nil then
+ local match = false
+ for _, v in ipairs(schema["enum"]) do
+ if v == data then
+ -- FIXME supposed to do deep-compare
+ match = true
+ break
+ end
+ end
+ if not match then
+ return false
+ end
+ end
+
+ -- XXX this is measured in byte, while JSON measures in ... bork
+ -- TODO use utf8.len?
+ if data is string then
+ if schema.maxLength and #data > schema.maxLength then
+ return false
+ end
+ if schema.minLength and #data < schema.minLength then
+ return false
+ end
+ end
+
+ if data is number then
+ if schema.multipleOf and (data == 0 or data % schema.multipleOf ~= 0) then
+ return false
+ end
+
+ if schema.maximum and not ( data <= schema.maximum ) then
+ return false
+ end
+
+ if schema.exclusiveMaximum and not ( data < schema.exclusiveMaximum ) then
+ return false
+ end
+
+ if schema.minimum and not ( data >= schema.minimum ) then
+ return false
+ end
+
+ if schema.exclusiveMinimum and not ( data > schema.exclusiveMinimum ) then
+ return false
+ end
+ end
+
+ if schema.allOf then
+ for _, sub in ipairs(schema.allOf) do
+ if not validate(sub, data, root) then
+ return false
+ end
+ end
+ end
+
+ if schema.oneOf then
+ local valid = 0
+ for _, sub in ipairs(schema.oneOf) do
+ if validate(sub, data, root) then
+ valid = valid + 1
+ end
+ end
+ if valid ~= 1 then
+ return false
+ end
+ end
+
+ if schema.anyOf then
+ local match = false
+ for _, sub in ipairs(schema.anyOf) do
+ if validate(sub, data, root) then
+ match = true
+ break
+ end
+ end
+ if not match then
+ return false
+ end
+ end
+
+ if schema["not"] then
+ if validate(schema["not"], data, root) then
+ return false
+ end
+ end
+
+ if schema["if"] ~= nil then
+ if validate(schema["if"], data, root) then
+ if schema["then"] then
+ return validate(schema["then"], data, root)
+ end
+ else
+ if schema["else"] then
+ return validate(schema["else"], data, root)
+ end
+ end
+ end
+
+ if schema.const ~= nil and schema.const ~= data then
+ return false
+ end
+
+ if data is table then
+
+ if schema.maxItems and #data > schema.maxItems then
+ return false
+ end
+
+ if schema.minItems and #data < schema.minItems then
+ return false
+ end
+
+ if schema.required then
+ for _, k in ipairs(schema.required) do
+ if data[k] == nil then
+ return false
+ end
+ end
+ end
+
+ if schema.propertyNames ~= nil then
+ for k in pairs(data) do
+ if not validate(schema.propertyNames, k, root) then
+ return false
+ end
+ end
+ end
+
+ if schema.properties then
+ for k, sub in pairs(schema.properties) do
+ if data[k] ~= nil and not validate(sub, data[k], root) then
+ return false
+ end
+ end
+ end
+
+ if schema.additionalProperties ~= nil then
+ for k, v in pairs(data) do
+ if schema.properties == nil or schema.properties[k as string] == nil then
+ if not validate(schema.additionalProperties, v, root) then
+ return false
+ end
+ end
+ end
+ end
+
+ if schema.uniqueItems then
+ -- only works for scalars, would need to deep-compare for objects/arrays/tables
+ local values : { any : boolean } = {}
+ for _, v in pairs(data) do
+ if values[v] then
+ return false
+ end
+ values[v] = true
+ end
+ end
+
+ local p = 0
+ if schema.prefixItems ~= nil then
+ for i, s in ipairs(schema.prefixItems) do
+ if data[i] == nil then
+ break
+ elseif validate(s, data[i], root) then
+ p = i
+ else
+ return false
+ end
+ end
+ end
+
+ if schema.items ~= nil then
+ for i = p+1, #data do
+ if not validate(schema.items, data[i], root) then
+ return false
+ end
+ end
+ end
+
+ if schema.contains ~= nil then
+ local found = false
+ for i = 1, #data do
+ if validate(schema.contains, data[i], root) then
+ found = true
+ break
+ end
+ end
+ if not found then
+ return false
+ end
+ end
+ end
+
+ return true;
+end
+
+
+json_schema_object.validate = validate;
+
+return json_schema_object;
diff --git a/teal-src/util/net.d.tl b/teal-src/util/net.d.tl
new file mode 100644
index 00000000..1040fcef
--- /dev/null
+++ b/teal-src/util/net.d.tl
@@ -0,0 +1,13 @@
+
+local enum type_strings
+ "both"
+ "ipv4"
+ "ipv6"
+end
+
+local record lib
+ local_addresses : function (type_strings, boolean) : { string }
+ pton : function (string):string
+ ntop : function (string):string
+end
+return lib
diff --git a/teal-src/util/poll.d.tl b/teal-src/util/poll.d.tl
new file mode 100644
index 00000000..8df56d57
--- /dev/null
+++ b/teal-src/util/poll.d.tl
@@ -0,0 +1,31 @@
+local record state
+ enum waiterr
+ "timeout"
+ "signal"
+ end
+ add : function (state, integer, boolean, boolean) : boolean
+ add : function (state, integer, boolean, boolean) : nil, string, integer
+ set : function (state, integer, boolean, boolean) : boolean
+ set : function (state, integer, boolean, boolean) : nil, string, integer
+ del : function (state, integer) : boolean
+ del : function (state, integer) : nil, string, integer
+ wait : function (state, integer) : integer, boolean, boolean
+ wait : function (state, integer) : nil, string, integer
+ wait : function (state, integer) : nil, waiterr
+ getfd : function (state) : integer
+end
+
+local record lib
+ new : function () : state
+ EEXIST : integer
+ EMFILE : integer
+ ENOENT : integer
+ enum api_backend
+ "epoll"
+ "poll"
+ "select"
+ end
+ api : api_backend
+end
+
+return lib
diff --git a/teal-src/util/pposix.d.tl b/teal-src/util/pposix.d.tl
new file mode 100644
index 00000000..68f49730
--- /dev/null
+++ b/teal-src/util/pposix.d.tl
@@ -0,0 +1,108 @@
+local record pposix
+ enum syslog_facility
+ "auth"
+ "authpriv"
+ "cron"
+ "daemon"
+ "ftp"
+ "kern"
+ "local0"
+ "local1"
+ "local2"
+ "local3"
+ "local4"
+ "local5"
+ "local6"
+ "local7"
+ "lpr"
+ "mail"
+ "syslog"
+ "user"
+ "uucp"
+ end
+
+ enum syslog_level
+ "debug"
+ "info"
+ "notice"
+ "warn"
+ "error"
+ end
+
+ enum ulimit_resource
+ "CORE"
+ "CPU"
+ "DATA"
+ "FSIZE"
+ "NOFILE"
+ "STACK"
+ "MEMLOCK"
+ "NPROC"
+ "RSS"
+ "NICE"
+ end
+
+ enum ulimit_unlimited
+ "unlimited"
+ end
+
+ type ulimit_limit = integer | ulimit_unlimited
+
+ record utsname
+ sysname : string
+ nodename : string
+ release : string
+ version : string
+ machine : string
+ domainname : string
+ end
+
+ record memoryinfo
+ allocated : integer
+ allocated_mmap : integer
+ used : integer
+ unused : integer
+ returnable : integer
+ end
+
+ abort : function ()
+
+ daemonize : function () : boolean, string
+
+ syslog_open : function (ident : string, facility : syslog_facility)
+ syslog_close : function ()
+ syslog_log : function (level : syslog_level, src : string, msg : string)
+ syslog_setminlevel : function (level : syslog_level)
+
+ getpid : function () : integer
+ getuid : function () : integer
+ getgid : function () : integer
+
+ setuid : function (uid : integer | string) : boolean, string -- string|integer
+ setgid : function (uid : integer | string) : boolean, string
+ initgroups : function (user : string, gid : integer) : boolean, string
+
+ umask : function (umask : string) : string
+
+ mkdir : function (dir : string) : boolean, string
+
+ setrlimit : function (resource : ulimit_resource, soft : ulimit_limit, hard : ulimit_limit) : boolean, string
+ getrlimit : function (resource : ulimit_resource) : boolean, ulimit_limit, ulimit_limit
+ getrlimit : function (resource : ulimit_resource) : boolean, string
+
+ uname : function () : utsname
+
+ setenv : function (key : string, value : string) : boolean
+
+ meminfo : function () : memoryinfo
+
+ atomic_append : function (f : FILE, s : string) : boolean, string, integer
+
+ isatty : function(FILE) : boolean
+
+ ENOENT : integer
+ _NAME : string
+ _VESRION : string
+end
+
+return pposix
diff --git a/teal-src/util/random.d.tl b/teal-src/util/random.d.tl
new file mode 100644
index 00000000..83ff2fcc
--- /dev/null
+++ b/teal-src/util/random.d.tl
@@ -0,0 +1,4 @@
+local record lib
+ bytes : function (n:integer):string
+end
+return lib
diff --git a/teal-src/util/ringbuffer.d.tl b/teal-src/util/ringbuffer.d.tl
new file mode 100644
index 00000000..e4726d68
--- /dev/null
+++ b/teal-src/util/ringbuffer.d.tl
@@ -0,0 +1,20 @@
+local record lib
+ record ringbuffer
+ find : function (ringbuffer, string) : integer
+ discard : function (ringbuffer, integer) : boolean
+ read : function (ringbuffer, integer, boolean) : string
+ readuntil : function (ringbuffer, string) : string
+ write : function (ringbuffer, string) : integer
+ size : function (ringbuffer) : integer
+ length : function (ringbuffer) : integer
+ sub : function (ringbuffer, integer, integer) : string
+ byte : function (ringbuffer, integer, integer) : integer...
+ free : function (ringbuffer) : integer
+ end
+
+ new : function (integer) : ringbuffer
+end
+
+return lib
+
+
diff --git a/teal-src/util/signal.d.tl b/teal-src/util/signal.d.tl
new file mode 100644
index 00000000..8610aa7f
--- /dev/null
+++ b/teal-src/util/signal.d.tl
@@ -0,0 +1,41 @@
+local record lib
+ enum signal
+ "SIGABRT"
+ "SIGALRM"
+ "SIGBUS"
+ "SIGCHLD"
+ "SIGCLD"
+ "SIGCONT"
+ "SIGFPE"
+ "SIGHUP"
+ "SIGILL"
+ "SIGINT"
+ "SIGIO"
+ "SIGIOT"
+ "SIGKILL"
+ "SIGPIPE"
+ "SIGPOLL"
+ "SIGPROF"
+ "SIGQUIT"
+ "SIGSEGV"
+ "SIGSTKFLT"
+ "SIGSTOP"
+ "SIGSYS"
+ "SIGTERM"
+ "SIGTRAP"
+ "SIGTTIN"
+ "SIGTTOU"
+ "SIGURG"
+ "SIGUSR1"
+ "SIGUSR2"
+ "SIGVTALRM"
+ "SIGWINCH"
+ "SIGXCPU"
+ "SIGXFSZ"
+ end
+ signal : function (integer | signal, function, boolean) : boolean
+ raise : function (integer | signal)
+ kill : function (integer, integer | signal)
+ -- enum : integer
+end
+return lib
diff --git a/teal-src/util/smqueue.tl b/teal-src/util/smqueue.tl
new file mode 100644
index 00000000..e149dde7
--- /dev/null
+++ b/teal-src/util/smqueue.tl
@@ -0,0 +1,99 @@
+local queue = require "util.queue";
+
+local record lib
+ -- T would typically be util.stanza
+ record smqueue<T>
+ _queue : queue.queue<T>
+ _head : integer
+ _tail : integer
+
+ enum ack_errors
+ "tail"
+ "head"
+ "pop"
+ end
+ push : function (smqueue, T)
+ ack : function (smqueue, integer) : { T }, ack_errors
+ resumable : function (smqueue<T>) : boolean
+ resume : function (smqueue<T>) : queue.queue.iterator, any, integer
+ type consume_iter = function (smqueue<T>) : T
+ consume : function (smqueue<T>) : consume_iter
+
+ table : function (smqueue<T>) : { T }
+ end
+ new : function <T>(integer) : smqueue<T>
+end
+
+local type smqueue = lib.smqueue;
+
+function smqueue:push(v)
+ self._head = self._head + 1;
+ -- Wraps instead of errors
+ assert(self._queue:push(v));
+end
+
+function smqueue:ack(h : integer) : { any }, smqueue.ack_errors
+ if h < self._tail then
+ return nil, "tail";
+ elseif h > self._head then
+ return nil, "head";
+ end
+ -- TODO optimize? cache table fields
+ local acked = {};
+ self._tail = h;
+ local expect = self._head - self._tail;
+ while expect < self._queue:count() do
+ local v = self._queue:pop();
+ if not v then return nil, "pop"; end
+ table.insert(acked, v);
+ end
+ return acked;
+end
+
+function smqueue:count_unacked() : integer
+ return self._head - self._tail;
+end
+
+function smqueue:count_acked() : integer
+ return self._tail;
+end
+
+function smqueue:resumable() : boolean
+ return self._queue:count() >= (self._head - self._tail);
+end
+
+function smqueue:resume() : queue.queue.iterator, any, integer
+ return self._queue:items();
+end
+
+function smqueue:consume() : queue.queue.consume_iter
+ return self._queue:consume()
+end
+
+-- Compatibility layer, plain ol' table
+function smqueue:table() : { any }
+ local t : { any } = {};
+ for i, v in self:resume() do
+ t[i] = v;
+ end
+ return t;
+end
+
+local function freeze(q : smqueue<any>) : { string:integer }
+ return { head = q._head, tail = q._tail }
+end
+
+local queue_mt = {
+ --
+ __name = "smqueue";
+ __index = smqueue;
+ __len = smqueue.count_unacked;
+ __freeze = freeze;
+}
+
+function lib.new<T>(size : integer) : queue.queue<T>
+ assert(size>0);
+ return setmetatable({ _head = 0; _tail = 0; _queue = queue.new(size, true) }, queue_mt);
+end
+
+return lib;
diff --git a/teal-src/util/stanza.d.tl b/teal-src/util/stanza.d.tl
new file mode 100644
index 00000000..a358248a
--- /dev/null
+++ b/teal-src/util/stanza.d.tl
@@ -0,0 +1,62 @@
+local record lib
+
+ type children_iter = function ( stanza_t ) : stanza_t
+ type childtags_iter = function () : stanza_t
+ type maptags_cb = function ( stanza_t ) : stanza_t
+
+ record stanza_t
+ name : string
+ attr : { string : string }
+ { stanza_t | string }
+ tags : { stanza_t }
+
+ query : function ( stanza_t, string ) : stanza_t
+ body : function ( stanza_t, string, { string : string } ) : stanza_t
+ text_tag : function ( stanza_t, string, string, { string : string } ) : stanza_t
+ tag : function ( stanza_t, string, { string : string } ) : stanza_t
+ text : function ( stanza_t, string ) : stanza_t
+ up : function ( stanza_t ) : stanza_t
+ reset : function ( stanza_t ) : stanza_t
+ add_direct_child : function ( stanza_t, stanza_t )
+ add_child : function ( stanza_t, stanza_t )
+ remove_children : function ( stanza_t, string, string ) : stanza_t
+
+ get_child : function ( stanza_t, string, string ) : stanza_t
+ get_text : function ( stanza_t ) : string
+ get_child_text : function ( stanza_t, string, string ) : string
+ child_with_name : function ( stanza_t, string, string ) : stanza_t
+ child_with_ns : function ( stanza_t, string, string ) : stanza_t
+ children : function ( stanza_t ) : children_iter, stanza_t, integer
+ childtags : function ( stanza_t, string, string ) : childtags_iter
+ maptags : function ( stanza_t, maptags_cb ) : stanza_t
+ find : function ( stanza_t, string ) : stanza_t | string
+
+ top_tag : function ( stanza_t ) : string
+ pretty_print : function ( stanza_t ) : string
+ pretty_top_tag : function ( stanza_t ) : string
+
+ get_error : function ( stanza_t ) : string, string, string, stanza_t
+ indent : function ( stanza_t, integer, string ) : stanza_t
+ end
+
+ record serialized_stanza_t
+ name : string
+ attr : { string : string }
+ { serialized_stanza_t | string }
+ end
+
+ stanza : function ( string, { string : string } ) : stanza_t
+ is_stanza : function ( any ) : boolean
+ preserialize : function ( stanza_t ) : serialized_stanza_t
+ deserialize : function ( serialized_stanza_t ) : stanza_t
+ clone : function ( stanza_t, boolean ) : stanza_t
+ message : function ( { string : string }, string ) : stanza_t
+ iq : function ( { string : string } ) : stanza_t
+ reply : function ( stanza_t ) : stanza_t
+ error_reply : function ( stanza_t, string, string, string, string )
+ presence : function ( { string : string } ) : stanza_t
+ xml_escape : function ( string ) : string
+ pretty_print : function ( string ) : string
+end
+
+return lib
diff --git a/teal-src/util/strbitop.d.tl b/teal-src/util/strbitop.d.tl
new file mode 100644
index 00000000..010efdb8
--- /dev/null
+++ b/teal-src/util/strbitop.d.tl
@@ -0,0 +1,6 @@
+local record mod
+ sand : function (string, string) : string
+ sor : function (string, string) : string
+ sxor : function (string, string) : string
+end
+return mod
diff --git a/teal-src/util/table.d.tl b/teal-src/util/table.d.tl
new file mode 100644
index 00000000..0ff5ed95
--- /dev/null
+++ b/teal-src/util/table.d.tl
@@ -0,0 +1,6 @@
+local record lib
+ create : function (narr:integer, nrec:integer):table
+ pack : function (...:any):{any}
+end
+return lib
+
diff --git a/teal-src/util/time.d.tl b/teal-src/util/time.d.tl
new file mode 100644
index 00000000..e159706b
--- /dev/null
+++ b/teal-src/util/time.d.tl
@@ -0,0 +1,6 @@
+
+local record lib
+ now : function () : number
+ monotonic : function () : number
+end
+return lib
diff --git a/teal-src/util/uuid.d.tl b/teal-src/util/uuid.d.tl
new file mode 100644
index 00000000..45fd4312
--- /dev/null
+++ b/teal-src/util/uuid.d.tl
@@ -0,0 +1,8 @@
+local record lib
+ get_nibbles : (number) : string
+ generate : function () : string
+
+ seed : function (string)
+end
+return lib
+
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 };