summaryrefslogtreecommitdiff
path: root/macros/luatex/latex
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2025-01-02 03:04:34 +0000
committerNorbert Preining <norbert@preining.info>2025-01-02 03:04:34 +0000
commit2eea71934e853f5e45633ccf0f7915b4f133919a (patch)
treeb5d38f640b78d108926cd12a371f857bdecbb202 /macros/luatex/latex
parenta22c35c9797ac178b8552f754a91fa976d850a53 (diff)
CTAN sync 202501020304
Diffstat (limited to 'macros/luatex/latex')
-rw-r--r--macros/luatex/latex/luatbls/README.md27
-rw-r--r--macros/luatex/latex/luatbls/luatbls.lua310
-rw-r--r--macros/luatex/latex/luatbls/luatbls.pdfbin0 -> 73500 bytes
-rw-r--r--macros/luatex/latex/luatbls/luatbls.sty163
-rw-r--r--macros/luatex/latex/luatbls/luatbls.tex410
5 files changed, 910 insertions, 0 deletions
diff --git a/macros/luatex/latex/luatbls/README.md b/macros/luatex/latex/luatbls/README.md
new file mode 100644
index 0000000000..b2fc6681de
--- /dev/null
+++ b/macros/luatex/latex/luatbls/README.md
@@ -0,0 +1,27 @@
+# luatbls
+
+Lua tables made accessible in LaTeX
+
+# License
+
+Copyright (C) 2025 Kale Ewasiuk
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/macros/luatex/latex/luatbls/luatbls.lua b/macros/luatex/latex/luatbls/luatbls.lua
new file mode 100644
index 0000000000..a14310fc6d
--- /dev/null
+++ b/macros/luatex/latex/luatbls/luatbls.lua
@@ -0,0 +1,310 @@
+--% Kale Ewasiuk (kalekje@gmail.com)
+--% 2025-01-01
+--% Copyright (C) 2025 Kale Ewasiuk
+--%
+--% Permission is hereby granted, free of charge, to any person obtaining a copy
+--% of this software and associated documentation files (the "Software"), to deal
+--% in the Software without restriction, including without limitation the rights
+--% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+--% copies of the Software, and to permit persons to whom the Software is
+--% furnished to do so, subject to the following conditions:
+--%
+--% The above copyright notice and this permission notice shall be included in
+--% all copies or substantial portions of the Software.
+--%
+--% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+--% ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+--% TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+--% PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+--% SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+--% ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+--% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+--% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+--% OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+local luatbls = {}
+
+luatbls._luakeys = require'luakeys'()
+
+luatbls._rec_tbl = ''
+luatbls._rec_tbl_opts = {}
+
+luatbls._xysep = '%s+' -- spaces separate x-y coordinates
+luatbls._tblv = '<v>'
+luatbls._tblk = '<k>'
+luatbls._cspfx = 'dtbl'
+luatbls._debug = false
+
+function luatbls._dprint(s1, s2)
+ if luatbls._debug then
+ penlight.wrth(s1, s1)
+ end
+end
+
+
+setmetatable(luatbls, {__call=function(t,s)return t._get_tbl_item(s) end})
+
+function luatbls._get_tbl_name(s)
+ s = s:strip()
+ if s == '' then
+ return luatbls._rec_tbl
+ end
+ for _, delim in ipairs{'.', '/', '|'} do
+ s = s:split(delim)[1] -- if tbl reference had a . | or /, an indexer was used
+ end
+ if luatbls[s] == nil then
+ local validtbls = penlight.List(penlight.tablex.kkeys(luatbls)):filter(function(s) return not s:startswith('_') end):join(', ')
+ penlight.tex.pkgerror('luatbls', 'Tried to access undefined tbl: "'..s..'". Valid tbls are: '..validtbls)
+ return luatbls._rec_tbl
+ end
+ return s
+end
+
+function luatbls._get_tbl(s)
+ s = luatbls._get_tbl_name(s)
+ return luatbls[s]
+end
+
+function luatbls._get_tbl_index(s, undec)
+ undec = undec or false -- flag for allowing undeclared indexing
+ local tbl = ''
+ local key = ''
+ local s_raw = s
+ if s:find('%.') then
+ local tt = s:split('.')
+ tbl = tt[1]
+ key = tt[2]
+ elseif s:find('/') then
+ local tt = s:split('/')
+ tbl = tt[1]
+ if tbl == '' then tbl = luatbls._rec_tbl end
+ key = tonumber(tonumber(tt[2]))
+ if key < 0 then key = #luatbls[tbl]+1+key end
+ else
+ tbl = luatbls._rec_tbl
+ key = tonumber(s) or s
+ if type(key) == 'number' and key < 0 then key = #luatbls[tbl]+1+key end
+ end
+ if tbl == '' then tbl = luatbls._rec_tbl end
+
+ if (luatbls[tbl] == nil) or ((not undec) and (luatbls[tbl][key] == nil)) then
+ penlight.tex.pkgerror('luatbls', 'Invalid tbl index attempt using: "'..s_raw..'". We tried to use tbl="' ..tbl..'" and key="'..key..'"'..
+ 'Note that "|" is forbidden here. The recent table is: '..luatbls._rec_tbl)
+ end
+ return tbl, key
+end
+
+function luatbls._get_tbl_seq(s)
+ local tblseq = s:split('|')
+ local tbl = nil
+ local seq = nil
+ if #tblseq == 1 then
+ tbl = luatbls._get_tbl_name('')
+ seq = tblseq[1]
+ if seq == '' then seq = ':,*' end
+ elseif #tblseq == 2 then
+ tbl = luatbls._get_tbl_name(tblseq[1])
+ seq = tblseq[2]
+ if seq == '' then seq = ':,*' end
+ end
+ return tbl, seq
+end
+
+
+
+function luatbls._get_tbl_item(s, p) -- get item with string, p means print value
+ p = p or false
+ local tbl, key = luatbls._get_tbl_index(s)
+ local itm = luatbls[tbl][key]
+ if p then
+ tex.sprint(tostring(itm))
+ end
+ return itm
+end
+
+
+function luatbls._set_tbl_item(s, v)
+ tbl, key = luatbls._get_tbl_index(s)
+ luatbls[tbl][key] = v
+end
+
+function luatbls._check_recent_tbl_undefault()
+ local undefaults = {}
+ if luatbls._rec_tbl_opts ~= nil then
+ local defaults = penlight.tablex.union(
+ luatbls._rec_tbl_opts.defs or {},
+ luatbls._rec_tbl_opts.defaults or {}
+ )
+ for k, v in pairs(luatbls[luatbls._rec_tbl]) do
+ if defaults[k] == nil then
+ undefaults[#undefaults+1] = k
+ end
+ end
+ if penlight.hasval(undefaults) then
+ penlight.tex.pkgerror('luatbls',
+ 'Invalid keys passed to tbl keyval: ' .. (', '):join(undefaults) ..
+ ' ; choices are: ' .. (', '):join(penlight.tablex.keys(defaults))
+ )
+ end
+ end
+end
+
+function luatbls._make_alpha_key(k)
+ if tonumber(k) ~= nil then
+ k = penlight.Char(tonumber(k))
+ end
+ return k
+end
+
+function luatbls._def_tbl(ind, def, g)
+ local _tbl, _key = luatbls._get_tbl_index(ind)
+ if def == '' then def = luatbls._cspfx.._tbl..luatbls._make_alpha_key(_key) end
+ luatbls._def_tbl_one(luatbls[_tbl][_key], def, g)
+end
+
+
+function luatbls._def_tbl_some(tblseq, def, g)
+ for t, k, v in luatbls._iter_tbls_vals(tblseq) do
+ if def == '' then def = luatbls._cspfx..t end
+ local def2 = def .. luatbls._make_alpha_key(k)
+ luatbls._def_tbl_one(v, def2, g)
+ end
+end
+
+function luatbls._def_tbl_one(v, cs, g)
+ if type(v) == 'table' then
+ for kk, vv in pairs(v) do
+ kk = luatbls._make_alpha_key(kk)
+ token.set_macro(cs..kk, tostring(vv), g)
+ end
+ else
+ token.set_macro(cs, tostring(v), g)
+ end
+end
+
+function luatbls._def_tbl_coords(ind, def)
+ local tbl, key = luatbls._get_tbl_index(ind)
+ local str = luatbls[tbl][key]
+ if def == '' then def = luatbls._cspfx..tbl..key end
+ local x, y = str:strip():splitv(luatbls._xysep)
+ if (not penlight.hasval(x)) or (not penlight.hasval(y)) then
+ penlight.tex.pkgerror('luatbls', '_def_tbl_coords function could not parse coordiantes given as "'..str..'" ensure two numbers separated by space are given!', '', true)
+ end
+ token.set_macro(def..'x', tostring(x))
+ token.set_macro(def..'y', tostring(y))
+end
+
+
+function luatbls._for_tbl_prt(k, v,cmd)
+ local cmd_new = cmd:gsub(luatbls._tblv, tostring(v)):gsub(luatbls._tblk, tostring(k)):gsub('(\\%w+) ', '%1') -- for some reason a space gets added to \cs, maybe
+ luatbls._dprint(cmd_new, '_for_tbl replacement')
+ tex.sprint(cmd_new)
+end
+
+function luatbls._for_tbl(tblseq, cmd)
+ for t, k, v in luatbls._iter_tbls_vals(tblseq) do
+ luatbls._for_tbl_prt(k, v,cmd)
+ end
+end
+
+function luatbls._for_tbl_e(tbl, cmd)
+ for k, v in pairs(tbl) do
+ luatbls._for_tbl_prt(k, v,cmd)
+ end
+end
+
+
+function luatbls._iter_tbls_vals(s)
+ if s:find('|') or ((s:find('%.') == nil) and (s:find('/') == nil)) then
+ local tbl, seq = luatbls._get_tbl_seq(s)
+ local keyval = {}
+ for key, val in penlight.seq.tbltrain(luatbls._get_tbl(tbl), seq) do
+ -- todo this should check validity of keys for sequences
+ keyval[#keyval+1] = {key, val}
+ end
+ luatbls._dprint(keyval, 'luatbls._iter_tbls_vals is iterating through tbl: '..tbl..' with sequence: '..seq)
+ local i = 0
+ return function()
+ i = i + 1
+ if i <= #keyval then
+ return tbl, keyval[i][1], keyval[i][2]
+ end
+ end
+ else
+ local tbl, key = luatbls._get_tbl_index(s)
+ local val = luatbls[tbl][key]
+ luatbls._dprint(keyval, 'luatbls._iter_tbls_vals is using tbl: '..tbl..' with key: '..key)
+ local i = 1
+ return function()
+ if i == 1 then
+ i = i + 1 -- only return the single value
+ return tbl, key, val
+ end
+ end
+ end
+end
+
+function luatbls._make_args(s, key, val)
+ local args = {val}
+ if s == nil then
+ return args
+ end
+ s = s:split(')')[1]
+ args = luatbls._luakeys.parse(s, {naked_as_value=true})
+ for i, v in ipairs(args) do
+ if v == luatbls._tblv then
+ args[i] = val
+ elseif v == luatbls._tblk then
+ args[i] = key
+ end
+ end
+ return args
+end
+
+function luatbls._make_func(f, key, val)
+ f = f:strip()
+ fargs = f:split('(')
+ f = fargs[1]
+ args = luatbls._make_args(fargs[2], key, val)
+ if f:startswith(':') then
+ f = f:sub(2,-1)
+ if type(val) == 'string' then
+ f = string[f]
+ elseif type(val) == 'table' then
+ f = penlight.tablex[f]
+ end
+ else
+ f = penlight._Gdot(f)
+ end
+ --luatbls._dprint() -- todo more debug printing
+ return f, args
+end
+
+
+
+function luatbls._make_newtbl(tblind, newtbl)
+ if tblind == '' then tblind = luatbls._rec_tbl..'|' end
+ if newtbl ~= '' then -- determine if new tbl is needed
+ local ogtbl = luatbls._get_tbl_name(tblind)
+ luatbls[newtbl] = penlight.tablex.deepcopy(luatbls[ogtbl])
+ luatbls._rec_tbl = newtbl
+ tblind, _ = string.gsub(tblind, ogtbl, newtbl, 1)
+ end
+ return tblind
+end
+
+function luatbls._apply_tbl(tblind, func, newtbl)
+ tblind = luatbls._make_newtbl(tblind, newtbl)
+ for _, f in pairs(func:split('|')) do
+ for tbl, key, val in luatbls._iter_tbls_vals(tblind) do
+ local thefunc, args = luatbls._make_func(f, key, val)
+ if thefunc == nil then
+ penlight.tex.pkgerror('luatbls', 'Tried to apply function: "'..f..'" to tbl value. It yielded no function. Ensure : is used for self-methods')
+ end
+ luatbls[tbl][key] = thefunc(penlight.utils.unpack(args))
+ end
+ end
+end
+
+return luatbls \ No newline at end of file
diff --git a/macros/luatex/latex/luatbls/luatbls.pdf b/macros/luatex/latex/luatbls/luatbls.pdf
new file mode 100644
index 0000000000..e8bc8a1694
--- /dev/null
+++ b/macros/luatex/latex/luatbls/luatbls.pdf
Binary files differ
diff --git a/macros/luatex/latex/luatbls/luatbls.sty b/macros/luatex/latex/luatbls/luatbls.sty
new file mode 100644
index 0000000000..cbbd5377a1
--- /dev/null
+++ b/macros/luatex/latex/luatbls/luatbls.sty
@@ -0,0 +1,163 @@
+% Kale Ewasiuk (kalekje@gmail.com)
+% 2025-01-01
+% Copyright (C) 2025 Kale Ewasiuk
+%
+% Permission is hereby granted, free of charge, to any person obtaining a copy
+% of this software and associated documentation files (the "Software"), to deal
+% in the Software without restriction, including without limitation the rights
+% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+% copies of the Software, and to permit persons to whom the Software is
+% furnished to do so, subject to the following conditions:
+%
+% The above copyright notice and this permission notice shall be included in
+% all copies or substantial portions of the Software.
+%
+% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+% ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+% TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+% PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+% SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+% ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+% OR OTHER DEALINGS IN THE SOFTWARE.
+
+\RequirePackage{luacode}
+\RequirePackage{luakeys}
+\RequirePackage{penlightplus}
+
+
+\luadirect{luatbls = require'luatbls'}
+
+
+\NewDocumentCommand{\tblnew}{m}{\luadirect{% initialize a tbl and set empty
+ luatbls[\luastring{#1}] = {}
+ luatbls._rec_tbl = \luastring{#1}
+}}
+
+
+\NewDocumentCommand{\tblchg}{ m }{\luadirect{% change recent table
+ luatbls._rec_tbl = luatbls._get_tbl_name(\luastring{#1})
+}}
+
+\NewDocumentCommand{\tblfrkv}{m +m O{}}{\luadirect{% parse a tbl from key-vals, naked are set to boolean
+ luatbls._rec_tbl_opts = luatbls._luakeys.parse(\luastring{#3})
+ luatbls[\luastring{#1}] = luatbls._luakeys.parse(string.subpar(\luastring{#2}), luatbls._rec_tbl_opts)
+ luatbls._rec_tbl = \luastring{#1}
+}}
+
+\NewDocumentCommand{\tblfrkvN}{m +m O{}}{\luadirect{%
+ luatbls._rec_tbl_opts = luatbls._luakeys.parse(\luastring{#3})
+ luatbls[\luastring{#1}] = luatbls._luakeys.parse(string.subpar(\luastringN{#2}), luatbls._rec_tbl_opts)
+ luatbls._rec_tbl = \luastring{#1}
+}}
+
+\NewDocumentCommand{\tblfrcsv}{m +m O{}}{\tblfrkv{#1}{#2}[naked_as_value=true,#3]}% naked are set to array
+
+\NewDocumentCommand{\tblfrcsvN}{m +m O{}}{\tblfrkvN{#1}{#2}[naked_as_value=true,#3]}
+
+\NewDocumentCommand{\tblkvundefcheck}{}{\luadirect{luatbls._check_recent_tbl_undefault()}}% check defaults list and throw error if foreign keys were used
+
+
+
+\NewDocumentCommand{\tblapp}{m m}{\luadirect{% append to a table (ie using integer index) with a value (second arg)
+ __tbl__ = luatbls._get_tbl_name(\luastring{#1})
+ table.insert(luatbls[__tbl__], \luastring{#2})
+}}
+
+
+\NewDocumentCommand{\tblappN}{m m}{\luadirect{% append to a table (ie using integer index) with a value (second arg) #
+ __tbl__ = luatbls._get_tbl_name(\luastring{#1})
+ table.insert(luatbls[__tbl__], \luastringN{#2})
+}}
+
+
+\NewDocumentCommand{\tblcon}{m m}{\luadirect{% concatenate to a table (ie using integer index) with a list of comma separated values (second arg) #
+ __tbl__ = luatbls._get_tbl_name(\luastring{#1})
+ for k, v in ipairs(luatbls._luakeys.parse(string.subpar(\luastring{#2}), {naked_as_value=true})) do
+ table.insert(luatbls[__tbl__], v)
+ end
+}}
+
+
+\NewDocumentCommand{\tblconN}{m m}{\luadirect{% concatenate to a table (ie using integer index) with a list of comma separated values (second arg) #
+ __tbl__ = luatbls._get_tbl_name(\luastring{#1})
+ for k, v in ipairs(luatbls._luakeys.parse(string.subpar(\luastringN{#2}), {naked_as_value=true})) do
+ table.insert(luatbls[__tbl__], v)
+ end
+}}
+
+
+\NewDocumentCommand{\tbladd}{m m}{\luadirect{% add a kv pair to a table
+ __tbl__, __key__ = luatbls._get_tbl_index(\luastring{#1}, true)
+ luatbls[__tbl__][__key__] = \luastring{#2}
+}}
+
+\NewDocumentCommand{\tbladdN}{m m}{\luadirect{% add a kv pair to a table
+ __tbl__, __key__ = luatbls._get_tbl_index(\luastring{#1}, true)
+ luatbls[__tbl__][__key__] = \luastringN{#2}
+}}
+
+
+\NewDocumentCommand{\tblupd}{m m}{\luadirect{% update to a table (ie using integer index) with a list of comma separated values (second arg) #
+ __tbl__ = luatbls._get_tbl_name(\luastring{#1})
+ __tbl_new__ = luatbls._luakeys.parse(string.subpar(\luastring{#2})
+ penlight.tablex.update(luatbls[__tbl__], __tbl_new__)
+}}
+
+
+\NewDocumentCommand{\tblupdN}{m m}{\luadirect{% update to a table (ie using integer index) with a list of comma separated values (second arg) #
+ __tbl__ = luatbls._get_tbl_name(\luastring{#1})
+ __tbl_new__ = luatbls._luakeys.parse(string.subpar(\luastringN{#2})
+ penlight.tablex.update(luatbls[__tbl__], __tbl_new__)
+}}
+
+
+
+
+\NewDocumentCommand{\tblget}{m}{\luadirect{% get an item using i syntax
+ luatbls._get_tbl_item(\luastring{#1}, true)
+}}
+
+\NewDocumentCommand{\tblgetE}{m}{\luadirect{% get an item explicitly
+ tex.sprint(tostring(luatbls.#1))
+}}
+
+\NewDocumentCommand{\tblset}{m m}{\luadirect{% set item with {value}
+ luatbls._set_tbl_item(\luastring{#1}, \luastring{#2})
+}}
+
+\NewDocumentCommand{\tblsetN}{m m}{\luadirect{% set item with {value}
+ luatbls._set_tbl_item(\luastring{#1}, \luastringN{#2})
+}}
+
+
+\NewDocumentCommand{\tblif}{ m m O{}}{\ifluax{luatbls._get_tbl_item(\luastring{#1})}{#2}[#3]}
+
+\NewDocumentCommand{\tblifv}{m m O{}}{\ifluaxv{luatbls._get_tbl_item(\luastring{#1})}{#2}[#3]}
+
+\NewDocumentCommand{\tblifeq}{m m m O{}}{\ifluax{luatbls._get_tbl_item(\luastring{#1}) == #2}{#3}[#4]}
+
+
+\NewDocumentCommand{\tblfor}{ m m }{\luadirect{luatbls._for_tbl(\luastring{#1}, \luastring{#2})}}
+\NewDocumentCommand{\tblforN}{ m m }{\luadirect{luatbls._for_tbl(\luastring{#1}, \luastringN{#2})}}
+
+\NewDocumentCommand{\tblforE}{ m m }{\luadirect{luatbls._for_tbl_e(luatbls.#1, \luastring{#2})}}
+\NewDocumentCommand{\tblforEN}{ m m }{\luadirect{luatbls._for_tbl_e(luatbls.#1, \luastringN{#2})}}
+
+
+\NewDocumentCommand{\tbldef}{ m m }{\luadirect{luatbls._def_tbl(\luastring{#1}, \luastring{#2})}}
+\NewDocumentCommand{\tblgdef}{ m m }{\luadirect{luatbls._def_tbl(\luastring{#1}, \luastring{#2}, 'global')}}
+
+\NewDocumentCommand{\tbldefs}{ m O{} }{\luadirect{luatbls._def_tbl_some(\luastring{#1}, \luastring{#2})}}
+\NewDocumentCommand{\tblgdefs}{ m O{} }{\luadirect{luatbls._def_tbl_some(\luastring{#1}, \luastring{#2}, 'global')}}
+
+\NewDocumentCommand{\tbldefxy}{ m O{} }{\luadirect{luatbls._def_tbl_coords(\luastring{#1}, \luastring{#2})}}% define #2x and #2y from a space delimited x-y pair
+
+\NewDocumentCommand{\tblapply}{m m O{}}{\luadirect{luatbls._apply_tbl(\luastring{#1}, \luastring{#2}, \luastring{#3})}}
+
+\NewDocumentCommand{\tblprt}{s m}{%
+ \luadirect{penlight.wrth(luatbls._get_tbl(\luastring{#2}),'luatbls: '..\luastring{#2})}%
+ \IfBooleanTF{#1}{\PackageError{luatbls}{Program terminated by tblprt*}{}\stop}{}%
+}
+
diff --git a/macros/luatex/latex/luatbls/luatbls.tex b/macros/luatex/latex/luatbls/luatbls.tex
new file mode 100644
index 0000000000..13e9dbf30b
--- /dev/null
+++ b/macros/luatex/latex/luatbls/luatbls.tex
@@ -0,0 +1,410 @@
+% Kale Ewasiuk (kalekje@gmail.com)
+% 2025-01-01
+% Copyright (C) 2025 Kale Ewasiuk
+%
+% Permission is hereby granted, free of charge, to any person obtaining a copy
+% of this software and associated documentation files (the "Software"), to deal
+% in the Software without restriction, including without limitation the rights
+% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+% copies of the Software, and to permit persons to whom the Software is
+% furnished to do so, subject to the following conditions:
+%
+% The above copyright notice and this permission notice shall be included in
+% all copies or substantial portions of the Software.
+%
+% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+% ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+% TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+% PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+% SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+% ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+% OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+\documentclass[11pt,parskip=half]{scrartcl}
+\usepackage[left=0.75in,right=0.75in,top=1in,bottom=1in]{geometry}
+\setlength{\parindent}{0ex}
+\newcommand{\llcmd}[1]{\leavevmode\llap{\texttt{\detokenize{#1}}}}
+\newcommand{\cmd}[1]{\texttt{\detokenize{#1}}}
+\newcommand{\qcmd}[1]{``\cmd{#1}''}
+\usepackage{url}
+\usepackage[svgnames]{xcolor}
+\usepackage{showexpl}
+\lstset{explpreset={justification=\raggedright,pos=r,hsep=1cm,preset={\color{Navy}\small}}}
+\setlength\ResultBoxRule{0mm}
+\lstset{
+ language=[LaTeX]TeX,
+ basicstyle=\ttfamily\footnotesize\color{blue},
+ commentstyle=\ttfamily\footnotesize\color{gray},
+ frame=none,
+ numbers=left,
+ numberstyle=\ttfamily\footnotesize\color{gray},
+ prebreak=\raisebox{0ex}[0ex][0ex]{\color{gray}\ensuremath{\hookleftarrow}},
+ extendedchars=true,
+ breaklines=true,
+ tabsize=4,
+}
+\addtokomafont{title}{\raggedright}
+\addtokomafont{author}{\raggedright}
+\addtokomafont{date}{\raggedright}
+\author{Kale Ewasiuk (\url{kalekje@gmail.com})}
+\usepackage[yyyymmdd]{datetime}\renewcommand{\dateseparator}{--}
+\date{\today}
+
+\usepackage{enumitem}
+
+\RequirePackage{luatbls}
+
+\title{luatbls}
+\subtitle{Create, modify, and use Lua tables from within LaTeX}
+
+
+\begin{document}
+%
+
+\maketitle
+
+
+\subsection*{Introduction}
+
+This package provides a Lua-table interface based on the \cmd{luakeys} package:\\
+\url{https://mirror.quantum5.ca/CTAN/macros/luatex/generic/luakeys/luakeys.pdf}\\
+
+A global table called \cmd{luatbls} is created by loading this package. This table contains all user-defined tables
+as well as internal package functions and settings.
+User tables are stored directly under the module's table, so you can access a table within Lua by using:
+\cmd{luatbls['mytable']} or \cmd{luatbls.mytable}. Further, \cmd{luatbls} can be called directly to obtain a table item by \cmd{luatbls'i'},
+where \cmd{i} is a ``flexible'' indexing system discussed in the next paragraphs.
+
+If you want to change the \cmd{luakeys} global parser options, you can adjust them by:\\
+\cmd{\directlua{luatbls._luakeys.opts.OPTION = VALUE}}\\
+For debugging, set \cmd{\directlua{luatbls._debug = true}}
+
+
+In this documentation, arguments are represented as follows:\\
+\llcmd{t }: table name. If none provided, the most recent is used.\\
+\llcmd{k }: a string key.\\
+\llcmd{n }: an integer index.\\
+\llcmd{v }: a value.\\
+\llcmd{i }: the flexible indexer to get a single item.\\
+\llcmd{I }: the flexible indexer to get a single or multiple items.\\
+\llcmd{keyval }: a key-value string for the table. Standalone values are set to boolean.\\
+\llcmd{csv }: a key-value string where standalone values are parsed as array-like.
+
+There are a few ways to use the index (placeholder \cmd{i}.\\
+\cmd{t.k} where \cmd{t} is the table name and \cmd{k} is a string key (i.e. uses \cmd{luatbls.t.k}),\\
+\cmd{t/n} where \cmd{n} is an integer index (i.e. uses \cmd{t.k[n]}); note that negative indexes are allowed where -1 is the last element.
+Alternatively, \cmd{t} and the symbol can be omitted, and simply pass the element without the table name as a prefix,
+where the assumed table is the last one that was created or changed to (i.e. the most 'recent' table).
+In this case, passing a number will assume an integer index.
+
+To use a \cmd{I},
+you can explicitly select tables and groups of keys by \cmd{t|seq}, or \cmd{t.k}, or \cmd{t/n}.
+If no \cmd{|./} is provided, the recent table is used and the argument is assumed to be a sequence of keys.
+\cmd{penlightplus}'s command \cmd{penlight.seq.tbltrain()} syntax is used for \cmd{seq}uences.
+To summarize what \cmd{seq} can be, a comma-separated list of numbers or keys are used to specify which elements are iterated over.
+NumPy-like slicing is possibly with \cmd{:} to choose integer ranges.
+If \cmd{*} is provided,
+all string keys are iterated.
+If \cmd{I} is entirely blank, all elements of the recent table are used, which is equivalent to \cmd{t|*,:}.\\
+
+Note: nested tables are currently not fully supported. Some variations of commands have an \cmd{E} suffix which indicates
+that nested elements can be explicitly indexed.
+The table name must be specified, and the validity of table names and keys are not checked.
+
+The \cmd{tbl} commands fully expand the
+\cmd{t}, \cmd{k}, \cmd{n}, \cmd{i}, and \cmd{I} arguments. However a variation with an \cmd{N}-appended is usually provided
+which will not expand the \cmd{v}, \cmd{keyval}, or \cmd{csv} args.
+
+
+
+%%%
+\subsection*{Creating Tables}
+
+%\tblfrkv{kale}{kale=ewasiuk}
+%\luadirect{penlight.wrth(luatbls'kale.kale')}
+
+\cmd{\tblnew{t}} declares a new table with name \cmd{t}\\
+\cmd{\tblchg{t}} changes the 'recent' table\\
+\\
+\cmd{\tblfrkv{t}{keyval}[opts]} new table from key-vals using \cmd{luakeys} \\
+\cmd{\tblfrkvN{t}{keyval}[opts]} does not expand key-val string \cmd{luakeys} \\
+
+\cmd{\tblkvundefcheck} will throw an error if you use define a table from key-values
+and use a key that was not specified in the luakeys parse options via \cmd{opts.defaults} or \cmd{opts.defs}.\\
+\\
+ \cmd{\tblfrcsv{t}{csv}[opts]} a shorthand \cmd{\tblfrkv{t}{csv}[naked_as_value=true,opts]}, a good way to convert a comma-separated list to an array\\
+ \cmd{\tblfrcsvN{t}{csv}[opts]} same as above, but the csv is not expanded.\\
+
+
+\subsection*{Setting, getting, and modifying}
+
+
+\cmd{\tblset{i}{v}} sets a value of the table/index \cmd{i} to \cmd{v}\\
+\cmd{\tblsetN{i}{v}} same as above, but the value is not expanded.\\
+\\
+\cmd{\tblget{i}} gets the value and \cmd{tex.sprint()}s it\\
+\cmd{\tblgetE{t.k}} An 'explicit' version of tbl get. Use this for nested tables. The tbl name must be specified.
+The validity of table names and keys are not checked.\\
+
+\begin{LTXexample}
+\tblfrkv{ex}{a,b,c,first=john,last=smith}%
+ [defaults={x=0,1=one,n=false,y=yes}]
+\tblget{ex.a}\\
+\tblset{a}{tRuE!!}
+\tblget{a}\\
+\tblget{ex.x}\\
+\tblget{.x}\\
+\tbladd{ex.newkey}{val}\tblget{newkey}\\
+\tbladd{nk}{VAL}\tblget{nk}\\
+\end{LTXexample}
+
+\begin{LTXexample}
+\tblfrcsv{EX}{x={1,2,{1,2,3}},name=me}
+\tblgetE{EX.x[1]}\\
+\tblgetE{EX.x[3][3]}\\
+\tblgetE{EX.name}\\
+\end{LTXexample}
+
+
+
+\cmd{\tbladd{i}{v}} add a new value to a table using index method\\
+\cmd{\tbladdN{i}{v}} above, but don't expand the value argument\\
+\\
+\cmd{\tblapp{t}{v}} append a \cmd{v}alue (integer-wise) to a \cmd{t}able\\
+\cmd{\tblappN{t}{v}}\\
+\\
+\cmd{\tblupd{t}{keyval}} update a \cmd{t}able with more \cmd{keyval}s\\
+\cmd{\tblupdN{t}{keyval}}\\
+\\
+\cmd{\tblcon{t}{csv}} concatenate array-style \cmd{csv} at the end of \cmd{t}\\
+\cmd{\tblconN{t}{csv}}\\
+
+
+
+\subsection*{Conditionals}
+
+\cmd{\tblif{i}{tr}[fa]} runs code \cmd{tr} if the item is true else \cmd{fa}\\
+\cmd{\tblifv{i}{tr}[fa]} runs code \cmd{tr} if the item is truth-y (using \cmd{pl.hasval}) else \cmd{fa}\\
+\cmd{\tblifeq{i}{v}{tr}[fa]} checks the equivalency of to a user-specified value. Quotes must be used to indicate strings.
+
+\begin{LTXexample}
+\tblfrcsv{x}{n=false,y=true,
+ k0="",kv=val,k2=6}
+\tblif{n}{tr}[FA]\\
+\tblif{k0}{TR}[fa]\\
+\tblifv{k0}{tr}[FA]\\
+\tblifeq{kv}{'val'}{TR}[fa]\\
+\tblifeq{k2}{6}{TR}[fa]\\
+\end{LTXexample}
+
+
+\subsection*{Iterating}
+%TODO use PIPE SYMBOLZ \dsadsa
+
+\cmd{\tblfor{I}{template}} and \cmd{\tblforN} By default, iterates over all elements (\cmd{seq = *,:}),
+but arbitrary indices/keys can be iterated over as per \cmd{penlight.seq.tbltrain} syntax.
+\cmd{<k>} and \cmd{<v>} are placeholders in the template that are replaced by the keys and vals and can be changed by:
+\cmd{\luadirect{luatbls._tblv = '<v>'}}
+
+If you want to iterate over a second-level tabel, you must use:\\
+\cmd{\tblforE} and \cmd{\tblforEN}, and explicitly provide the table and element.
+
+\begin{LTXexample}
+\tblfrcsv{x}{n1,k1=v1,n2,n3,n4,
+ k2=v2,k3=v3,n5,n6}
+1> \tblfor{:}{<k> = <v>; }\\
+2> \tblfor{*}{<k> = <v>; }\\
+3> \tblfor{1,*,2::2}{<k> = <v>; }\\
+4> \tblfor{ x | 1,*,2::2}{<k> = <v>; }\\
+\tblfrcsv{x}{a,{a,b,c}}
+5> \tblforE{x[2]}{<k> = <v>; }
+\end{LTXexample}
+
+
+
+
+\subsection*{Definitions}
+\cmd{\tbldef{i}[cs]} pushes the value to macro \cmd{\cs}. If cs is not provided, a default cs name of \cmd{dtbl<t><k>} is used.\\
+\cmd{\tblgdef{i}[cs]} like above but global definition is used.\\
+
+\cmd{\tbldefs{I}[cspfx]} and \cmd{\tblgdefs{I}[cspfx]} defines items in table \cmd{t} (use recent if blank) with format \cmd{<cspfx><key>} where \cmd{cspfx} is a command sequence prefix.
+If \cmd{cspfx} is blank, keys will be defined as \cmd{dtbl<t><k>}.
+The default \cmd{cspfx} is changed by:\\\cmd{\luadirect{luatbls._cspfx = 'dtbl'}}\\
+Numerical keys are converted to capital letters: 1->A, 2->B.
+It is recommended that tables and keys contain letters only for predictable behaviour when using this feature.\\
+If the value of a tbl's key is a table, every element in that table is defined, and the keys of that nested table is
+appended to the cs: \cmd{dtbl<t><k1><k2>} (noting that numbers are converted to letters).\\
+
+
+\begin{LTXexample}
+\tblfrcsv{EX}{n1,kA=v1,n2,n3,n4,
+ kB=v2,kC=v3,n5,n6}
+1>\tbldef{kA}{mycs}\mycs\tbldef{kA}{}\dtblEXkA\\
+2> \tbldef{EX/1}{}\dtblEXA
+\end{LTXexample}
+
+\begin{LTXexample}
+\tblfrcsv{EX}{x={1,2,3}}
+1>\tbldef{x}{mycs}\mycsA, \mycsB \\
+2>\tbldefs{}\dtblEXxA, \dtblEXxB
+\end{LTXexample}
+
+
+
+\cmd{\tbldefxy{i}[cspfx]} splits the value of item by space, and creates two definitions \cmd{<cspfx>x} and \cmd{<cspfx>y}.
+This might be useful for passing and using tikz coordinates, for example \cmd{xy=0 5}. An error is thrown if the values
+are non-numeric.\\
+
+\begin{LTXexample}[width=0.5\linewidth]
+\tblfrkv{EX}{coords=12 34,other}
+\tbldefxy{coords}[d]\dx, \dy \\
+\tbldefxy{coords}\dtblEXcoordsx, \dtblEXcoordsy \\
+\end{LTXexample}
+
+
+
+\subsection*{Utilities}
+
+\cmd{\tblapply{I}{func1(<v>,x,y)|:func2}[newtable]} apply a Lua function(s).\\If \cmd{newtable} is provided, a new table is created (and made the recent table)
+and the original table is preserved.\\ The \cmd{.}, \cmd{/} or \cmd{|} indexer may be used to apply a function
+to a single value or group of keys.
+Multiple functions can be applied sequentially, separated by \cmd{|}.
+
+An arbitrary global function (including additional arguments) can be used,
+but if a function is prefixed with a \cmd{:}, the class method will be called.
+The \cmd{stringx} and \cmd{tablex} methods
+from \cmd{penlight} are used depending on the value's type. See:\\
+\url{https://lunarmodules.github.io/Penlight/}
+
+Arguments can be specified with round brackets, where
+\cmd{<v>} and \cmd{<k>} are used as a placeholder for the values and keys.
+If no arguments are passed, it is assumed that the value is the only argument.
+Note that luakeys parses the args, so quotes are not needed around strings for the args.
+
+
+\begin{LTXexample}[width=0.5\linewidth]
+\tblfrcsv{ex}{{a, b, c}}
+\tblapply{}{:concat(<v>,-) | :upper}[new]
+1> \tblgetE{ex[1][1]}\\
+2> \tblget{new/1}\\
+\tblfrcsv{ex}{HelloWorld}
+\tblapply{}{string.sub(<v>,2,-5)}[new]
+3> \tblget{new/1}
+\end{LTXexample}
+
+%\begin{luacode*}
+% penlight.wrth(penlight.tablex.list2comma({'a','b','c'}),'XYZ')
+%\end{luacode*}
+
+%\tblprt*{ex}
+
+
+
+
+\cmd{\tblprt{t}} pretty-print the table in console. Using \cmd{\tblprt*{}} will terminate the LaTeX program immediately after and issue an error, which could be useful for debugging.
+
+
+
+\luadirect{luatbls._debug = true}
+
+\subsubsection*{An Example}
+%
+\begin{LTXexample}\scriptsize
+\NewDocumentCommand{\Exampletbl}{m}{
+ \tblfrcsv{ex}{#1}[defaults={sal=Hello}]
+ %\tblkvundefcheck
+ \tblapply{ex.auth}{:list2comma}
+ \tblget{sal}, \tblget{auth}! Thank you for writing such a great novel.
+ My favorite parts were:
+ \begin{description}
+ \tblforEN{ex.chaps}{\item[<k>] <v> }
+ \end{description}
+ It was also very cool to learn that
+ \tblgetE{ex.num[1]}*\tblgetE{ex.num[2]}=
+ \luadirect{tex.sprint(tostring(luatbls.ex.num[1]*luatbls.ex.num[2]))}
+}
+\Exampletbl{auth={You,Me,Dupree},
+ chaps={intro=very enticing, climax=thrilling, finale=what a twist!}
+ num={12,13}
+}
+\end{LTXexample}
+
+\tblprt{ex}
+
+
+
+
+
+
+\end{document}
+
+
+
+
+
+
+%\begin{luacode*}
+% function prt_pyth()
+% t = pl.tbls.pyth
+% if not t.a then
+% pl.tex.pkgerror('must pass a= to \\pyth')
+% elseif not t.b then
+% t.b = (tonumber(t.c)^2 -
+% tonumber(t.a)^2)^0.5
+% elseif not t.c then
+% t.c = (tonumber(t.a)^2 +
+% tonumber(t.b)^2)^0.5
+% end
+% local t = pl.tbx.fmt(t,'.'..t.d..'f') -- format table according to d decimals
+% s = 'Right-angle sides a=$a and b=$b form a hypotenuse of c=$c'
+% pl.tex.prt(s:fmt(t))
+% end
+%\end{luacode*}
+%\NewDocumentCommand{\pyth}{m}{%
+% \tblfrkv{pyth}{#1}[defaults={a=false,b=false,c=false,d=0,e=extras}]
+% \luadirect{prt_pyth()}%
+%}
+%
+%\pyth{a=3,c=5}\\
+%\pyth{a=3.2,b=4.2,d=2}\\
+%C: \tblget{c}
+%
+%\end{LTXexample}
+
+%
+
+%
+%\begin{luacode*}
+%function prttol()
+% local dec = penlight.tbls.tol[4] or 1
+% penlight.wrth(dec,'??')
+% penlight.tbls.tol[3] = penlight.tbls.tol[3] or 3
+% penlight.tbls.tol[4] = penlight.tbls.tol[1]*(1.0-penlight.tbls.tol[3]/100.0) + 0.0
+% penlight.tbls.tol[5] = penlight.tbls.tol[1]*(1.0+penlight.tbls.tol[3]/100.0) + 0.0
+% --penlight.tbls.tol['k'] = 'fuckboi'
+% --ttt = pl.tbx.fmt(penlight.tbls.tol, '.3f')
+% penlight.wrth(('$1\\$2 (\\pmpct{$3} tolerance, $4\\ndash$5\\$2)'):fmt(penlight.tbls.tol, '4=.'..dec..'f, 5=.'..dec..'f'), 'XYZ')
+%end
+%\end{luacode*}
+%\NewDocumentCommand{\prttol}{ m }{\tblfrcsv{tol}{#1}\luadirect{prttol()}}% {50.0,kV,3,P} % 50\us (\pmpct{20} tolerance, 40=--60\us), P is optional and precision of the range (number of decimals)
+%
+%\prttol{50,kV,3}
+%
+%\begin{luacode*}
+% pl.wrth(pl.filterfiles('.',true,'.*%.tex'), 'FF')
+%\end{luacode*}
+
+
+
+
+%\begin{luacode*}
+% for t, k, v in luatbls._iter_tbls_vals('my|*,:') do
+% --for t, k, v in luatbls._iter_tbls_vals('my/1') do
+% penlight.wrth({t,k,v})
+% end
+%\end{luacode*}
+
+