summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/penlight
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2022-02-28 03:02:40 +0000
committerNorbert Preining <norbert@preining.info>2022-02-28 03:02:40 +0000
commit7a8d1925127ad121a8f591db931524f9b8eacb93 (patch)
treedf5084184ea4d6732d6a9fae1d263b6915dbb36d /macros/luatex/generic/penlight
parenta911d39178540078c8ff5bc880418478d7433c40 (diff)
CTAN sync 202202280302
Diffstat (limited to 'macros/luatex/generic/penlight')
-rw-r--r--macros/luatex/generic/penlight/penlight.lua2256
-rw-r--r--macros/luatex/generic/penlight/penlight.pdfbin29642 -> 38237 bytes
-rw-r--r--macros/luatex/generic/penlight/penlight.sty12
-rw-r--r--macros/luatex/generic/penlight/penlight.tex109
-rw-r--r--macros/luatex/generic/penlight/penlightextras.lua738
5 files changed, 2030 insertions, 1085 deletions
diff --git a/macros/luatex/generic/penlight/penlight.lua b/macros/luatex/generic/penlight/penlight.lua
index bea9316071..4b2436be1b 100644
--- a/macros/luatex/generic/penlight/penlight.lua
+++ b/macros/luatex/generic/penlight/penlight.lua
@@ -21,7 +21,7 @@ do
--
-- This implements some useful things on [LOM](http://matthewwild.co.uk/projects/luaexpat/lom.html) documents, such as returned by `lxp.lom.parse`.
-- In particular, it can convert LOM back into XML text, with optional pretty-printing control.
--- It is s based on stanza.lua from [Prosody](http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua)
+-- It is based on stanza.lua from [Prosody](http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua)
--
-- > d = xml.parse "<nodes><node id='1'>alice</node></nodes>"
-- > = d
@@ -49,493 +49,880 @@ do
-- @module pl.xml
local utils = require 'pl.utils'
-local split = utils.split;
-local t_insert = table.insert;
-local t_concat = table.concat;
-local t_remove = table.remove;
-local s_match = string.match;
-local tostring = tostring;
-local setmetatable = setmetatable;
-local getmetatable = getmetatable;
-local pairs = pairs;
-local ipairs = ipairs;
-local type = type;
-local next = next;
-local print = print;
-local unpack = utils.unpack;
-local s_gsub = string.gsub;
-local s_find = string.find;
-local pcall,require,io = pcall,require,io
+local split = utils.split
+local t_insert = table.insert
+local t_concat = table.concat
+local t_remove = table.remove
+local s_match = string.match
+local tostring = tostring
+local setmetatable = setmetatable
+local getmetatable = getmetatable
+local pairs = pairs
+local ipairs = ipairs
+local type = type
+local next = next
+local print = print
+local unpack = utils.unpack
+local s_gsub = string.gsub
+local s_sub = string.sub
+local s_find = string.find
+local pcall = pcall
+local require = require
+
+
+utils.raise_deprecation {
+ source = "Penlight " .. utils._VERSION,
+ message = "the contents of module 'pl.xml' has been deprecated, please use a more specialized library instead",
+ version_removed = "2.0.0",
+ deprecated_after = "1.11.0",
+ no_trace = true,
+}
+
+
local _M = {}
local Doc = { __type = "doc" };
Doc.__index = Doc;
+
+local function is_text(s) return type(s) == 'string' end
+local function is_tag(d) return type(d) == 'table' and is_text(d.tag) end
+
+
+
--- create a new document node.
--- @param tag the tag name
--- @param attr optional attributes (table of name-value pairs)
+-- @tparam string tag the tag name
+-- @tparam[opt={}] table attr attributes (table of name-value pairs)
+-- @return the Node object
+-- @see xml.elem
+-- @usage
+-- local doc = xml.new("main", { hello = "world", answer = "42" })
+-- print(doc) --> <main hello='world' answer='42'/>
function _M.new(tag, attr)
- local doc = { tag = tag, attr = attr or {}, last_add = {}};
- return setmetatable(doc, Doc);
+ if type(tag) ~= "string" then
+ error("expected 'tag' to be a string value, got: " .. type(tag), 2)
+ end
+ attr = attr or {}
+ if type(attr) ~= "table" then
+ error("expected 'attr' to be a table value, got: " .. type(attr), 2)
+ end
+
+ local doc = { tag = tag, attr = attr, last_add = {}};
+ return setmetatable(doc, Doc);
end
---- parse an XML document. By default, this uses lxp.lom.parse, but
--- falls back to basic_parse, or if use_basic is true
--- @param text_or_file file or string representation
+
+--- parse an XML document. By default, this uses lxp.lom.parse, but
+-- falls back to basic_parse, or if `use_basic` is truthy
+-- @param text_or_filename file or string representation
-- @param is_file whether text_or_file is a file name or not
-- @param use_basic do a basic parse
-- @return a parsed LOM document with the document metatatables set
-- @return nil, error the error can either be a file error or a parse error
-function _M.parse(text_or_file, is_file, use_basic)
- local parser,status,lom
- if use_basic then parser = _M.basic_parse
+function _M.parse(text_or_filename, is_file, use_basic)
+ local parser,status,lom
+ if use_basic then
+ parser = _M.basic_parse
+ else
+ status,lom = pcall(require,'lxp.lom')
+ if not status then
+ parser = _M.basic_parse
else
- status,lom = pcall(require,'lxp.lom')
- if not status then parser = _M.basic_parse else parser = lom.parse end
+ parser = lom.parse
end
- if is_file then
- local f,err = io.open(text_or_file)
- if not f then return nil,err end
- text_or_file = f:read '*a'
- f:close()
+ end
+
+ if is_file then
+ local text_or_filename, err = utils.readfile(text_or_filename)
+ if not text_or_filename then
+ return nil, err
end
- local doc,err = parser(text_or_file)
- if not doc then return nil,err end
- if lom then
- _M.walk(doc,false,function(_,d)
- setmetatable(d,Doc)
- end)
+ end
+
+ local doc, err = parser(text_or_filename)
+ if not doc then
+ return nil, err
+ end
+
+ if lom then
+ _M.walk(doc, false, function(_, d)
+ setmetatable(d, Doc)
+ end)
+ end
+ return doc
+end
+
+
+--- Create a Node with a set of children (text or Nodes) and attributes.
+-- @tparam string tag a tag name
+-- @tparam table|string items either a single child (text or Node), or a table where the hash
+-- part is the attributes and the list part is the children (text or Nodes).
+-- @return the new Node
+-- @see xml.new
+-- @see xml.tags
+-- @usage
+-- local doc = xml.elem("top", "hello world") -- <top>hello world</top>
+-- local doc = xml.elem("main", xml.new("child")) -- <main><child/></main>
+-- local doc = xml.elem("main", { "this ", "is ", "nice" }) -- <main>this is nice</main>
+-- local doc = xml.elem("main", { xml.new "this",
+-- xml.new "is",
+-- xml.new "nice" }) -- <main><this/><is/><nice/></main>
+-- local doc = xml.elem("main", { hello = "world" }) -- <main hello='world'/>
+-- local doc = xml.elem("main", {
+-- "prefix",
+-- xml.elem("child", { "this ", "is ", "nice"}),
+-- "postfix",
+-- attrib = "value"
+-- }) -- <main attrib='value'>prefix<child>this is nice</child>postfix</main>"
+function _M.elem(tag, items)
+ local s = _M.new(tag)
+ if is_text(items) then items = {items} end
+ if is_tag(items) then
+ t_insert(s,items)
+ elseif type(items) == 'table' then
+ for k,v in pairs(items) do
+ if is_text(k) then
+ s.attr[k] = v
+ t_insert(s.attr,k)
+ else
+ s[k] = v
+ end
+ end
+ end
+ return s
+end
+
+
+--- given a list of names, return a number of element constructors.
+-- If passing a comma-separated string, then whitespace surrounding the values
+-- will be stripped.
+--
+-- The returned constructor functions are a shortcut to `xml.elem` where you
+-- no longer provide the tag-name, but only the `items` table.
+-- @tparam string|table list a list of names, or a comma-separated string.
+-- @return (multiple) constructor functions; `function(items)`. For the `items`
+-- parameter see `xml.elem`.
+-- @see xml.elem
+-- @usage
+-- local new_parent, new_child = xml.tags 'mom, kid'
+-- doc = new_parent {new_child 'Bob', new_child 'Annie'}
+-- -- <mom><kid>Bob</kid><kid>Annie</kid></mom>
+function _M.tags(list)
+ local ctors = {}
+ if is_text(list) then
+ list = split(list:match("^%s*(.-)%s*$"),'%s*,%s*')
+ end
+ for i,tag in ipairs(list) do
+ local function ctor(items)
+ return _M.elem(tag,items)
end
- return doc
+ ctors[i] = ctor
+ end
+ return unpack(ctors)
end
----- convenient function to add a document node, This updates the last inserted position.
--- @param tag a tag name
--- @param attrs optional set of attributes (name-string pairs)
+
+--- Adds a document Node, at current position.
+-- This updates the last inserted position to the new Node.
+-- @tparam string tag the tag name
+-- @tparam[opt={}] table attrs attributes (table of name-value pairs)
+-- @return the current node (`self`)
+-- @usage
+-- local doc = xml.new("main")
+-- doc:addtag("penlight", { hello = "world"})
+-- doc:addtag("expat") -- added to 'penlight' since position moved
+-- print(doc) --> <main><penlight hello='world'><expat/></penlight></main>
function Doc:addtag(tag, attrs)
- local s = _M.new(tag, attrs);
- (self.last_add[#self.last_add] or self):add_direct_child(s);
- t_insert(self.last_add, s);
- return self;
+ local s = _M.new(tag, attrs)
+ self:add_child(s)
+ t_insert(self.last_add, s)
+ return self
end
---- convenient function to add a text node. This updates the last inserted position.
--- @param text a string
+
+--- Adds a text node, at current position.
+-- @tparam string text a string
+-- @return the current node (`self`)
+-- @usage
+-- local doc = xml.new("main")
+-- doc:text("penlight")
+-- doc:text("expat")
+-- print(doc) --> <main><penlightexpat</main>
function Doc:text(text)
- (self.last_add[#self.last_add] or self):add_direct_child(text);
- return self;
+ self:add_child(text)
+ return self
end
----- go up one level in a document
+
+--- Moves current position up one level.
+-- @return the current node (`self`)
function Doc:up()
- t_remove(self.last_add);
- return self;
+ t_remove(self.last_add)
+ return self
end
+
+--- Resets current position to top level.
+-- Resets to the `self` node.
+-- @return the current node (`self`)
function Doc:reset()
- local last_add = self.last_add;
- for i = 1,#last_add do
- last_add[i] = nil;
- end
- return self;
+ local last_add = self.last_add
+ for i = 1,#last_add do
+ last_add[i] = nil
+ end
+ return self
end
---- append a child to a document directly.
+
+--- Append a child to the currrent Node (ignoring current position).
-- @param child a child node (either text or a document)
+-- @return the current node (`self`)
+-- @usage
+-- local doc = xml.new("main")
+-- doc:add_direct_child("dog")
+-- doc:add_direct_child(xml.new("child"))
+-- doc:add_direct_child("cat")
+-- print(doc) --> <main>dog<child/>cat</main>
function Doc:add_direct_child(child)
- t_insert(self, child);
+ t_insert(self, child)
+ return self
end
---- append a child to a document at the last element added
+
+--- Append a child at the current position (without changing position).
-- @param child a child node (either text or a document)
+-- @return the current node (`self`)
+-- @usage
+-- local doc = xml.new("main")
+-- doc:addtag("one")
+-- doc:add_child(xml.new("item1"))
+-- doc:add_child(xml.new("item2"))
+-- doc:add_child(xml.new("item3"))
+-- print(doc) --> <main><one><item1/><item2/><item3/></one></main>
function Doc:add_child(child)
- (self.last_add[#self.last_add] or self):add_direct_child(child);
- return self;
+ (self.last_add[#self.last_add] or self):add_direct_child(child)
+ return self
end
+
--accessing attributes: useful not to have to expose implementation (attr)
--but also can allow attr to be nil in any future optimizations
---- set attributes of a document node.
--- @param t a table containing attribute/value pairs
-function Doc:set_attribs (t)
- for k,v in pairs(t) do
- self.attr[k] = v
- end
+
+--- Set attributes of a document node.
+-- Will add/overwite values, but will not remove existing ones.
+-- Operates on the Node itself, will not take position into account.
+-- @tparam table t a table containing attribute/value pairs
+-- @return the current node (`self`)
+function Doc:set_attribs(t)
+ -- TODO: keep array part in sync
+ for k,v in pairs(t) do
+ self.attr[k] = v
+ end
+ return self
end
---- set a single attribute of a document node.
+
+--- Set a single attribute of a document node.
+-- Operates on the Node itself, will not take position into account.
-- @param a attribute
--- @param v its value
+-- @param v its value, pass in `nil` to delete the attribute
+-- @return the current node (`self`)
function Doc:set_attrib(a,v)
- self.attr[a] = v
+ -- TODO: keep array part in sync
+ self.attr[a] = v
+ return self
end
---- access the attributes of a document node.
+
+--- Gets the attributes of a document node.
+-- Operates on the Node itself, will not take position into account.
+-- @return table with attributes (attribute/value pairs)
function Doc:get_attribs()
- return self.attr
+ return self.attr
end
-local function is_text(s) return type(s) == 'string' end
---- function to create an element with a given tag name and a set of children.
--- @param tag a tag name
--- @param items either text or a table where the hash part is the attributes and the list part is the children.
-function _M.elem(tag,items)
- local s = _M.new(tag)
- if is_text(items) then items = {items} end
- if _M.is_tag(items) then
- t_insert(s,items)
- elseif type(items) == 'table' then
- for k,v in pairs(items) do
- if is_text(k) then
- s.attr[k] = v
- t_insert(s.attr,k)
- else
- s[k] = v
- end
- end
- end
- return s
-end
---- given a list of names, return a number of element constructors.
--- @param list a list of names, or a comma-separated string.
--- @usage local parent,children = doc.tags 'parent,children' <br>
--- doc = parent {child 'one', child 'two'}
-function _M.tags(list)
- local ctors = {}
- if is_text(list) then list = split(list,'%s*,%s*') end
- for _,tag in ipairs(list) do
- local ctor = function(items) return _M.elem(tag,items) end
- t_insert(ctors,ctor)
- end
- return unpack(ctors)
-end
+local template_cache do
+ local templ_cache = {}
-local templ_cache = {}
-
-local function template_cache (templ)
+ -- @param templ a template, a string being valid xml to be parsed, or a Node object
+ function template_cache(templ)
if is_text(templ) then
- if templ_cache[templ] then
- templ = templ_cache[templ]
- else
- local str,err = templ
- templ,err = _M.parse(str,false,true)
- if not templ then return nil,err end
- templ_cache[str] = templ
+ if templ_cache[templ] then
+ -- cache hit
+ return templ_cache[templ]
+
+ else
+ -- parse and cache
+ local ptempl, err = _M.parse(templ,false,true)
+ if not ptempl then
+ return nil, err
end
- elseif not _M.is_tag(templ) then
- return nil, "template is not a document"
+ templ_cache[templ] = ptempl
+ return ptempl
+ end
end
- return templ
+
+ if is_tag(templ) then
+ return templ
+ end
+
+ return nil, "template is not a document"
+ end
end
-local function is_data(data)
+
+do
+ local function is_data(data)
return #data == 0 or type(data[1]) ~= 'table'
-end
+ end
-local function prepare_data(data)
+
+ local function prepare_data(data)
-- a hack for ensuring that $1 maps to first element of data, etc.
-- Either this or could change the gsub call just below.
for i,v in ipairs(data) do
- data[tostring(i)] = v
+ data[tostring(i)] = v
+ end
+ end
+
+ --- create a substituted copy of a document,
+ -- @param template may be a document or a string representation which will be parsed and cached
+ -- @param data a table of name-value pairs or a list of such tables
+ -- @return an XML document
+ function Doc.subst(template, data)
+ if type(data) ~= 'table' or not next(data) then
+ return nil, "data must be a non-empty table"
end
-end
---- create a substituted copy of a document,
--- @param templ may be a document or a string representation which will be parsed and cached
--- @param data a table of name-value pairs or a list of such tables
--- @return an XML document
-function Doc.subst(templ, data)
- local err
- if type(data) ~= 'table' or not next(data) then return nil, "data must be a non-empty table" end
if is_data(data) then
- prepare_data(data)
+ prepare_data(data)
end
- templ,err = template_cache(templ)
- if err then return nil, err end
+
+ local templ, err = template_cache(template)
+ if err then
+ return nil, err
+ end
+
local function _subst(item)
- return _M.clone(templ,function(s)
- return s:gsub('%$(%w+)',item)
- end)
+ return _M.clone(templ, function(s)
+ return s:gsub('%$(%w+)', item)
+ end)
end
- if is_data(data) then return _subst(data) end
+
+ if is_data(data) then
+ return _subst(data)
+ end
+
local list = {}
- for _,item in ipairs(data) do
- prepare_data(item)
- t_insert(list,_subst(item))
+ for _, item in ipairs(data) do
+ prepare_data(item)
+ t_insert(list, _subst(item))
end
+
if data.tag then
- list = _M.elem(data.tag,list)
+ list = _M.elem(data.tag,list)
end
return list
+ end
end
---- get the first child with a given tag name.
+--- Return the first child with a given tag name (non-recursive).
-- @param tag the tag name
+-- @return the child Node found or `nil` if not found
function Doc:child_with_name(tag)
- for _, child in ipairs(self) do
- if child.tag == tag then return child; end
+ for _, child in ipairs(self) do
+ if child.tag == tag then
+ return child
end
+ end
end
-local _children_with_name
-function _children_with_name(self,tag,list,recurse)
- for _, child in ipairs(self) do if type(child) == 'table' then
- if child.tag == tag then t_insert(list,child) end
- if recurse then _children_with_name(child,tag,list,recurse) end
- end end
-end
---- get all elements in a document that have a given tag.
--- @param tag a tag name
--- @param dont_recurse optionally only return the immediate children with this tag name
--- @return a list of elements
-function Doc:get_elements_with_name(tag,dont_recurse)
+do
+ -- @param self document node to traverse
+ -- @param tag tag-name to look for
+ -- @param list array table to add the matching ones to
+ -- @param recurse if truthy, recursivly search the node
+ local function _children_with_name(self, tag, list, recurse)
+ -- TODO: protect against recursion
+ for _, child in ipairs(self) do
+ if type(child) == 'table' then
+ if child.tag == tag then
+ t_insert(list, child)
+ end
+ if recurse then
+ _children_with_name(child, tag, list, recurse)
+ end
+ end
+ end
+ end
+
+ --- Returns all elements in a document that have a given tag.
+ -- @tparam string tag a tag name
+ -- @tparam[opt=false] boolean dont_recurse optionally only return the immediate children with this tag name
+ -- @return a list of elements found, list will be empty if none was found.
+ function Doc:get_elements_with_name(tag, dont_recurse)
local res = {}
- _children_with_name(self,tag,res,not dont_recurse)
+ _children_with_name(self, tag, res, not dont_recurse)
return res
+ end
end
--- iterate over all children of a document node, including text nodes.
+
+
+--- Iterator over all children of a document node, including text nodes.
+-- This function is not recursive, so returns only direct child nodes.
+-- @return iterator that returns a single Node per iteration.
function Doc:children()
- local i = 0;
- return function (a)
- i = i + 1
- return a[i];
- end, self, i;
+ local i = 0;
+ return function (a)
+ i = i + 1
+ return a[i];
+ end, self, i;
end
--- return the first child element of a node, if it exists.
+
+--- Return the first child element of a node, if it exists.
+-- This will skip text nodes.
+-- @return first child Node or `nil` if there is none.
function Doc:first_childtag()
- if #self == 0 then return end
- for _,t in ipairs(self) do
- if type(t) == 'table' then return t end
+ if #self == 0 then
+ return
+ end
+ for _, t in ipairs(self) do
+ if is_tag(t) then
+ return t
end
+ end
end
+
+--- Iterator that matches tag names, and a namespace (non-recursive).
+-- @tparam[opt=nil] string tag tag names to return. Returns all tags if not provided.
+-- @tparam[opt=nil] string xmlns the namespace value ('xmlns' attribute) to return. If not
+-- provided will match all namespaces.
+-- @return iterator that returns a single Node per iteration.
function Doc:matching_tags(tag, xmlns)
- xmlns = xmlns or self.attr.xmlns;
- local tags = self;
- local start_i, max_i, v = 1, #tags;
- return function ()
- for i=start_i,max_i do
- v = tags[i];
- if (not tag or v.tag == tag)
- and (not xmlns or xmlns == v.attr.xmlns) then
- start_i = i+1;
- return v;
- end
- end
- end, tags, start_i;
+ -- TODO: this doesn't make sense??? namespaces are not "xmnls", as matched below
+ -- but "xmlns:name"... so should be a string-prefix match if anything...
+ xmlns = xmlns or self.attr.xmlns;
+ local tags = self
+ local next_i = 1
+ local max_i = #tags
+ local node
+ return function ()
+ for i = next_i, max_i do
+ node = tags[i];
+ if (not tag or node.tag == tag) and
+ (not xmlns or xmlns == node.attr.xmlns) then
+ next_i = i + 1
+ return node
+ end
+ end
+ end, tags, next_i
end
---- iterate over all child elements of a document node.
+
+--- Iterator over all child tags of a document node. This will skip over
+-- text nodes.
+-- @return iterator that returns a single Node per iteration.
function Doc:childtags()
- local i = 0;
- return function (a)
- local v
- repeat
- i = i + 1
- v = self[i]
- if v and type(v) == 'table' then return v; end
- until not v
- end, self[1], i;
+ local i = 0;
+ return function (a)
+ local v
+ repeat
+ i = i + 1
+ v = self[i]
+ if v and type(v) == 'table' then
+ return v
+ end
+ until not v
+ end, self[1], i;
end
---- visit child element of a node and call a function, possibility modifying the document.
--- @param callback a function passed the node (text or element). If it returns nil, that node will be removed.
--- If it returns a value, that will replace the current node.
+
+--- Visit child Nodes of a node and call a function, possibly modifying the document.
+-- Text elements will be skipped.
+-- This is not recursive, so only direct children will be passed.
+-- @tparam function callback a function with signature `function(node)`, passed the node.
+-- The element will be updated with the returned value, or deleted if it returns `nil`.
function Doc:maptags(callback)
- local is_tag = _M.is_tag
- local i = 1;
- while i <= #self do
- if is_tag(self[i]) then
- local ret = callback(self[i]);
- if ret == nil then
- t_remove(self, i);
- else
- self[i] = ret;
- i = i + 1;
- end
- end
+ local i = 1;
+
+ while i <= #self do
+ if is_tag(self[i]) then
+ local ret = callback(self[i]);
+ if ret == nil then
+ -- remove it
+ t_remove(self, i);
+
+ else
+ -- update it
+ self[i] = ret;
+ i = i + 1;
+ end
+ else
+ i = i + 1
end
- return self;
+ end
+
+ return self;
end
-local xml_escape
+
do
- local escape_table = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
- function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
- _M.xml_escape = xml_escape;
+ local escape_table = {
+ ["'"] = "&apos;",
+ ['"'] = "&quot;",
+ ["<"] = "&lt;",
+ [">"] = "&gt;",
+ ["&"] = "&amp;",
+ }
+
+ --- Escapes a string for safe use in xml.
+ -- Handles quotes(single+double), less-than, greater-than, and ampersand.
+ -- @tparam string str string value to escape
+ -- @return escaped string
+ -- @usage
+ -- local esc = xml.xml_escape([["'<>&]]) --> "&quot;&apos;&lt;&gt;&amp;"
+ function _M.xml_escape(str)
+ return (s_gsub(str, "['&<>\"]", escape_table))
+ end
+end
+local xml_escape = _M.xml_escape
+
+do
+ local escape_table = {
+ quot = '"',
+ apos = "'",
+ lt = "<",
+ gt = ">",
+ amp = "&",
+ }
+
+ --- Unescapes a string from xml.
+ -- Handles quotes(single+double), less-than, greater-than, and ampersand.
+ -- @tparam string str string value to unescape
+ -- @return unescaped string
+ -- @usage
+ -- local unesc = xml.xml_escape("&quot;&apos;&lt;&gt;&amp;") --> [["'<>&]]
+ function _M.xml_unescape(str)
+ return (str:gsub( "&(%a+);", escape_table))
+ end
end
+local xml_unescape = _M.xml_unescape
-- pretty printing
-- if indent, then put each new tag on its own line
-- if attr_indent, put each new attribute on its own line
-local function _dostring(t, buf, self, xml_escape, parentns, idn, indent, attr_indent)
- local nsid = 0;
- local tag = t.tag
- local lf,alf = ""," "
- if indent then lf = '\n'..idn end
- if attr_indent then alf = '\n'..idn..attr_indent end
- t_insert(buf, lf.."<"..tag);
- local function write_attr(k,v)
- if s_find(k, "\1", 1, true) then
- local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
- nsid = nsid + 1;
- t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'");
- elseif not(k == "xmlns" and v == parentns) then
- t_insert(buf, alf..k.."='"..xml_escape(v).."'");
- end
- end
- -- it's useful for testing to have predictable attribute ordering, if available
- if #t.attr > 0 then
- for _,k in ipairs(t.attr) do
- write_attr(k,t.attr[k])
- end
- else
- for k, v in pairs(t.attr) do
- write_attr(k,v)
- end
+local function _dostring(t, buf, parentns, block_indent, tag_indent, attr_indent)
+ local nsid = 0
+ local tag = t.tag
+
+ local lf = ""
+ if tag_indent then
+ lf = '\n'..block_indent
+ end
+
+ local alf = " "
+ if attr_indent then
+ alf = '\n'..block_indent..attr_indent
+ end
+
+ t_insert(buf, lf.."<"..tag)
+
+ local function write_attr(k,v)
+ if s_find(k, "\1", 1, true) then
+ nsid = nsid + 1
+ local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$")
+ t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'")
+
+ elseif not (k == "xmlns" and v == parentns) then
+ t_insert(buf, alf..k.."='"..xml_escape(v).."'");
end
- local len,has_children = #t;
- if len == 0 then
- local out = "/>"
- if attr_indent then out = '\n'..idn..out end
- t_insert(buf, out);
- else
- t_insert(buf, ">");
- for n=1,len do
- local child = t[n];
- if child.tag then
- self(child, buf, self, xml_escape, t.attr.xmlns,idn and idn..indent, indent, attr_indent );
- has_children = true
- else -- text element
- t_insert(buf, xml_escape(child));
- end
- end
- t_insert(buf, (has_children and lf or '').."</"..tag..">");
+ end
+
+ -- it's useful for testing to have predictable attribute ordering, if available
+ if #t.attr > 0 then
+ -- TODO: the key-value list is leading, what if they are not in-sync
+ for _,k in ipairs(t.attr) do
+ write_attr(k,t.attr[k])
+ end
+ else
+ for k, v in pairs(t.attr) do
+ write_attr(k,v)
+ end
+ end
+
+ local len = #t
+ local has_children
+
+ if len == 0 then
+ t_insert(buf, attr_indent and '\n'..block_indent.."/>" or "/>")
+
+ else
+ t_insert(buf, ">");
+
+ for n = 1, len do
+ local child = t[n]
+
+ if child.tag then
+ has_children = true
+ _dostring(child, buf, t.attr.xmlns, block_indent and block_indent..tag_indent, tag_indent, attr_indent)
+
+ else
+ -- text element
+ t_insert(buf, xml_escape(child))
+ end
end
+
+ t_insert(buf, (has_children and lf or '').."</"..tag..">");
+ end
end
----- pretty-print an XML document
---- @param t an XML document
---- @param idn an initial indent (indents are all strings)
---- @param indent an indent for each level
---- @param attr_indent if given, indent each attribute pair and put on a separate line
---- @param xml force prefacing with default or custom <?xml...>
---- @return a string representation
-function _M.tostring(t,idn,indent, attr_indent, xml)
- local buf = {};
- if xml then
- if type(xml) == "string" then
- buf[1] = xml
- else
- buf[1] = "<?xml version='1.0'?>"
- end
+--- Function to pretty-print an XML document.
+-- @param doc an XML document
+-- @tparam[opt] string|int b_ind an initial block-indent (required when `t_ind` is set)
+-- @tparam[opt] string|int t_ind an tag-indent for each level (required when `a_ind` is set)
+-- @tparam[opt] string|int a_ind if given, indent each attribute pair and put on a separate line
+-- @tparam[opt] string|bool xml_preface force prefacing with default or custom <?xml...>, if truthy then `&lt;?xml version='1.0'?&gt;` will be used as default.
+-- @return a string representation
+-- @see Doc:tostring
+function _M.tostring(doc, b_ind, t_ind, a_ind, xml_preface)
+ local buf = {}
+
+ if type(b_ind) == "number" then b_ind = (" "):rep(b_ind) end
+ if type(t_ind) == "number" then t_ind = (" "):rep(t_ind) end
+ if type(a_ind) == "number" then a_ind = (" "):rep(a_ind) end
+
+ if xml_preface then
+ if type(xml_preface) == "string" then
+ buf[1] = xml_preface
+ else
+ buf[1] = "<?xml version='1.0'?>"
end
- _dostring(t, buf, _dostring, xml_escape, nil,idn,indent, attr_indent);
- return t_concat(buf);
+ end
+
+ _dostring(doc, buf, nil, b_ind, t_ind, a_ind, xml_preface)
+
+ return t_concat(buf)
end
+
Doc.__tostring = _M.tostring
---- get the full text value of an element
+
+--- Method to pretty-print an XML document.
+-- Invokes `xml.tostring`.
+-- @tparam[opt] string|int b_ind an initial indent (required when `t_ind` is set)
+-- @tparam[opt] string|int t_ind an indent for each level (required when `a_ind` is set)
+-- @tparam[opt] string|int a_ind if given, indent each attribute pair and put on a separate line
+-- @tparam[opt="&lt;?xml version='1.0'?&gt;"] string xml_preface force prefacing with default or custom <?xml...>
+-- @return a string representation
+-- @see xml.tostring
+function Doc:tostring(b_ind, t_ind, a_ind, xml_preface)
+ return _M.tostring(self, b_ind, t_ind, a_ind, xml_preface)
+end
+
+
+--- get the full text value of an element.
+-- @return a single string with all text elements concatenated
+-- @usage
+-- local doc = xml.new("main")
+-- doc:text("one")
+-- doc:add_child(xml.elem "two")
+-- doc:text("three")
+--
+-- local t = doc:get_text() --> "onethree"
function Doc:get_text()
- local res = {}
- for i,el in ipairs(self) do
- if is_text(el) then t_insert(res,el) end
- end
- return t_concat(res);
+ local res = {}
+ for i,el in ipairs(self) do
+ if is_text(el) then t_insert(res,el) end
+ end
+ return t_concat(res);
end
---- make a copy of a document
--- @param doc the original document
--- @param strsubst an optional function for handling string copying which could do substitution, etc.
-function _M.clone(doc, strsubst)
- local lookup_table = {};
- local function _copy(object,kind,parent)
- if type(object) ~= "table" then
- if strsubst and is_text(object) then return strsubst(object,kind,parent)
- else return object
- end
- elseif lookup_table[object] then
- return lookup_table[object]
- end
- local new_table = {};
- lookup_table[object] = new_table
- local tag = object.tag
- new_table.tag = _copy(tag,'*TAG',parent)
- if object.attr then
- local res = {}
- for attr,value in pairs(object.attr) do
- res[attr] = _copy(value,attr,object)
- end
- new_table.attr = res
- end
- for index = 1,#object do
- local v = _copy(object[index],'*TEXT',object)
- t_insert(new_table,v)
+
+do
+ local function _copy(object, kind, parent, strsubst, lookup_table)
+ if type(object) ~= "table" then
+ if strsubst and is_text(object) then
+ return strsubst(object, kind, parent)
+ else
+ return object
+ end
+ end
+
+ if lookup_table[object] then
+ error("recursion detected")
+ end
+ lookup_table[object] = true
+
+ local new_table = {}
+ lookup_table[object] = new_table
+
+ local tag = object.tag
+ new_table.tag = _copy(tag, '*TAG', parent, strsubst, lookup_table)
+
+ if object.attr then
+ local res = {}
+ for attr, value in pairs(object.attr) do
+ if type(attr) == "string" then
+ res[attr] = _copy(value, attr, object, strsubst, lookup_table)
end
- return setmetatable(new_table, getmetatable(object))
+ end
+ new_table.attr = res
end
- return _copy(doc)
+ for index = 1, #object do
+ local v = _copy(object[index], '*TEXT', object, strsubst, lookup_table)
+ t_insert(new_table,v)
+ end
+
+ return setmetatable(new_table, getmetatable(object))
+ end
+
+ --- Returns a copy of a document.
+ -- The `strsubst` parameter is a callback with signature `function(object, kind, parent)`.
+ --
+ -- Param `kind` has the following values, and parameters:
+ --
+ -- - `"*TAG"`: `object` is the tag-name, `parent` is the Node object. Returns the new tag name.
+ --
+ -- - `"*TEXT"`: `object` is the text-element, `parent` is the Node object. Returns the new text value.
+ --
+ -- - other strings not prefixed with `*`: `kind` is the attribute name, `object` is the
+ -- attribute value, `parent` is the Node object. Returns the new attribute value.
+ --
+ -- @tparam Node|string doc a Node object or string (text node)
+ -- @tparam[opt] function strsubst an optional function for handling string copying
+ -- which could do substitution, etc.
+ -- @return copy of the document
+ -- @see Doc:filter
+ function _M.clone(doc, strsubst)
+ return _copy(doc, nil, nil, strsubst, {})
+ end
end
+
+--- Returns a copy of a document.
+-- This is the method version of `xml.clone`.
+-- @see xml.clone
+-- @name Doc:filter
+-- @tparam[opt] function strsubst an optional function for handling string copying
Doc.filter = _M.clone -- also available as method
---- compare two documents.
--- @param t1 any value
--- @param t2 any value
-function _M.compare(t1,t2)
+do
+ local function _compare(t1, t2, recurse_check)
+
local ty1 = type(t1)
local ty2 = type(t2)
- if ty1 ~= ty2 then return false, 'type mismatch' end
+
+ if ty1 ~= ty2 then
+ return false, 'type mismatch'
+ end
+
if ty1 == 'string' then
- return t1 == t2 and true or 'text '..t1..' ~= text '..t2
+ if t1 == t2 then
+ return true
+ else
+ return false, 'text '..t1..' ~= text '..t2
+ end
+ end
+
+ if ty1 ~= 'table' or ty2 ~= 'table' then
+ return false, 'not a document'
+ end
+
+ if recurse_check[t1] then
+ return false, "recursive document"
+ end
+ recurse_check[t1] = true
+
+ if t1.tag ~= t2.tag then
+ return false, 'tag '..t1.tag..' ~= tag '..t2.tag
+ end
+
+ if #t1 ~= #t2 then
+ return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag
end
- if ty1 ~= 'table' or ty2 ~= 'table' then return false, 'not a document' end
- if t1.tag ~= t2.tag then return false, 'tag '..t1.tag..' ~= tag '..t2.tag end
- if #t1 ~= #t2 then return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag end
+
-- compare attributes
for k,v in pairs(t1.attr) do
- if t2.attr[k] ~= v then return false, 'mismatch attrib' end
+ local t2_value = t2.attr[k]
+ if type(k) == "string" then
+ if t2_value ~= v then return false, 'mismatch attrib' end
+ else
+ if t2_value ~= nil and t2_value ~= v then return false, "mismatch attrib order" end
+ end
end
for k,v in pairs(t2.attr) do
- if t1.attr[k] ~= v then return false, 'mismatch attrib' end
+ local t1_value = t1.attr[k]
+ if type(k) == "string" then
+ if t1_value ~= v then return false, 'mismatch attrib' end
+ else
+ if t1_value ~= nil and t1_value ~= v then return false, "mismatch attrib order" end
+ end
end
+
-- compare children
- for i = 1,#t1 do
- local yes,err = _M.compare(t1[i],t2[i])
- if not yes then return err end
+ for i = 1, #t1 do
+ local ok, err = _compare(t1[i], t2[i], recurse_check)
+ if not ok then
+ return ok, err
+ end
end
return true
+ end
+
+ --- Compare two documents or elements.
+ -- Equality is based on tag, child nodes (text and tags), attributes and order
+ -- of those (order only fails if both are given, and not equal).
+ -- @tparam Node|string t1 a Node object or string (text node)
+ -- @tparam Node|string t2 a Node object or string (text node)
+ -- @treturn boolean `true` when the Nodes are equal.
+ function _M.compare(t1,t2)
+ return _compare(t1, t2, {})
+ end
end
+
--- is this value a document element?
-- @param d any value
-function _M.is_tag(d)
- return type(d) == 'table' and is_text(d.tag)
-end
+-- @treturn boolean `true` if it is a `table` with property `tag` being a string value.
+-- @name is_tag
+_M.is_tag = is_tag
+
---- call the desired function recursively over the document.
--- @param doc the document
--- @param depth_first visit child notes first, then the current node
--- @param operation a function which will receive the current tag name and current node.
-function _M.walk (doc, depth_first, operation)
- if not depth_first then operation(doc.tag,doc) end
+do
+ local function _walk(doc, depth_first, operation, recurse_check)
+ if not depth_first then operation(doc.tag, doc) end
for _,d in ipairs(doc) do
- if _M.is_tag(d) then
- _M.walk(d,depth_first,operation)
- end
+ if is_tag(d) then
+ assert(not recurse_check[d], "recursion detected")
+ recurse_check[d] = true
+ _walk(d, depth_first, operation, recurse_check)
+ end
end
- if depth_first then operation(doc.tag,doc) end
+ if depth_first then operation(doc.tag, doc) end
+ end
+
+ --- Calls a function recursively over Nodes in the document.
+ -- Will only call on tags, it will skip text nodes.
+ -- The function signature for `operation` is `function(tag_name, Node)`.
+ -- @tparam Node|string doc a Node object or string (text node)
+ -- @tparam boolean depth_first visit child nodes first, then the current node
+ -- @tparam function operation a function which will receive the current tag name and current node.
+ function _M.walk(doc, depth_first, operation)
+ return _walk(doc, depth_first, operation, {})
+ end
end
+
local html_empty_elements = { --lists all HTML empty (void) elements
br = true,
img = true,
@@ -553,13 +940,10 @@ local html_empty_elements = { --lists all HTML empty (void) elements
embed = true,
}
-local escapes = { quot = "\"", apos = "'", lt = "<", gt = ">", amp = "&" }
-local function unescape(str) return (str:gsub( "&(%a+);", escapes)); end
-
--- Parse a well-formed HTML file as a string.
-- Tags are case-insenstive, DOCTYPE is ignored, and empty elements can be .. empty.
-- @param s the HTML
-function _M.parsehtml (s)
+function _M.parsehtml(s)
return _M.basic_parse(s,false,true)
end
@@ -567,9 +951,7 @@ end
-- @param s the XML document to be parsed.
-- @param all_text if true, preserves all whitespace. Otherwise only text containing non-whitespace is included.
-- @param html if true, uses relaxed HTML rules for parsing
-function _M.basic_parse(s,all_text,html)
- local t_insert,t_remove = table.insert,table.remove
- local s_find,s_sub = string.find,string.sub
+function _M.basic_parse(s, all_text, html)
local stack = {}
local top = {}
@@ -577,12 +959,12 @@ function _M.basic_parse(s,all_text,html)
local arg = {}
s:gsub("([%w:%-_]+)%s*=%s*([\"'])(.-)%2", function (w, _, a)
if html then w = w:lower() end
- arg[w] = unescape(a)
+ arg[w] = xml_unescape(a)
end)
if html then
s:gsub("([%w:%-_]+)%s*=%s*([^\"']+)%s*", function (w, a)
w = w:lower()
- arg[w] = unescape(a)
+ arg[w] = xml_unescape(a)
end)
end
return arg
@@ -617,7 +999,7 @@ function _M.basic_parse(s,all_text,html)
if html_empty_elements[label] then empty = "/" end
end
if all_text or not s_find(text, "^%s*$") then
- t_insert(top, unescape(text))
+ t_insert(top, xml_unescape(text))
end
if empty == "/" then -- empty element tag
t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc))
@@ -640,7 +1022,7 @@ function _M.basic_parse(s,all_text,html)
end
local text = s_sub(s, i)
if all_text or not s_find(text, "^%s*$") then
- t_insert(stack[#stack], unescape(text))
+ t_insert(stack[#stack], xml_unescape(text))
end
if #stack > 1 then
error("unclosed "..stack[#stack].tag)
@@ -649,145 +1031,151 @@ function _M.basic_parse(s,all_text,html)
return is_text(res[1]) and res[2] or res[1]
end
-local function empty(attr) return not attr or not next(attr) end
-local function is_element(d) return type(d) == 'table' and d.tag ~= nil end
+do
+ local match do
--- returns the key,value pair from a table if it has exactly one entry
-local function has_one_element(t)
- local key,value = next(t)
- if next(t,key) ~= nil then return false end
- return key,value
-end
+ local function empty(attr) return not attr or not next(attr) end
-local function append_capture(res,tbl)
- if not empty(tbl) then -- no point in capturing empty tables...
- local key
- if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table
- key = tbl._
- tbl._ = nil
- if empty(tbl) then return end
- end
- -- a table with only one pair {[0]=value} shall be reduced to that value
- local numkey,val = has_one_element(tbl)
- if numkey == 0 then tbl = val end
- if key then
- res[key] = tbl
- else -- otherwise, we append the captured table
- t_insert(res,tbl)
- end
+ local append_capture do
+ -- returns the key,value pair from a table if it has exactly one entry
+ local function has_one_element(t)
+ local key,value = next(t)
+ if next(t,key) ~= nil then return false end
+ return key,value
+ end
+
+ function append_capture(res,tbl)
+ if not empty(tbl) then -- no point in capturing empty tables...
+ local key
+ if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table
+ key = tbl._
+ tbl._ = nil
+ if empty(tbl) then return end
+ end
+ -- a table with only one pair {[0]=value} shall be reduced to that value
+ local numkey,val = has_one_element(tbl)
+ if numkey == 0 then tbl = val end
+ if key then
+ res[key] = tbl
+ else -- otherwise, we append the captured table
+ t_insert(res,tbl)
+ end
+ end
+ end
end
-end
-local function make_number(pat)
- if pat:find '^%d+$' then -- $1 etc means use this as an array location
- pat = tonumber(pat)
+ local function make_number(pat)
+ if pat:find '^%d+$' then -- $1 etc means use this as an array location
+ pat = tonumber(pat)
+ end
+ return pat
end
- return pat
-end
-local function capture_attrib(res,pat,value)
- pat = make_number(pat:sub(2))
- res[pat] = value
- return true
-end
+ local function capture_attrib(res,pat,value)
+ pat = make_number(pat:sub(2))
+ res[pat] = value
+ return true
+ end
-local match
-function match(d,pat,res,keep_going)
- local ret = true
- if d == nil then d = '' end --return false end
- -- attribute string matching is straight equality, except if the pattern is a $ capture,
- -- which always succeeds.
- if is_text(d) then
- if not is_text(pat) then return false end
- if _M.debug then print(d,pat) end
- if pat:find '^%$' then
- return capture_attrib(res,pat,d)
+ function match(d,pat,res,keep_going)
+ local ret = true
+ if d == nil then d = '' end --return false end
+ -- attribute string matching is straight equality, except if the pattern is a $ capture,
+ -- which always succeeds.
+ if is_text(d) then
+ if not is_text(pat) then return false end
+ if _M.debug then print(d,pat) end
+ if pat:find '^%$' then
+ return capture_attrib(res,pat,d)
+ else
+ return d == pat
+ end
else
- return d == pat
- end
- else
- if _M.debug then print(d.tag,pat.tag) end
- -- this is an element node. For a match to succeed, the attributes must
- -- match as well.
- -- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute
- local tagpat = pat.tag:match '^(.-)%-$'
- if tagpat then
- tagpat = make_number(tagpat)
- res[tagpat] = d.tag
- end
- if d.tag == pat.tag or tagpat then
-
- if not empty(pat.attr) then
- if empty(d.attr) then ret = false
- else
- for prop,pval in pairs(pat.attr) do
- local dval = d.attr[prop]
- if not match(dval,pval,res) then ret = false; break end
- end
- end
+ if _M.debug then print(d.tag,pat.tag) end
+ -- this is an element node. For a match to succeed, the attributes must
+ -- match as well.
+ -- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute
+ local tagpat = pat.tag:match '^(.-)%-$'
+ if tagpat then
+ tagpat = make_number(tagpat)
+ res[tagpat] = d.tag
end
- -- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..}
- if ret and #pat > 0 then
- local i,j = 1,1
- local function next_elem()
- j = j + 1 -- next child element of data
- if is_text(d[j]) then j = j + 1 end
- return j <= #d
- end
- repeat
- local p = pat[i]
- -- repeated {{<...>}} patterns shall match one or more elements
- -- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X}
- if is_element(p) and p.repeated then
- local found
- repeat
- local tbl = {}
- ret = match(d[j],p,tbl,false)
- if ret then
- found = false --true
- append_capture(res,tbl)
- end
- until not next_elem() or (found and not ret)
- i = i + 1
+ if d.tag == pat.tag or tagpat then
+
+ if not empty(pat.attr) then
+ if empty(d.attr) then ret = false
else
- ret = match(d[j],p,res,false)
- if ret then i = i + 1 end
+ for prop,pval in pairs(pat.attr) do
+ local dval = d.attr[prop]
+ if not match(dval,pval,res) then ret = false; break end
+ end
end
- until not next_elem() or i > #pat -- run out of elements or patterns to match
- -- if every element in our pattern matched ok, then it's been a successful match
- if i > #pat then return true end
+ end
+ -- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..}
+ if ret and #pat > 0 then
+ local i,j = 1,1
+ local function next_elem()
+ j = j + 1 -- next child element of data
+ if is_text(d[j]) then j = j + 1 end
+ return j <= #d
+ end
+ repeat
+ local p = pat[i]
+ -- repeated {{<...>}} patterns shall match one or more elements
+ -- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X}
+ if is_tag(p) and p.repeated then
+ local found
+ repeat
+ local tbl = {}
+ ret = match(d[j],p,tbl,false)
+ if ret then
+ found = false --true
+ append_capture(res,tbl)
+ end
+ until not next_elem() or (found and not ret)
+ i = i + 1
+ else
+ ret = match(d[j],p,res,false)
+ if ret then i = i + 1 end
+ end
+ until not next_elem() or i > #pat -- run out of elements or patterns to match
+ -- if every element in our pattern matched ok, then it's been a successful match
+ if i > #pat then return true end
+ end
+ if ret then return true end
+ else
+ ret = false
end
- if ret then return true end
- else
- ret = false
- end
- -- keep going anyway - look at the children!
- if keep_going then
- for child in d:childtags() do
- ret = match(child,pat,res,keep_going)
- if ret then break end
+ -- keep going anyway - look at the children!
+ if keep_going then
+ for child in d:childtags() do
+ ret = match(child,pat,res,keep_going)
+ if ret then break end
+ end
end
end
+ return ret
end
- return ret
-end
+ end
-function Doc:match(pat)
- local err
- pat,err = template_cache(pat)
- if not pat then return nil, err end
- _M.walk(pat,false,function(_,d)
- if is_text(d[1]) and is_element(d[2]) and is_text(d[3]) and
- d[1]:find '%s*{{' and d[3]:find '}}%s*' then
- t_remove(d,1)
- t_remove(d,2)
- d[1].repeated = true
- end
- end)
+ --- does something...
+ function Doc:match(pat)
+ local err
+ pat,err = template_cache(pat)
+ if not pat then return nil, err end
+ _M.walk(pat,false,function(_,d)
+ if is_text(d[1]) and is_tag(d[2]) and is_text(d[3]) and
+ d[1]:find '%s*{{' and d[3]:find '}}%s*' then
+ t_remove(d,1)
+ t_remove(d,2)
+ d[1].repeated = true
+ end
+ end)
- local res = {}
- local ret = match(self,pat,res,true)
- return res,ret
+ local res = {}
+ local ret = match(self,pat,res,true)
+ return res,ret
+ end
end
@@ -1416,14 +1804,14 @@ do
error('bad cell specifier: '..s)
end
- --- parse a spreadsheet range.
- -- The range can be specified either as 'A1:B2' or 'R1C1:R2C2';
- -- a special case is a single element (e.g 'A1' or 'R1C1')
+ --- parse a spreadsheet range or cell.
+ -- The range/cell can be specified either as 'A1:B2' or 'R1C1:R2C2' or for
+ -- single cells as 'A1' or 'R1C1'.
-- @string s a range (case insensitive).
-- @treturn int start row
-- @treturn int start col
- -- @treturn int end row
- -- @treturn int end col
+ -- @treturn int end row (or `nil` if the range was a single cell)
+ -- @treturn int end col (or `nil` if the range was a single cell)
function array2d.parse_range (s)
assert_arg(1,s,'string')
s = s:upper()
@@ -1439,15 +1827,11 @@ do
end
end
---- get a slice of a 2D array using spreadsheet range notation. @see parse_range
--- @array2d t a 2D array
--- @string rstr range expression
--- @return a slice
--- @see array2d.parse_range
--- @see array2d.slice
-function array2d.range (t,rstr)
- assert_arg(1,t,'table')
- return array2d.slice(t,array2d.parse_range(rstr))
+--- get a slice of a 2D array.
+-- Same as `slice`.
+-- @see slice
+function array2d.range (...)
+ return array2d.slice(...)
end
local default_range do
@@ -1465,12 +1849,16 @@ local default_range do
-- Negative indices will be counted from the end, too low, or too high
-- will be limited by the array sizes.
-- @array2d t a 2D array
- -- @int i1 start row (default 1)
- -- @int j1 start col (default 1)
- -- @int i2 end row (default N)
- -- @int j2 end col (default M)
- -- return i1, j1, i2, j2
+ -- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+ -- @tparam[opt=1] int j1 start col
+ -- @tparam[opt=N] int i2 end row
+ -- @tparam[opt=M] int j2 end col
+ -- @see parse_range
+ -- @return i1, j1, i2, j2
function array2d.default_range (t,i1,j1,i2,j2)
+ if (type(i1) == 'string') and not (j1 or i2 or j2) then
+ i1, j1, i2, j2 = array2d.parse_range(i1)
+ end
local nr, nc = array2d.size(t)
i1 = norm_value(i1 or 1, nr)
j1 = norm_value(j1 or 1, nc)
@@ -1484,10 +1872,11 @@ end
--- get a slice of a 2D array. Note that if the specified range has
-- a 1D result, the rank of the result will be 1.
-- @array2d t a 2D array
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
-- @return an array, 2D in general but 1D in special cases.
function array2d.slice (t,i1,j1,i2,j2)
assert_arg(1,t,'table')
@@ -1513,10 +1902,11 @@ end
--- set a specified range of an array to a value.
-- @array2d t a 2D array
-- @param value the value (may be a function, called as `val(i,j)`)
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
-- @see tablex.set
function array2d.set (t,value,i1,j1,i2,j2)
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
@@ -1537,10 +1927,11 @@ end
-- @array2d t a 2D array
-- @param f a file object (default stdout)
-- @string fmt a format string (default is just to use tostring)
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
function array2d.write (t,f,fmt,i1,j1,i2,j2)
assert_arg(1,t,'table')
f = f or stdout
@@ -1560,10 +1951,11 @@ end
-- @array2d t 2D array
-- @func row_op function to call on each value; `row_op(row,j)`
-- @func end_row_op function to call at end of each row; `end_row_op(i)`
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2)
assert_arg(1,t,'table')
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
@@ -1581,10 +1973,11 @@ end
-- @int di start row in dest
-- @int dj start col in dest
-- @array2d src a 2D array
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
function array2d.move (dest,di,dj,src,i1,j1,i2,j2)
assert_arg(1,dest,'table')
assert_arg(4,src,'table')
@@ -1604,10 +1997,11 @@ end
--- iterate over all elements in a 2D array, with optional indices.
-- @array2d a 2D array
-- @bool indices with indices (default false)
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
-- @return either `value` or `i,j,value` depending on the value of `indices`
function array2d.iter(a,indices,i1,j1,i2,j2)
assert_arg(1,a,'table')
@@ -1647,7 +2041,7 @@ function array2d.columns(a)
end
--- iterate over all rows.
--- Returns a copy of the row, for read-only purrposes directly iterating
+-- Returns a copy of the row, for read-only purposes directly iterating
-- is more performant; `ipairs(a)`
-- @array2d a a 2D array
-- @return row, row-index
@@ -2129,7 +2523,7 @@ end
-- See `utils.unpack` for a version that is nil-safe.
-- @param t table to unpack
-- @param[opt] i index from which to start unpacking, defaults to 1
--- @param[opt] t index of the last element to unpack, defaults to #t
+-- @param[opt] j index of the last element to unpack, defaults to #t
-- @return multiple return values from the table
-- @function table.unpack
-- @see utils.unpack
@@ -2191,7 +2585,7 @@ end
-- @param ... any arguments
if not warn then -- luacheck: ignore
local enabled = false
- function warn(arg1, ...) -- luacheck: ignore
+ local function warn(arg1, ...) -- luacheck: ignore
if type(arg1) == "string" and arg1:sub(1, 1) == "@" then
-- control message
if arg1 == "@on" then
@@ -2209,6 +2603,8 @@ if not warn then -- luacheck: ignore
io.stderr:write("\n")
end
end
+ -- use rawset to bypass OpenResty's protection of global scope
+ rawset(_G, "warn", warn)
end
return compat
@@ -4122,7 +4518,7 @@ end
--- Return a list of all file names within an array which match a pattern.
-- @tab filenames An array containing file names.
--- @string pattern A shell pattern.
+-- @string pattern A shell pattern (see `fnmatch`).
-- @treturn List(string) List of matching file names.
-- @raise dir and mask must be strings
function dir.filter(filenames,pattern)
@@ -4152,11 +4548,12 @@ local function _listfiles(dirname,filemode,match)
end
--- return a list of all files in a directory which match a shell pattern.
--- @string dirname A directory. If not given, all files in current directory are returned.
--- @string mask A shell pattern. If not given, all files are returned.
+-- @string[opt='.'] dirname A directory.
+-- @string[opt] mask A shell pattern (see `fnmatch`). If not given, all files are returned.
-- @treturn {string} list of files
-- @raise dirname and mask must be strings
function dir.getfiles(dirname,mask)
+ dirname = dirname or '.'
assert_dir(1,dirname)
if mask then assert_string(2,mask) end
local match
@@ -4170,10 +4567,11 @@ function dir.getfiles(dirname,mask)
end
--- return a list of all subdirectories of the directory.
--- @string dirname A directory
+-- @string[opt='.'] dirname A directory.
-- @treturn {string} a list of directories
--- @raise dir must be a a valid directory
+-- @raise dir must be a valid directory
function dir.getdirectories(dirname)
+ dirname = dirname or '.'
assert_dir(1,dirname)
return _listfiles(dirname,false)
end
@@ -4567,13 +4965,14 @@ function dir.dirtree( d )
end
---- Recursively returns all the file starting at _path_. It can optionally take a shell pattern and
--- only returns files that match _shell_pattern_. If a pattern is given it will do a case insensitive search.
--- @string start_path A directory. If not given, all files in current directory are returned.
--- @string shell_pattern A shell pattern. If not given, all files are returned.
--- @treturn List(string) containing all the files found recursively starting at _path_ and filtered by _shell_pattern_.
+--- Recursively returns all the file starting at 'path'. It can optionally take a shell pattern and
+-- only returns files that match 'shell_pattern'. If a pattern is given it will do a case insensitive search.
+-- @string[opt='.'] start_path A directory.
+-- @string[opt='*'] shell_pattern A shell pattern (see `fnmatch`).
+-- @treturn List(string) containing all the files found recursively starting at 'path' and filtered by 'shell_pattern'.
-- @raise start_path must be a directory
function dir.getallfiles( start_path, shell_pattern )
+ start_path = start_path or '.'
assert_dir(1,start_path)
shell_pattern = shell_pattern or "*"
@@ -5113,7 +5512,7 @@ return function(env)
if prevenvmt then
_prev_index = prevenvmt.__index
if prevenvmt.__newindex then
- gmt.__index = prevenvmt.__newindex
+ gmt.__newindex = prevenvmt.__newindex
end
end
@@ -5634,7 +6033,7 @@ function lapp.process_options_string(str,args)
local enump = '|' .. enums .. '|'
vtype = 'string'
constraint = function(s)
- lapp.assert(enump:match('|'..s..'|'),
+ lapp.assert(enump:find('|'..s..'|', 1, true),
"value '"..s.."' not in "..enums
)
end
@@ -10428,9 +10827,10 @@ end,
--
-- See @{03-strings.md|the Guide}
--
--- Dependencies: `pl.utils`
+-- Dependencies: `pl.utils`, `pl.types`
-- @module pl.stringx
local utils = require 'pl.utils'
+local is_callable = require 'pl.types'.is_callable
local string = string
local find = string.find
local type,setmetatable,ipairs = type,setmetatable,ipairs
@@ -10441,10 +10841,13 @@ local sub = string.sub
local reverse = string.reverse
local concat = table.concat
local append = table.insert
+local remove = table.remove
local escape = utils.escape
local ceil, max = math.ceil, math.max
local assert_arg,usplit = utils.assert_arg,utils.split
local lstrip
+local unpack = utils.unpack
+local pack = utils.pack
local function assert_string (n,s)
assert_arg(n,s,'string')
@@ -10489,7 +10892,8 @@ function stringx.isalnum(s)
return find(s,'^%w+$') == 1
end
---- does s only contain spaces?
+--- does s only contain whitespace?
+-- Matches on pattern '%s' so matches space, newline, tabs, etc.
-- @string s a string
function stringx.isspace(s)
assert_string(1,s)
@@ -10631,10 +11035,14 @@ end
-- @usage stringx.expandtabs('\tone,two,three', 4) == ' one,two,three'
-- @usage stringx.expandtabs(' \tone,two,three', 4) == ' one,two,three'
function stringx.expandtabs(s,tabsize)
- assert_string(1,s)
- tabsize = tabsize or 8
- return (s:gsub("([^\t\r\n]*)\t", function(before_tab)
+ assert_string(1,s)
+ tabsize = tabsize or 8
+ return (s:gsub("([^\t\r\n]*)\t", function(before_tab)
+ if tabsize == 0 then
+ return before_tab
+ else
return before_tab .. (" "):rep(tabsize - #before_tab % tabsize)
+ end
end))
end
@@ -10808,36 +11216,37 @@ local function _strip(s,left,right,chrs)
return sub(s,f,t)
end
---- trim any whitespace on the left of s.
+--- trim any characters on the left of s.
-- @string s the string
-- @string[opt='%s'] chrs default any whitespace character,
--- but can be a string of characters to be trimmed
+-- but can be a string of characters to be trimmed
function stringx.lstrip(s,chrs)
assert_string(1,s)
return _strip(s,true,false,chrs)
end
lstrip = stringx.lstrip
---- trim any whitespace on the right of s.
+--- trim any characters on the right of s.
-- @string s the string
-- @string[opt='%s'] chrs default any whitespace character,
--- but can be a string of characters to be trimmed
+-- but can be a string of characters to be trimmed
function stringx.rstrip(s,chrs)
assert_string(1,s)
return _strip(s,false,true,chrs)
end
---- trim any whitespace on both left and right of s.
+--- trim any characters on both left and right of s.
-- @string s the string
-- @string[opt='%s'] chrs default any whitespace character,
--- but can be a string of characters to be trimmed
+-- but can be a string of characters to be trimmed
+-- @usage stringx.strip(' --== Hello ==-- ', "- =") --> 'Hello'
function stringx.strip(s,chrs)
assert_string(1,s)
return _strip(s,true,true,chrs)
end
---- Partioning Strings
--- @section partioning
+--- Partitioning Strings
+-- @section partitioning
--- split a string using a pattern. Note that at least one value will be returned!
-- @string s the string
@@ -10864,7 +11273,7 @@ end
--- partition the string using first occurance of a delimiter
-- @string s the string
--- @string ch delimiter
+-- @string ch delimiter (match as plain string, no patterns)
-- @return part before ch
-- @return ch
-- @return part after ch
@@ -10878,7 +11287,7 @@ end
--- partition the string p using last occurance of a delimiter
-- @string s the string
--- @string ch delimiter
+-- @string ch delimiter (match as plain string, no patterns)
-- @return part before ch
-- @return ch
-- @return part after ch
@@ -10904,6 +11313,250 @@ function stringx.at(s,idx)
return sub(s,idx,idx)
end
+
+--- Text handling
+-- @section text
+
+
+--- indent a multiline string.
+-- @tparam string s the (multiline) string
+-- @tparam integer n the size of the indent
+-- @tparam[opt=' '] string ch the character to use when indenting
+-- @return indented string
+function stringx.indent (s,n,ch)
+ assert_arg(1,s,'string')
+ assert_arg(2,n,'number')
+ local lines = usplit(s ,'\n')
+ local prefix = string.rep(ch or ' ',n)
+ for i, line in ipairs(lines) do
+ lines[i] = prefix..line
+ end
+ return concat(lines,'\n')..'\n'
+end
+
+
+--- dedent a multiline string by removing any initial indent.
+-- useful when working with [[..]] strings.
+-- Empty lines are ignored.
+-- @tparam string s the (multiline) string
+-- @return a string with initial indent zero.
+-- @usage
+-- local s = dedent [[
+-- One
+--
+-- Two
+--
+-- Three
+-- ]]
+-- assert(s == [[
+-- One
+--
+-- Two
+--
+-- Three
+-- ]])
+function stringx.dedent (s)
+ assert_arg(1,s,'string')
+ local lst = usplit(s,'\n')
+ if #lst>0 then
+ local ind_size = math.huge
+ for i, line in ipairs(lst) do
+ local i1, i2 = lst[i]:find('^%s*[^%s]')
+ if i1 and i2 < ind_size then
+ ind_size = i2
+ end
+ end
+ for i, line in ipairs(lst) do
+ lst[i] = lst[i]:sub(ind_size, -1)
+ end
+ end
+ return concat(lst,'\n')..'\n'
+end
+
+
+
+do
+ local buildline = function(words, size, breaklong)
+ -- if overflow is set, a word longer than size, will overflow the size
+ -- otherwise it will be chopped in line-length pieces
+ local line = {}
+ if #words[1] > size then
+ -- word longer than line
+ if not breaklong then
+ line[1] = words[1]
+ remove(words, 1)
+ else
+ line[1] = words[1]:sub(1, size)
+ words[1] = words[1]:sub(size + 1, -1)
+ end
+ else
+ local len = 0
+ while words[1] and (len + #words[1] <= size) or
+ (len == 0 and #words[1] == size) do
+ if words[1] ~= "" then
+ line[#line+1] = words[1]
+ len = len + #words[1] + 1
+ end
+ remove(words, 1)
+ end
+ end
+ return stringx.strip(concat(line, " ")), words
+ end
+
+ --- format a paragraph into lines so that they fit into a line width.
+ -- It will not break long words by default, so lines can be over the length
+ -- to that extent.
+ -- @tparam string s the string to format
+ -- @tparam[opt=70] integer width the margin width
+ -- @tparam[opt=false] boolean breaklong if truthy, words longer than the width given will be forced split.
+ -- @return a list of lines (List object), use `fill` to return a string instead of a `List`.
+ -- @see pl.List
+ -- @see fill
+ stringx.wrap = function(s, width, breaklong)
+ s = s:gsub('\n',' ') -- remove line breaks
+ s = stringx.strip(s) -- remove leading/trailing whitespace
+ if s == "" then
+ return { "" }
+ end
+ width = width or 70
+ local out = {}
+ local words = usplit(s, "%s")
+ while words[1] do
+ out[#out+1], words = buildline(words, width, breaklong)
+ end
+ return makelist(out)
+ end
+end
+
+--- format a paragraph so that it fits into a line width.
+-- @tparam string s the string to format
+-- @tparam[opt=70] integer width the margin width
+-- @tparam[opt=false] boolean breaklong if truthy, words longer than the width given will be forced split.
+-- @return a string, use `wrap` to return a list of lines instead of a string.
+-- @see wrap
+function stringx.fill (s,width,breaklong)
+ return concat(stringx.wrap(s,width,breaklong),'\n') .. '\n'
+end
+
+--- Template
+-- @section Template
+
+
+local function _substitute(s,tbl,safe)
+ local subst
+ if is_callable(tbl) then
+ subst = tbl
+ else
+ function subst(f)
+ local s = tbl[f]
+ if not s then
+ if safe then
+ return f
+ else
+ error("not present in table "..f)
+ end
+ else
+ return s
+ end
+ end
+ end
+ local res = gsub(s,'%${([%w_]+)}',subst)
+ return (gsub(res,'%$([%w_]+)',subst))
+end
+
+
+
+local Template = {}
+stringx.Template = Template
+Template.__index = Template
+setmetatable(Template, {
+ __call = function(obj,tmpl)
+ return Template.new(tmpl)
+ end
+})
+
+--- Creates a new Template class.
+-- This is a shortcut to `Template.new(tmpl)`.
+-- @tparam string tmpl the template string
+-- @function Template
+-- @treturn Template
+function Template.new(tmpl)
+ assert_arg(1,tmpl,'string')
+ local res = {}
+ res.tmpl = tmpl
+ setmetatable(res,Template)
+ return res
+end
+
+--- substitute values into a template, throwing an error.
+-- This will throw an error if no name is found.
+-- @tparam table tbl a table of name-value pairs.
+-- @return string with place holders substituted
+function Template:substitute(tbl)
+ assert_arg(1,tbl,'table')
+ return _substitute(self.tmpl,tbl,false)
+end
+
+--- substitute values into a template.
+-- This version just passes unknown names through.
+-- @tparam table tbl a table of name-value pairs.
+-- @return string with place holders substituted
+function Template:safe_substitute(tbl)
+ assert_arg(1,tbl,'table')
+ return _substitute(self.tmpl,tbl,true)
+end
+
+--- substitute values into a template, preserving indentation. <br>
+-- If the value is a multiline string _or_ a template, it will insert
+-- the lines at the correct indentation. <br>
+-- Furthermore, if a template, then that template will be substituted
+-- using the same table.
+-- @tparam table tbl a table of name-value pairs.
+-- @return string with place holders substituted
+function Template:indent_substitute(tbl)
+ assert_arg(1,tbl,'table')
+ if not self.strings then
+ self.strings = usplit(self.tmpl,'\n')
+ end
+
+ -- the idea is to substitute line by line, grabbing any spaces as
+ -- well as the $var. If the value to be substituted contains newlines,
+ -- then we split that into lines and adjust the indent before inserting.
+ local function subst(line)
+ return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
+ local subtmpl
+ local s = tbl[f]
+ if not s then error("not present in table "..f) end
+ if getmetatable(s) == Template then
+ subtmpl = s
+ s = s.tmpl
+ else
+ s = tostring(s)
+ end
+ if s:find '\n' then
+ local lines = usplit(s, '\n')
+ for i, line in ipairs(lines) do
+ lines[i] = sp..line
+ end
+ s = concat(lines, '\n') .. '\n'
+ end
+ if subtmpl then
+ return _substitute(s, tbl)
+ else
+ return s
+ end
+ end)
+ end
+
+ local lines = {}
+ for i, line in ipairs(self.strings) do
+ lines[i] = subst(line)
+ end
+ return concat(lines,'\n')..'\n'
+end
+
+
+
--- Miscelaneous
-- @section misc
@@ -10936,79 +11589,148 @@ end
stringx.capitalize = stringx.title
-local ellipsis = '...'
-local n_ellipsis = #ellipsis
-
---- Return a shortened version of a string.
--- Fits string within w characters. Removed characters are marked with ellipsis.
--- @string s the string
--- @int w the maxinum size allowed
--- @bool tail true if we want to show the end of the string (head otherwise)
--- @usage ('1234567890'):shorten(8) == '12345...'
--- @usage ('1234567890'):shorten(8, true) == '...67890'
--- @usage ('1234567890'):shorten(20) == '1234567890'
-function stringx.shorten(s,w,tail)
- assert_string(1,s)
- if #s > w then
- if w < n_ellipsis then return ellipsis:sub(1,w) end
- if tail then
- local i = #s - w + 1 + n_ellipsis
- return ellipsis .. s:sub(i)
- else
- return s:sub(1,w-n_ellipsis) .. ellipsis
- end
- end
- return s
+do
+ local ellipsis = '...'
+ local n_ellipsis = #ellipsis
+
+ --- Return a shortened version of a string.
+ -- Fits string within w characters. Removed characters are marked with ellipsis.
+ -- @string s the string
+ -- @int w the maxinum size allowed
+ -- @bool tail true if we want to show the end of the string (head otherwise)
+ -- @usage ('1234567890'):shorten(8) == '12345...'
+ -- @usage ('1234567890'):shorten(8, true) == '...67890'
+ -- @usage ('1234567890'):shorten(20) == '1234567890'
+ function stringx.shorten(s,w,tail)
+ assert_string(1,s)
+ if #s > w then
+ if w < n_ellipsis then return ellipsis:sub(1,w) end
+ if tail then
+ local i = #s - w + 1 + n_ellipsis
+ return ellipsis .. s:sub(i)
+ else
+ return s:sub(1,w-n_ellipsis) .. ellipsis
+ end
+ end
+ return s
+ end
end
---- Utility function that finds any patterns that match a long string's an open or close.
--- Note that having this function use the least number of equal signs that is possible is a harder algorithm to come up with.
--- Right now, it simply returns the greatest number of them found.
--- @param s The string
--- @return 'nil' if not found. If found, the maximum number of equal signs found within all matches.
-local function has_lquote(s)
- local lstring_pat = '([%[%]])(=*)%1'
- local equals, new_equals, _
- local finish = 1
- repeat
- _, finish, _, new_equals = s:find(lstring_pat, finish)
- if new_equals then
- equals = max(equals or 0, #new_equals)
- end
- until not new_equals
- return equals
+do
+ -- Utility function that finds any patterns that match a long string's an open or close.
+ -- Note that having this function use the least number of equal signs that is possible is a harder algorithm to come up with.
+ -- Right now, it simply returns the greatest number of them found.
+ -- @param s The string
+ -- @return 'nil' if not found. If found, the maximum number of equal signs found within all matches.
+ local function has_lquote(s)
+ local lstring_pat = '([%[%]])(=*)%1'
+ local equals, new_equals, _
+ local finish = 1
+ repeat
+ _, finish, _, new_equals = s:find(lstring_pat, finish)
+ if new_equals then
+ equals = max(equals or 0, #new_equals)
+ end
+ until not new_equals
+
+ return equals
+ end
+
+ --- Quote the given string and preserve any control or escape characters, such that reloading the string in Lua returns the same result.
+ -- @param s The string to be quoted.
+ -- @return The quoted string.
+ function stringx.quote_string(s)
+ assert_string(1,s)
+ -- Find out if there are any embedded long-quote sequences that may cause issues.
+ -- This is important when strings are embedded within strings, like when serializing.
+ -- Append a closing bracket to catch unfinished long-quote sequences at the end of the string.
+ local equal_signs = has_lquote(s .. "]")
+
+ -- Note that strings containing "\r" can't be quoted using long brackets
+ -- as Lua lexer converts all newlines to "\n" within long strings.
+ if (s:find("\n") or equal_signs) and not s:find("\r") then
+ -- If there is an embedded sequence that matches a long quote, then
+ -- find the one with the maximum number of = signs and add one to that number.
+ equal_signs = ("="):rep((equal_signs or -1) + 1)
+ -- Long strings strip out leading newline. We want to retain that, when quoting.
+ if s:find("^\n") then s = "\n" .. s end
+ local lbracket, rbracket =
+ "[" .. equal_signs .. "[",
+ "]" .. equal_signs .. "]"
+ s = lbracket .. s .. rbracket
+ else
+ -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly.
+ s = ("%q"):format(s):gsub("\r", "\\r")
+ end
+ return s
+ end
end
---- Quote the given string and preserve any control or escape characters, such that reloading the string in Lua returns the same result.
--- @param s The string to be quoted.
--- @return The quoted string.
-function stringx.quote_string(s)
- assert_string(1,s)
- -- Find out if there are any embedded long-quote sequences that may cause issues.
- -- This is important when strings are embedded within strings, like when serializing.
- -- Append a closing bracket to catch unfinished long-quote sequences at the end of the string.
- local equal_signs = has_lquote(s .. "]")
-
- -- Note that strings containing "\r" can't be quoted using long brackets
- -- as Lua lexer converts all newlines to "\n" within long strings.
- if (s:find("\n") or equal_signs) and not s:find("\r") then
- -- If there is an embedded sequence that matches a long quote, then
- -- find the one with the maximum number of = signs and add one to that number.
- equal_signs = ("="):rep((equal_signs or -1) + 1)
- -- Long strings strip out leading newline. We want to retain that, when quoting.
- if s:find("^\n") then s = "\n" .. s end
- local lbracket, rbracket =
- "[" .. equal_signs .. "[",
- "]" .. equal_signs .. "]"
- s = lbracket .. s .. rbracket
+
+--- Python-style formatting operator.
+-- Calling `text.format_operator()` overloads the % operator for strings to give
+-- Python/Ruby style formated output.
+-- This is extended to also do template-like substitution for map-like data.
+--
+-- Note this goes further than the original, and will allow these cases:
+--
+-- 1. a single value
+-- 2. a list of values
+-- 3. a map of var=value pairs
+-- 4. a function, as in gsub
+--
+-- For the second two cases, it uses $-variable substituion.
+--
+-- When called, this function will monkey-patch the global `string` metatable by
+-- adding a `__mod` method.
+--
+-- See <a href="http://lua-users.org/wiki/StringInterpolation">the lua-users wiki</a>
+--
+-- @usage
+-- require 'pl.text'.format_operator()
+-- local out1 = '%s = %5.3f' % {'PI',math.pi} --> 'PI = 3.142'
+-- local out2 = '$name = $value' % {name='dog',value='Pluto'} --> 'dog = Pluto'
+function stringx.format_operator()
+
+ local format = string.format
+
+ -- a more forgiving version of string.format, which applies
+ -- tostring() to any value with a %s format.
+ local function formatx (fmt,...)
+ local args = pack(...)
+ local i = 1
+ for p in fmt:gmatch('%%.') do
+ if p == '%s' and type(args[i]) ~= 'string' then
+ args[i] = tostring(args[i])
+ end
+ i = i + 1
+ end
+ return format(fmt,unpack(args))
+ end
+
+ local function basic_subst(s,t)
+ return (s:gsub('%$([%w_]+)',t))
+ end
+
+ getmetatable("").__mod = function(a, b)
+ if b == nil then
+ return a
+ elseif type(b) == "table" and getmetatable(b) == nil then
+ if #b == 0 then -- assume a map-like table
+ return _substitute(a,b,true)
+ else
+ return formatx(a,unpack(b))
+ end
+ elseif type(b) == 'function' then
+ return basic_subst(a,b)
else
- -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly.
- s = ("%q"):format(s):gsub("\r", "\\r")
+ return formatx(a,b)
end
- return s
+ end
end
+--- import the stringx functions into the global string (meta)table
function stringx.import()
utils.import(stringx,string)
end
@@ -12413,246 +13135,26 @@ end,
-- libraries, see string.Template). It also provides similar functions to those
-- found in the textwrap module.
--
--- See @{03-strings.md.String_Templates|the Guide}.
---
--- Calling `text.format_operator()` overloads the % operator for strings to give Python/Ruby style formated output.
--- This is extended to also do template-like substitution for map-like data.
+-- IMPORTANT: this module has been deprecated and will be removed in a future
+-- version (2.0). The contents of this module have moved to the `pl.stringx`
+-- module.
--
--- > require 'pl.text'.format_operator()
--- > = '%s = %5.3f' % {'PI',math.pi}
--- PI = 3.142
--- > = '$name = $value' % {name='dog',value='Pluto'}
--- dog = Pluto
+-- See @{03-strings.md.String_Templates|the Guide}.
--
--- Dependencies: `pl.utils`, `pl.types`
+-- Dependencies: `pl.stringx`, `pl.utils`
-- @module pl.text
-local gsub = string.gsub
-local concat,append = table.concat,table.insert
-local utils = require 'pl.utils'
-local bind1,usplit,assert_arg = utils.bind1,utils.split,utils.assert_arg
-local is_callable = require 'pl.types'.is_callable
-local unpack = utils.unpack
-
-local text = {}
-
-
-local function makelist(l)
- return setmetatable(l, require('pl.List'))
-end
-
-local function lstrip(str) return (str:gsub('^%s+','')) end
-local function strip(str) return (lstrip(str):gsub('%s+$','')) end
-local function split(s,delim) return makelist(usplit(s,delim)) end
-
-local function imap(f,t,...)
- local res = {}
- for i = 1,#t do res[i] = f(t[i],...) end
- return res
-end
-
-local function _indent (s,sp)
- local sl = split(s,'\n')
- return concat(imap(bind1('..',sp),sl),'\n')..'\n'
-end
-
---- indent a multiline string.
--- @param s the string
--- @param n the size of the indent
--- @param ch the character to use when indenting (default ' ')
--- @return indented string
-function text.indent (s,n,ch)
- assert_arg(1,s,'string')
- assert_arg(2,n,'number')
- return _indent(s,string.rep(ch or ' ',n))
-end
-
---- dedent a multiline string by removing any initial indent.
--- useful when working with [[..]] strings.
--- @param s the string
--- @return a string with initial indent zero.
-function text.dedent (s)
- assert_arg(1,s,'string')
- local sl = split(s,'\n')
- local _,i2 = (#sl>0 and sl[1] or ''):find('^%s*')
- sl = imap(string.sub,sl,i2+1)
- return concat(sl,'\n')..'\n'
-end
-
---- format a paragraph into lines so that they fit into a line width.
--- It will not break long words, so lines can be over the length
--- to that extent.
--- @param s the string
--- @param width the margin width, default 70
--- @return a list of lines (List object)
--- @see pl.List
-function text.wrap (s,width)
- assert_arg(1,s,'string')
- width = width or 70
- s = s:gsub('\n',' ')
- local i,nxt = 1
- local lines,line = {}
- while i < #s do
- nxt = i+width
- if s:find("[%w']",nxt) then -- inside a word
- nxt = s:find('%W',nxt+1) -- so find word boundary
- end
- line = s:sub(i,nxt)
- i = i + #line
- append(lines,strip(line))
- end
- return makelist(lines)
-end
-
---- format a paragraph so that it fits into a line width.
--- @param s the string
--- @param width the margin width, default 70
--- @return a string
--- @see wrap
-function text.fill (s,width)
- return concat(text.wrap(s,width),'\n') .. '\n'
-end
-
-local Template = {}
-text.Template = Template
-Template.__index = Template
-setmetatable(Template, {
- __call = function(obj,tmpl)
- return Template.new(tmpl)
- end})
-
-function Template.new(tmpl)
- assert_arg(1,tmpl,'string')
- local res = {}
- res.tmpl = tmpl
- setmetatable(res,Template)
- return res
-end
-
-local function _substitute(s,tbl,safe)
- local subst
- if is_callable(tbl) then
- subst = tbl
- else
- function subst(f)
- local s = tbl[f]
- if not s then
- if safe then
- return f
- else
- error("not present in table "..f)
- end
- else
- return s
- end
- end
- end
- local res = gsub(s,'%${([%w_]+)}',subst)
- return (gsub(res,'%$([%w_]+)',subst))
-end
-
---- substitute values into a template, throwing an error.
--- This will throw an error if no name is found.
--- @param tbl a table of name-value pairs.
-function Template:substitute(tbl)
- assert_arg(1,tbl,'table')
- return _substitute(self.tmpl,tbl,false)
-end
-
---- substitute values into a template.
--- This version just passes unknown names through.
--- @param tbl a table of name-value pairs.
-function Template:safe_substitute(tbl)
- assert_arg(1,tbl,'table')
- return _substitute(self.tmpl,tbl,true)
-end
-
---- substitute values into a template, preserving indentation. <br>
--- If the value is a multiline string _or_ a template, it will insert
--- the lines at the correct indentation. <br>
--- Furthermore, if a template, then that template will be subsituted
--- using the same table.
--- @param tbl a table of name-value pairs.
-function Template:indent_substitute(tbl)
- assert_arg(1,tbl,'table')
- if not self.strings then
- self.strings = split(self.tmpl,'\n')
- end
- -- the idea is to substitute line by line, grabbing any spaces as
- -- well as the $var. If the value to be substituted contains newlines,
- -- then we split that into lines and adjust the indent before inserting.
- local function subst(line)
- return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
- local subtmpl
- local s = tbl[f]
- if not s then error("not present in table "..f) end
- if getmetatable(s) == Template then
- subtmpl = s
- s = s.tmpl
- else
- s = tostring(s)
- end
- if s:find '\n' then
- s = _indent(s,sp)
- end
- if subtmpl then return _substitute(s,tbl)
- else return s
- end
- end)
- end
- local lines = imap(subst,self.strings)
- return concat(lines,'\n')..'\n'
-end
-
-------- Python-style formatting operator ------
--- (see <a href="http://lua-users.org/wiki/StringInterpolation">the lua-users wiki</a>) --
-
-function text.format_operator()
-
- local format = string.format
+local utils = require("pl.utils")
- -- a more forgiving version of string.format, which applies
- -- tostring() to any value with a %s format.
- local function formatx (fmt,...)
- local args = {...}
- local i = 1
- for p in fmt:gmatch('%%.') do
- if p == '%s' and type(args[i]) ~= 'string' then
- args[i] = tostring(args[i])
- end
- i = i + 1
- end
- return format(fmt,unpack(args))
- end
-
- local function basic_subst(s,t)
- return (s:gsub('%$([%w_]+)',t))
- end
-
- -- Note this goes further than the original, and will allow these cases:
- -- 1. a single value
- -- 2. a list of values
- -- 3. a map of var=value pairs
- -- 4. a function, as in gsub
- -- For the second two cases, it uses $-variable substituion.
- getmetatable("").__mod = function(a, b)
- if b == nil then
- return a
- elseif type(b) == "table" and getmetatable(b) == nil then
- if #b == 0 then -- assume a map-like table
- return _substitute(a,b,true)
- else
- return formatx(a,unpack(b))
- end
- elseif type(b) == 'function' then
- return basic_subst(a,b)
- else
- return formatx(a,b)
- end
- end
-end
+utils.raise_deprecation {
+ source = "Penlight " .. utils._VERSION,
+ message = "the contents of module 'pl.text' has moved into 'pl.stringx'",
+ version_removed = "2.0.0",
+ deprecated_after = "1.11.0",
+ no_trace = true,
+}
-return text
+return require "pl.stringx"
end,
@@ -12919,7 +13421,10 @@ local format = string.format
local compat = require 'pl.compat'
local stdout = io.stdout
local append = table.insert
+local concat = table.concat
local _unpack = table.unpack -- always injected by 'compat'
+local find = string.find
+local sub = string.sub
local is_windows = compat.is_windows
local err_mode = 'default'
@@ -12928,7 +13433,7 @@ local operators
local _function_factories = {}
-local utils = { _VERSION = "1.11.0" }
+local utils = { _VERSION = "1.12.0" }
for k, v in pairs(compat) do utils[k] = v end
--- Some standard patterns
@@ -12970,7 +13475,7 @@ utils.pack = table.pack -- added here to be symmetrical with unpack
-- that this one DOES honor the `n` field in the table `t`, such that it is 'nil-safe'.
-- @param t table to unpack
-- @param[opt] i index from which to start unpacking, defaults to 1
--- @param[opt] t index of the last element to unpack, defaults to `t.n` or `#t`
+-- @param[opt] j index of the last element to unpack, defaults to `t.n` or else `#t`
-- @return multiple return values from the table
-- @function utils.unpack
-- @see compat.unpack
@@ -13156,6 +13661,68 @@ function utils.assert_arg (n,val,tp,verify,msg,lev)
return val
end
+--- creates an Enum table.
+-- This helps prevent magic strings in code by throwing errors for accessing
+-- non-existing values.
+--
+-- Calling on the object does the same, but returns a soft error; `nil + err`.
+--
+-- The values are equal to the keys. The enum object is
+-- read-only.
+-- @param ... strings that make up the enumeration.
+-- @return Enum object
+-- @usage -- accessing at runtime
+-- local obj = {}
+-- obj.MOVEMENT = utils.enum("FORWARD", "REVERSE", "LEFT", "RIGHT")
+--
+-- if current_movement == obj.MOVEMENT.FORWARD then
+-- -- do something
+--
+-- elseif current_movement == obj.MOVEMENT.REVERES then
+-- -- throws error due to typo 'REVERES', so a silent mistake becomes a hard error
+-- -- "'REVERES' is not a valid value (expected one of: 'FORWARD', 'REVERSE', 'LEFT', 'RIGHT')"
+--
+-- end
+-- @usage -- validating user-input
+-- local parameter = "...some user provided option..."
+-- local ok, err = obj.MOVEMENT(parameter) -- calling on the object
+-- if not ok then
+-- print("bad 'parameter', " .. err)
+-- os.exit(1)
+-- end
+function utils.enum(...)
+ local lst = utils.pack(...)
+ utils.assert_arg(1, lst[1], "string") -- at least 1 string
+
+ local enum = {}
+ for i, value in ipairs(lst) do
+ utils.assert_arg(i, value, "string")
+ enum[value] = value
+ end
+
+ local valid = "(expected one of: '" .. concat(lst, "', '") .. "')"
+ setmetatable(enum, {
+ __index = function(self, key)
+ error(("'%s' is not a valid value %s"):format(tostring(key), valid), 2)
+ end,
+ __newindex = function(self, key, value)
+ error("the Enum object is read-only", 2)
+ end,
+ __call = function(self, key)
+ if type(key) == "string" then
+ local v = rawget(self, key)
+ if v then
+ return v
+ end
+ end
+ return nil, ("'%s' is not a valid value %s"):format(tostring(key), valid)
+ end
+ })
+
+ return enum
+end
+
+
--- process a function argument.
-- This is used throughout Penlight and defines what is meant by a function:
-- Something that is callable, or an operator string as defined by <code>pl.operator</code>,
@@ -13367,7 +13934,7 @@ function utils.quote_arg(argument)
r[i] = utils.quote_arg(arg)
end
- return table.concat(r, " ")
+ return concat(r, " ")
end
-- only a single argument
if is_windows then
@@ -13436,7 +14003,6 @@ end
-- @see splitv
function utils.split(s,re,plain,n)
utils.assert_string(1,s)
- local find,sub,append = string.find, string.sub, table.insert
local i1,ls = 1,{}
if not re then re = '%s+' end
if re == '' then return {s} end
diff --git a/macros/luatex/generic/penlight/penlight.pdf b/macros/luatex/generic/penlight/penlight.pdf
index 1edb8bf2b5..a8e166a1ae 100644
--- a/macros/luatex/generic/penlight/penlight.pdf
+++ b/macros/luatex/generic/penlight/penlight.pdf
Binary files differ
diff --git a/macros/luatex/generic/penlight/penlight.sty b/macros/luatex/generic/penlight/penlight.sty
index 9f0ead874f..b9f5a9490a 100644
--- a/macros/luatex/generic/penlight/penlight.sty
+++ b/macros/luatex/generic/penlight/penlight.sty
@@ -1,5 +1,5 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-12-15
+% 2022-02-27
% Copyright (C) 2021 Kale Ewasiuk
%
% Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,7 +22,7 @@
% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
% OR OTHER DEALINGS IN THE SOFTWARE.
-\ProvidesPackage{penlight}[2021-12-15]
+\ProvidesPackage{penlight}[2022-02-27]
\RequirePackage{luacode}
@@ -34,9 +34,15 @@
penlight = require('penlight')
__PENLIGHT__ = 'penlight'
}}
+
+
\DeclareOption{stringx}{\luadirect{_G[__PENLIGHT__].stringx.import()}}
-\DeclareOption{format}{\luadirect{_G[__PENLIGHT__].text.format_operator()}}
+\DeclareOption{format}{\luadirect{_G[__PENLIGHT__].stringx.format_operator()}}
\DeclareOption{func}{\luadirect{_G[__PENLIGHT__].utils.import(__PENLIGHT__..'.func')}}
\DeclareOption{extras}{\luadirect{require('penlightextras')}}
+\DeclareOption{extrasnoglobals}{
+ __PL_NO_GLOBALS__ = true
+ \luadirect{require('penlightextras')
+ }}
\ProcessOptions*\relax
diff --git a/macros/luatex/generic/penlight/penlight.tex b/macros/luatex/generic/penlight/penlight.tex
index 79a5d08589..176dc4e557 100644
--- a/macros/luatex/generic/penlight/penlight.tex
+++ b/macros/luatex/generic/penlight/penlight.tex
@@ -1,5 +1,5 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-12-15
+% 2022-02-27
% Copyright (C) 2021 Kale Ewasiuk
%
% Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -25,7 +25,7 @@
\documentclass[11pt,parskip=half]{scrartcl}
\setlength{\parindent}{0ex}
-\newcommand{\llcmd}[1]{\leavevmode\llap{\texttt{\detokenize{#1}\ }}}
+\newcommand{\llcmd}[1]{\leavevmode\llap{\texttt{\detokenize{#1}}}}
\newcommand{\cmd}[1]{\texttt{\detokenize{#1}}}
\newcommand{\qcmd}[1]{``\cmd{#1}''}
\usepackage{url}
@@ -52,7 +52,7 @@
\date{\today}
-\RequirePackage{penlight}
+\RequirePackage[pl,extras]{penlight}
\title{penlight}
\subtitle{Lua libraries for use in LuaLaTeX}
@@ -60,7 +60,7 @@
\maketitle
The official documentation for the Lua library can be found here:\\
- \mbox{\url{https://stevedonovan.github.io/Penlight/api/manual/01-introduction.md.html#}}
+ \mbox{\url{https://lunarmodules.github.io/Penlight}}
\\
\subsection*{Required Package Option}
@@ -70,48 +70,91 @@
\texttt{penlight.XYZ} or \texttt{pl.XYZ}
+
+ \subsection*{texlua usage}
+If you want to use Penlight (and extras) with the \texttt{texlua} intrepreter (no document made, only for Lua files, useful for testing),
+you can access it by setting \cmd{__SKIP_TEX__ = true} and adding the package to path. For example:
+ \begin{verbatim}
+package.path = package.path .. ';'..'path/to/texmf/tex/latex/penlight/?.lua'
+penlight = require('penlight')
+-- below is optional
+__SKIP_TEX__ = true --only required if you want to use
+ --penlightextras without a LaTeX run
+-- __PL_NO_GLOBALS__ = true -- optional, skip global definitions
+require('penlightextras')
+\end{verbatim}
- \subsection*{Additional Package Options}
+\pagebreak
+
+
+\subsection*{Additional Package Options}
\noindent
- \begin{tabular}{lp{4.5in}}
+ \hspace*{-6ex}\begin{tabular}{lp{4.8in}}
\texttt{stringx} & will import additional string functions into the string meta table.\\
& this will be ran in pre-amble: \texttt{require('pl.stringx').import()}\\
- & \hspace*{-4em}\url{https://stevedonovan.github.io/Penlight/api/libraries/pl.stringx.html}\\\\
\texttt{format} & allows \% operator for Python-style string formating\\
- & this will be ran in pre-amble: \texttt{require('pl.text').format\_operator()}\\
- & \hspace*{-4em}\url{https://stevedonovan.github.io/Penlight/api/libraries/pl.text.html}\\\\
+ & this will be ran in pre-amble: \texttt{require('pl.stringx').format\_operator()}\\
+ & \mbox{\url{https://lunarmodules.github.io/Penlight/libraries/pl.stringx.html#format_operator}}
+ \\
\texttt{func} & allows placehold expressions eg. \texttt{\_1+1} to be used \\
& this will be ran in pre-amble: \texttt{penlight.utils.import('pl.func')}\\
- & \hspace*{-4em}\url{https://stevedonovan.github.io/Penlight/api/libraries/pl.func.html}\\\\
- \texttt{extras} & adds some additional functions. Adds functions to some Penlight sub-modules. The following Lua globals will be defined:
- \texttt{close\_bkt\_cnt,
- add\_bkt\_cnt,
- reset\_bkt\_cnt,
- \_NumBkts,
- \_xTrue,
- \_xFalse,
- \_xNoValue,
- \_\_SKIP\_TEX\_\_,
- hasval,
- mod, mod2,
- }\\
+ & \url{https://lunarmodules.github.io/Penlight/libraries/pl.func}\\
+ \texttt{\llap{extras}noglobals} & does the above three (\cmd{func,stringx,format}); adds some additional functions to \cmd{penlight} module; and adds the \cmd{pl.tex} sub-module.\\
+ \texttt{extras} & does the above \texttt{extrasnoglobals} but makes many of the functions global variables.
\end{tabular}
- \\\\
-
- If you want to use Penlight (and extras) with the \texttt{texlua} intrepreter (no document made, only for Lua files, useful for testing),
- you can access it by the following:
- \begin{verbatim}
- package.path = package.path .. ';'..'path/to/texmf/tex/latex/penlight/?.lua'
- penlight = require('penlight')
- __SKIP_TEX__ = true --only required if you want to use
- --penlightextras without a LaTeX run
- require('penlightextras')
- \end{verbatim}
+\subsection*{Extras}
+
+If \cmd{extras} is used, the following Lua globals will be defined:\\
+
+\subsubsection*{Misc stuff}
+\llcmd{__SKIP_TEX__} If using package with \cmd{texlua}, set this global before loading \cmd{penlight}\\
+\llcmd{__PL_NO_}\cmd{GLOBALS__} If using package with \cmd{texlua} and you don't want to set some globals (described in next sections), set this global before to \cmd{true} loading \cmd{penlight}\\
+\llcmd{hasval(x)} Python-like boolean testing\\
+\llcmd{COMP'xyz'()} Python-like comprehensions:\\\url{https://lunarmodules.github.io/Penlight/libraries/pl.comprehension.html}\\
+\llcmd{math.mod(n,d)}, \cmd{math.mod2(n)} math modulous\\
+\llcmd{string.}\cmd{totable(s)} string a table of characters\\
+\llcmd{kpairs(t), }\cmd{npairs(t)} iterate over keys only, or include nil value from table ipairs\\
+
+\llcmd{pl.utils.}\cmd{filterfiles}\cmd{(dir,filt,rec)} Get files from dir and apply glob-like filters. Set rec to \cmd{true} to include sub directories\\
+
+\pagebreak
+
+\subsubsection*{\cmd{pl.tex.} module is added}
+\llcmd{add_bkt}\cmd{_cnt(n), }\cmd{close_bkt_cnt(n), reset_bkt_cnt} functions to keep track of adding curly brackets as strings. \cmd{add} will return \cmd{n} (default 1) \{'s and increment a counter. \cmd{close} will return \cmd{n} \}'s (default will close all brackets) and decrement.\\
+\llcmd{_NumBkts} internal integer for tracking the number of brackets\\
+\llcmd{opencmd(cs)} prints \cmd{\cs}\{ and adds to the bracket counters.\\
+\\
+\llcmd{_xNoValue,}\cmd{_xTrue,_xFalse}: \cmd{xparse} equivalents for commands\\
+\\
+\llcmd{prt(x),prtn(x)} print without or with a newline at end. Tries to help with special characters or numbers printing.\\
+\llcmd{prtl(l),prtt(t)} print a literal string, or table\\
+\llcmd{wrt(x), wrtn(x)} write to log\\
+\llcmd{help_wrt}\cmd{(s1, s2)} pretty-print something to console. S2 is a flag to help you find.\\
+\llcmd{prt_array2d(tt)} pretty print a 2d array\\
+\\
+\llcmd{pkgwarn}\cmd{(pkg, msg1, msg2)} throw a package warning\\
+\llcmd{pkgerror}\cmd{(pkg, msg1, msg2, stop)} throw a package error. If stop is true, immediately ceases compile.\\
+\\
+\llcmd{defcmd}\cmd{(cs, val)} like \cmd{\gdef}\\
+\llcmd{newcmd}\cmd{(cs, val)} like \cmd{\newcommand}\\
+\llcmd{renewcmd}\cmd{(cs, val)} like \cmd{\renewcommand}\\
+\llcmd{prvcmd}\cmd{(cs, val)} like \cmd{\providecommand}\\
+\llcmd{deccmd}\cmd{(cs, dft, overwrite)} declare a command. If \cmd{dft} (default) is \cmd{nil}, \cmd{cs} is set
+to a package warning saying \cmd{'cs' was declared and used in document, but never set}. If \cmd{overwrite}
+is true, it will overwrite an existing command (using \cmd{defcmd}), otherwise, it will throw error like \cmd{newcmd}.\\
+
+
+
+\subsubsection*{global extras}
+If \cmd{extras} is used and NOT \cmd{extrasnoglobals}, then some globals are set.\\
+All \cmd{pl.tex} modules are added.\\
+\cmd{hasval}, \cmd{COMP}, \cmd{kpairs}, \cmd{npairs} are globals.\\
+\cmd{pl.tablex} functions are added to the \cmd{table} table.\\
\section*{}
Disclaimer: I am not the author of the Lua Penlight library.
diff --git a/macros/luatex/generic/penlight/penlightextras.lua b/macros/luatex/generic/penlight/penlightextras.lua
index 3479eb3fa0..0cdec03763 100644
--- a/macros/luatex/generic/penlight/penlightextras.lua
+++ b/macros/luatex/generic/penlight/penlightextras.lua
@@ -1,172 +1,264 @@
+
+__SKIP_TEX__ = __SKIP_TEX__ or false --if declared true before here, it will use regular print functions
+-- (for troubleshooting with texlua instead of actual use in lua latex)
+
+__PL_NO_GLOBALS__ = __PL_NO_GLOBALS__ or false
+
-- requires penlight
local pl = _G['penlight'] or _G['pl'] -- penlight for this namespace is pl
-local bind = bind or pl.func.bind
--- some bonus string operations, % text operator, and functional programmng
+
+-- some bonus string operations, % text operator, and functional programming
pl.stringx.import()
pl.text.format_operator()
-pl.utils.import('pl.func')
+pl.utils.import('pl.func') -- allow placeholder expressions _1 +1 etc.
+
+pl.COMP = require'pl.comprehension'.new() -- for comprehensions
+
+-- http://lua-users.org/wiki/SplitJoin -- todo read me!!
+
+pl.tex = {} -- adding a sub-module for tex related stuff
+
+local bind = bind or pl.func.bind
+
+
+function pl.hasval(x) -- if something has value
+ if (type(x) == 'function') or (type(x) == 'CFunction') or (type(x) == 'userdata') then
+ return true
+ elseif (x == nil) or (x == false) or (x == 0) or (x == '') or (x == {}) then
+ return false
+ elseif (type(x) ~= 'boolean') and (type(x) ~= 'number') and (type(x) ~= 'string') then -- something else? maybe ths not needed
+ if #x == 0 then -- one more check, probably no needed though, I was trying to cover other classes but they all tables
+ return false
+ else
+ return true
+ end
+ end
+ return true
+end
+
+
+-- Some simple and helpful LaTeX functions -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+-- xparse defaults
+pl.tex._xTrue = '\\BooleanTrue '
+pl.tex._xFalse = '\\BooleanFalse '
+pl.tex._xNoValue = '-NoValue-'
+
+--Generic LuaLaTeX utilities for print commands or environments
+
+if not __SKIP_TEX__ then
+ local function check_special_chars(s) -- todo extend to toher special chars?
+ if type(s) == 'string' then
+ if string.find(s, '[\n\r\t\0]') then
+ pl.tex.pkgwarn('penlight', 'printing string with special (eg. newline) char, possible unexpected behaviour on string: '..s)
+ end
+ end
+ end
-function help_wrt(s1, s2) -- helpful printing, makes it easy to debug, s1 is object, s2 is note
- local wrt = wrt or texio.write_nl
- local wrt = wrt or print
+ -- NOTE: usage is a bit different than default. If number is first arg, you CANT change catcode.
+ -- We don't need that under normal use, use tex.print or tex.sprint if you need
+ function pl.tex.prt(s, ...) -- print something, no new line after
+ check_special_chars(s)
+ if type(s) == 'number' then s = tostring(s) end
+ tex.sprint(s, ...) --can print lists as well, but will NOT put new line between them or anything printed
+ end
+
+ function pl.tex.prtn(s, ...) -- print with new line after, can print lists or nums. C-function not in Lua, apparantly
+ s = s or ''
+ check_special_chars(s)
+ if type(s) == 'number' then s = tostring(s) end
+ tex.print(s, ...)
+ end
+
+ pl.tex.wrt = texio.write
+ pl.tex.wrtn = texio.write_nl
+else
+ pl.tex.prt = io.write
+ pl.tex.prtn = print --print with new line
+ pl.tex.wrt = io.write
+ pl.tex.wrtn = io.write_nl
+end
+
+function pl.tex.prtl(str) -- prints a literal/lines string in latex, adds new line between them
+ for line in str:gmatch"[^\n]*" do -- gets all characters up to a new line
+ pl.tex.prtn(line)
+ end
+end
+
+-- todo option to specify between character? one for first table, on for recursives?
+function pl.tex.prtt(tab, d1, d2) -- prints a table with new line between each item
+ d1 = d1 or ''
+ d2 = d2 or '\\leavevmode\\\\'
+ for _, t in pairs(tab) do --
+ if type(t) ~= 'table' then
+ if d1 == '' then
+ pl.tex.prtn(t)
+ else
+ pl.tex.prt(t, d1)
+ end
+ else
+ pl.tex.prtn(d2)
+ pl.tex.prtt(t,d1,d2)
+ end
+ end
+end
+
+function pl.tex.help_wrt(s1, s2) -- helpful printing, makes it easy to debug, s1 is object, s2 is note
+ local wrt2 = wrt or texio.write_nl or print
s2 = s2 or ''
- wrt('\nvvvvv '..s2..'\n')
+ wrt2('\nvvvvv '..s2..'\n')
if type(s1) == 'table' then
- wrt(pl.pretty.write(s1))
+ wrt2(pl.pretty.write(s1))
else
- wrt(tostring(s1))
+ wrt2(tostring(s1))
end
- wrt('\n^^^^^\n')
+ wrt2('\n^^^^^\n')
end
-function prt_array2d(t)
+function pl.tex.prt_array2d(t)
for _, r in ipairs(t) do
local s = ''
for _, v in ipairs(r) do
s = s.. tostring(v)..', '
end
- print(s)
+ pl.tex.prt(s)
+ pl.tex.prt('\n')
end
end
-- -- -- -- --
-
--- -- -- -- functions below are helpers for arrays and 2d
-
-local function compare_elements(a, b, op, ele)
- op = op or pl.oper.gt
- ele = ele or 1
- return op(a[ele], b[ele])
+function pl.tex.pkgwarn(pkg, msg1, msg2)
+ pkg = pkg or ''
+ msg1 = msg1 or ''
+ msg2 = msg2 or ''
+ tex.sprint('\\PackageWarning{'..pkg..'}{'..msg1..'}{'..msg2..'}')
end
-local function comp_2ele_func(op, ele) -- make a 2 element comparison function,
- --sort with function on element nnum
- return bind(compare_elements, _1, _2, op, ele)
+function pl.tex.pkgerror(pkg, msg1, msg2, stop)
+ pkg = pkg or ''
+ msg1 = msg1 or ''
+ msg2 = msg2 or ''
+ stop = pl.hasval(stop)
+ tex.sprint('\\PackageError{'..pkg..'}{'..msg1..'}{'..msg2..'}')
+ if stop then tex.sprint('\\stop') end -- stop on the spot (say that 10 times)
end
--- -- -- --
+--definition helpers -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+function pl.tex.defcmd(cs, val) -- simple definitions
+ val = val or ''
+ token.set_macro(cs, val, 'global')
+end
-local function check_index(ij, rc) -- converts array index to positive value if negative
- if type(ij) ~= 'number' then
- return 1
+function pl.tex.prvcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ -- do nothing if token is defined already --pkgwarn('penlight', 'Definition '..cs..' is being overwritten')
else
- if ij < 0 then
- ij = rc + ij + 1
- elseif ij > rc then
- ij = rc
- end
- return ij
+ pl.tex.defcmd(cs, val)
end
end
-local function check_slice(M, i1, j1, i2, j2) -- ensure a slice is valid; i.e. all positive numbers
- r, c = pl.array2d.size(M)
- i1 = check_index(i1, r)
- i2 = check_index(i2, r)
- j1 = check_index(j1, c)
- j2 = check_index(j2, c)
- return i1, j1, i2, j2
-end
-local function check_func(func) -- check if a function is a PE, if so, make it a function
- if type(func) ~= 'function' then
- __func = I(func)
+function pl.tex.newcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ pl.tex.pkgerror('penlight: newcmd',cs..' already defined')
+ else
+ pl.tex.defcmd(cs, val)
end
- return __func
end
-
--- -- -- -- -- -- --
--- -- -- -- functions below extend the array2d module
-
-
-function pl.array2d.map_slice1(func, L, i1, i2) -- map a function to a slice of an array, can use PlcExpr
- i2 = i2 or i1
- local len = #L
- i1 = check_index(i1, len)
- i2 = check_index(i2, len)
- func = check_func(func)
- for i in pl.seq.range(i1,i2) do
- L[i] = func(L[i])
- end
- return L
+function pl.tex.renewcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ pl.tex.defcmd(cs, val)
+ else
+ pl.tex.pkgerror('penlight: renewcmd',cs..' not defined')
+ end
end
-function pl.array2d.map_slice2(func, M, i1, j1, i2, j2) -- map a function to a slice of a Matrix
- i1, j1, i2, j2 = check_slice(M, i1, j1, i2, j2)
- --for i,j in array2d.iter(M, true, i1, j1, i2, j2) do --todo this did not work, penlight may have fixed this
- func = check_func(func)
- for i in pl.seq.range(i1,i2) do
- for j in pl.seq.range(j1,j2) do
- M[i][j] = func(M[i][j])
- end
+function pl.tex.deccmd(cs, def, overwrite) -- declare a definition, placeholder throws an error if it used but not set!
+ overwrite = pl.hasval(overwrite)
+ local decfun
+ if overwrite then decfun = pl.tex.defcmd else decfun = pl.tex.newcmd end
+ if def == nil then
+ decfun(cs, pkgerror('penlight', cs..' was declared and used in document, but never set'))
+ else
+ decfun(cs, def)
end
- return M
end
-function pl.array2d.map_columns(func, M, j1, j2) -- map function to columns of matrix
- j2 = j2 or j1
- return map_slice2(func, M, 1, j1, -1, j2)
-end
-function pl.array2d.map_rows(func, M, i1, i2) -- map function to rows of matrix
- i2 = i2 or i1
- return map_slice2(func, M, i1, 1, i2, -1)
-end
+--
+-- -- todo add and improve this, options for args?
+--local function defcmd_nest(cs) -- for option if you'd like your commands under a parent ex. \csparent{var}
+-- tex.print('\\gdef\\'..cs..'#1{\\csname '..var..'--#1--\\endcsname}')
+--end
+--
+--
+--local function defcmd(cs, val, nargs)
+-- if (nargs == nil) or (args == 0) then
+-- token.set_macro(cs, tostring(val), 'global')
+-- else
+-- local args = '#1'
+-- tex.print('\\gdef\\'..cs..args..'{'..val..'}')
+-- -- todo https://tex.stackexchange.com/questions/57551/create-a-capitalized-macro-token-using-csname
+-- -- \expandafter\gdef\csname Two\endcsname#1#2{1:#1, two:#2} --todo do it like this
+-- end
+--end
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- -- -- -- -- -- -- --
-function pl.array2d.sortOP(M, op, ele) -- sort a 2d array based on operator criteria, ele is column, ie sort on which element
- M_new = {}
- for row in pl.seq.sort(M, comp_2ele_func(op, ele)) do
- M_new[#M_new+1] = row
- end
- return M_new
+
+-- when nesting commands, this makes it helpful to not worry about brackets
+pl.tex._NumBkts = 0
+--prt(opencmd('textbf')..opencmd('texttt')..'bold typwriter'..close_bkt_cnt())
+
+function pl.tex.opencmd(cmd)
+ return '\\'..cmd..add_bkt_cnt()
end
-function pl.array2d.like(M1, v)
- v = v or 0
- r, c = pl.array2d.size(M1)
- return pl.array2d.new(r,c,v)
+function pl.tex.reset_bkt_cnt(n)
+ n = n or 0
+ _NumBkts = n
end
-function pl.array2d.from_table(t) -- turns a labelled table to a 2d, label-free array
- t_new = {}
- for k, v in pairs(t) do
- if type(v) == 'table' then
- t_new_row = {k}
- for _, v_ in ipairs(v) do
- t_new_row[#t_new_row+1] = v_
- end
- t_new[#t_new+1] = t_new_row
- else
- t_new[#t_new+1] = {k, v}
- end
- end
- return t_new
+function pl.tex.add_bkt_cnt(n)
+ -- add open bracket n times, returns brackets
+ n = n or 1
+ _NumBkts = _NumBkts + n
+ return ('{'):rep(n)
end
-function pl.array2d.toTeX(M, EL) --puts & between columns, can choose to end line with \\ if EL is true (end-line)
- EL = EL or false
- if EL then EL = '\\\\' else EL = '' end
- return pl.array2d.reduce2(_1..EL.._2, _1..'&'.._2, M)..EL
+function pl.tex.close_bkt_cnt(n)
+ n = n or _NumBkts
+ local s = ('}'):rep(n)
+ _NumBkts = _NumBkts - n
+ return s
end
--- -- -- -- -- -- --
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+
--- -- -- -- -- -- -- -- -- -- -- -- functions below extend the operator module
-function pl.operator.strgt(a,b) return tostring(a) > tostring(b) end
-function pl.operator.strlt(a,b) return tostring(a) < tostring(b) end
+-- -- -- -- math stuff
+function math.mod(a, b) -- math modulo, return remainder only
+ return a - (math.floor(a/b)*b)
+end
+
+function math.mod2(a) -- math modulo 2
+ return math.mod(a,2)
+end
+
-- -- -- -- string stuff
@@ -184,14 +276,13 @@ local number = P{"number",
}
-local mt = getmetatable("") -- register functions with str
-
+local str_mt = getmetatable("") -- register functions with str
-function mt.__index.gnum(s)
+function str_mt.__index.gnum(s)
return number:match(s)
end
-function mt.__index.gextract(s, pat) --extract a pattern from string, returns both
+function str_mt.__index.gextract(s, pat) --extract a pattern from string, returns both
local s_extr = ''
local s_rem = s
for e in s:gmatch(pat) do
@@ -201,7 +292,7 @@ function mt.__index.gextract(s, pat) --extract a pattern from string, returns bo
return s_extr, s_rem
end
-function mt.__index.gfirst(s, t) -- get the first pattern found from a table of pattern
+function str_mt.__index.gfirst(s, t) -- get the first pattern found from a table of pattern
for _, pat in pairs(t) do
if string.find(s, pat) then
return pat
@@ -209,7 +300,7 @@ function mt.__index.gfirst(s, t) -- get the first pattern found from a table of
end
end
-function mt.__index.appif(S, W, B, O) --append W ord to S tring if B oolean true, otherwise O ther
+function str_mt.__index.appif(S, W, B, O) --append W ord to S tring if B oolean true, otherwise O ther
--append Word to String
if B then --if b is true
S = S .. W
@@ -221,155 +312,394 @@ function mt.__index.appif(S, W, B, O) --append W ord to S tring if B oolean true
end
-
--- -- -- -- -- math stuffs
-
-function mod(a, b) -- math modulo, return remainder only
- return a - (math.floor(a/b)*b)
+ function str_mt.__index.containsany(s, exp)
+ if type(exp) ~= 'table' then exp = {exp} end
+ for _, e in ipairs(exp) do
+ if s:find(e) then return true end
+ end
+ return false
end
-function mod2(a) -- math modulo 2
- return mod(a,2)
+function str_mt.__index.containsanycase(s, exp)
+ if type(exp) ~= 'table' then exp = {exp} end
+ for _, e in ipairs(exp) do
+ if s:lower():find(e:lower()) then return true end
+ end
+ return false
end
-function hasval(x) -- if something has value
- if (type(x) == 'function') then
- return true
- elseif (x == nil) or (x == false) or (x == 0) or (x == '') then
- return false
- elseif (type(x) ~= 'boolean') and (type(x) ~= 'number') and (type(x) ~= 'string') then
- if #x == 0 then
- return false
- else
- return true
- end
+function str_mt.__index.totable(str)
+ local t = {}
+ for i = 1, #str do
+ t[i] = str:sub(i, i)
end
- return true
+ return t
end
+-- -- -- -- function stuff
+
+function pl.clone_function(fn)
+ local dumped = string.dump(fn)
+ local cloned = loadstring(dumped)
+ local i = 1
+ while true do
+ local name = debug.getupvalue(fn, i)
+ if not name then
+ break
+ end
+ debug.upvaluejoin(cloned, i, fn, i)
+ i = i + 1
+ end
+ return cloned
+end
+-- -- -- -- -- -- -- -- -- -- -- -- functions below extend the operator module
--- Some simple and helpful LaTeX functions
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+function pl.operator.strgt(a,b) return tostring(a) > tostring(b) end
+function pl.operator.strlt(a,b) return tostring(a) < tostring(b) end
---Generic LuaLaTeX utilities for print commands or environments
--- http://lua-users.org/wiki/SplitJoin -- todo read me!!
-__SKIP_TEX__ = __SKIP_TEX__ or false --if declared true before here, it will use regular print functions
--- (for troubleshooting with texlua)
--- xparse defaults
-_xTrue = '\\BooleanTrue '
-_xFalse = '\\BooleanFalse '
-_xNoValue = '-NoValue-'
+-- -- -- -- functions below are helpers for arrays and 2d
-if not __SKIP_TEX__ then
- prt = tex.sprint --can print lists, but will NOT put new line between them
- prtn = tex.print --can print lists and will put new line. C-function not in Lua. think P rint R return
- wrt = texio.write
- wrtn = texio.write_nl
-else
- prt = io.write
- prtn = print --print with new line
- wrt = io.write
- wrtn = io.write_nl
+local function compare_elements(a, b, op, ele)
+ op = op or pl.oper.gt
+ ele = ele or 1
+ return op(a[ele], b[ele])
end
-function prtl(str) -- prints a literal string to latex, adds new line between them
- for line in str:gmatch"[^\n]*" do -- gets all characters up to a new line
- prtn(line)
- end
+local function comp_2ele_func(op, ele) -- make a 2 element comparison function,
+ --sort with function on element nnum
+ return bind(compare_elements, _1, _2, op, ele)
end
-_NumBkts = 0
-function reset_bkt_cnt(n)
- n = n or 0
- _NumBkts = n
-end
-function add_bkt_cnt(n)
- -- add open bracket n times, returns brackets
- n = n or 1
- _NumBkts = _NumBkts + n
- return ('{'):rep(n)
+
+-- table stuff below
+
+
+function pl.tablex.map_slice(func, T, j1, j2)
+ if type(j1) == 'string' then
+ return pl.array2d.map_slice(func, {T}, ','..j1)[1]
+ else
+ return pl.array2d.map_slice(func, {T}, 1, j1, 1, j2)[1]
+ end
end
-function close_bkt_cnt()
- local s = ('}'):rep(_NumBkts)
- reset_bkt_cnt()
- return s
+pl.array2d.map_slice1 = pl.tablex.map_slice
+
+
+-- todo option for multiple filters with AND logic, like the filter files??
+function pl.tablex.filterstr(t, exp, case)
+ -- case = case sensitive
+ case = hasval(case)
+ -- apply lua patterns to a table to filter iter
+ -- str or table of str's can be passed, OR logic is used if table is passed
+ if case then
+ return pl.tablex.filter(t, bind(string.containsany,_1,exp))
+ else
+ return pl.tablex.filter(t, bind(string.containsanycase,_1,exp))
+ end
end
+function pl.utils.filterfiles(...)
+ -- f1 is a series of filtering patterns, or condition
+ -- f2 is a series of filtering patters, or condition
+ -- (f1_a or f2_...) and (f2 .. ) must match
+ local args = table.pack(...)
+ -- todo -- check where boolean is for recursive or not, set starting argument
+ -- this could allow one to omit dir
+ -- todo if no boolean at all, assume dir = '.' and r = false
+ -- if boolean given, assume dir = '.'
+ local nstart = 3
+ local r = args[2]
+ local dir = args[1]
+ if type(args[1]) == 'boolean' then
+ dir = '.'
+ r = args[1]
+ nstart = 2
+ elseif type(args[2]) ~= 'boolean' then
+ dir = '.'
+ r = false
+ nstart = 1
+ end
+ local files
+ if r then files = pl.dir.getallfiles(dir)
+ else files = pl.dir.getfiles(dir)
+ end
+ for i=nstart,args.n do
+ files = pl.tablex.filter(files, pl.func.compose(bind(string.containsanycase,_1, args[i]), pl.path.basename))
+ end
+ return files
+end
+-- -- -- -- -- -- -- -- functions below extend the array2d module
---definition helpers -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- todo add and improve this
-local function defcmd_nest(cs) -- for option if you'd like your commands under a parent ex. \csparent{var}
- tex.print('\\gdef\\'..cs..'#1{\\csname '..var..'--#1--\\endcsname}')
+function pl.array2d.map_slice(func, M, i1, j1, i2, j2) -- map a function to a slice of a Matrix
+ func = pl.utils.function_arg(1, func)
+ for i,j in pl.array2d.iter(M, true, i1, j1, i2, j2) do
+ M[i][j] = func(M[i][j])
+ end
+ return M
end
+pl.array2d.map_slice2 = pl.array2d.map_slice
-local function defcmd(cs, val, nargs)
- if (nargs == nil) or (args == 0) then
- token.set_macro(cs, tostring(val), 'global')
+function pl.array2d.map_cols(func, M, j1, j2) -- map function to columns of matrix
+ if type(j1) == 'string' then
+ return pl.array2d.map_slice(func, M, ','..j1)
else
- local args = '#1'
- tex.print('\\gdef\\'..cs..args..'{'..val..'}')
- -- todo https://tex.stackexchange.com/questions/57551/create-a-capitalized-macro-token-using-csname
- -- \expandafter\gdef\csname Two\endcsname#1#2{1:#1, two:#2} --todo do it like this
+ j2 = j2 or -1
+ return pl.array2d.map_slice(func, M, 1, j1, -1, j2)
end
end
+pl.array2d.map_columns = pl.array2d.map_cols
-local function prvcmd(cs, val) -- provide command via lua
- if token.is_defined(cs) then
- tex.print('\\PackageWarning{YAMLvars}{Variable '..cs..' already defined, could not declare}{}')
+function pl.array2d.map_rows(func, M, i1, i2) -- map function to rows of matrix
+ if type(i1) == 'string' then
+ return pl.array2d.map_slice(func, M, i1)
else
- defcmd(cs, val)
+ i2 = i2 or -1
+ return pl.array2d.map_slice(func, M, i1, 1, i2, -1)
end
end
-local function newcmd(cs, val) -- provide command via lua
- if token.is_defined(cs) then
- tex.print('\\PackageError{luadefs}{Command '..cs..' already defined}{}')
- else
- defcmd(cs, val)
+-- -- -- -- -- -- -- --
+
+function pl.array2d.sortOP(M, op, ele) -- sort a 2d array based on operator criteria, ele is column, ie sort on which element
+ M_new = {}
+ for row in pl.seq.sort(M, comp_2ele_func(op, ele)) do
+ M_new[#M_new+1] = row
+ end
+ return M_new
+end
+
+function pl.array2d.like(M1, v)
+ v = v or 0
+ r, c = pl.array2d.size(M1)
+ return pl.array2d.new(r,c,v)
+end
+
+function pl.array2d.from_table(t) -- turns a labelled table to a 2d, label-free array
+ t_new = {}
+ for k, v in pairs(t) do
+ if type(v) == 'table' then
+ t_new_row = {k}
+ for _, v_ in ipairs(v) do
+ t_new_row[#t_new_row+1] = v_
+ end
+ t_new[#t_new+1] = t_new_row
+ else
+ t_new[#t_new+1] = {k, v}
+ end
end
+ return t_new
end
-local function renewcmd(cs, val) -- provide command via lua
- if token.is_defined(cs) then
- defcmd(cs, val)
+function pl.array2d.toTeX(M, EL) --puts & between columns, can choose to end line with \\ if EL is true (end-line)
+ EL = EL or false
+ if EL then EL = '\\\\' else EL = '' end
+ return pl.array2d.reduce2(_1..EL.._2, _1..'&'.._2, M)..EL
+end
+
+
+local function parse_numpy1d(i1, i2, iS)
+ i1 = tonumber(i1)
+ i2 = tonumber(i2)
+ if iS == ':' then
+ if i1 == nil then i1 = 1 end
+ if i2 == nil then i2 = -1 end
else
- tex.print('\\PackageError{luadefs}{Command '..cs..' already defined}{}')
+ if i1 == nil then
+ i1 = 1
+ i2 = -1
+ else
+ i2 = i1
+ end
end
+ return i1, i2
end
-local function deccmd(cs, def)
- if def == nil then
- prvcmd(cs, '\\PackageError{luadefs}{Command "'..cs..'" was declared and used but, not set}{}')
- else
- prvcmd(cs, def)
+function pl.array2d.parse_numpy2d_str(s)
+ s = s:gsub('%s+', '')
+ _, _, i1, iS, i2, j1, jS, j2 = string.find(s, "(%-?%d*)(:?)(%-?%d*),?(%-?%d*)(:?)(%-?%d*)")
+ i1, i2 = parse_numpy1d(i1, i2, iS)
+ j1, j2 = parse_numpy1d(j1, j2, jS)
+ return i1, j1, i2, j2
+end
+
+
+local _parse_range = pl.clone_function(pl.array2d.parse_range)
+
+function pl.array2d.parse_range(s) -- edit parse range to do numpy string if no letter passed
+ pl.utils.assert_arg(1,s,'string')
+ if not s:find'%a' then
+ return pl.array2d.parse_numpy2d_str(s)
end
+ return _parse_range(s)
end
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+
+
+
+if not __PL_NO_GLOBALS__ then
+ -- iterators
+ kpairs = pl.utils.kpairs
+ npairs = pl.utils.npairs
+ --enum = utils.enum
+
+ for k,v in pairs(pl.tablex) do -- extend the table table to contain tablex functions
+ _G['table'][k] = v
+ end
+
+ hasval = pl.hasval
+ COMP = pl.COMP
+
+ -- shortcuts
+-- http://stevedonovan.github.io/Penlight/api/libraries/pl.utils.html
+ pl.writefile = pl.utils.writefile
+ pl.readfile = pl.utils.readfile
+ pl.readlines = pl.utils.readfile
+ pl.filterfiles = pl.utils.filterfiles
+
+ pl.a2 = pl.array2d
+ pl.tbl = pl.tablex
+
+
+ for k,v in pairs(pl.tex) do -- make tex functions global
+ _G[k] = v
+ end
+
+ --_xTrue = pl.tex._xTrue
+ --_xFalse = pl.tex._xFalse
+ --_xNoValue = pl.tex._xNoValue
+ --
+ --prt = pl.tex.prt
+ --prtn = pl.tex.prtn
+ --wrt = pl.tex.wrt
+ --wrtn = pl.tex.wrtn
+ --
+ --prtl = pl.tex.prtl
+ --prtt = pl.tex.prtt
+ --
+ --help_wrt = pl.tex.help_wrt
+ --prt_array2d = pl.tex.prt_array2d
+ --
+ --pkgwarn = pl.tex.pkgwarn
+ --pkgerror = pl.tex.pkgerror
+ --
+ --defcmd = pl.tex.defcmd
+ --prvcmd = pl.tex.prvcmd
+ --newcmd = pl.tex.newcmd
+ --renewcmd = pl.tex.renewcmd
+ --deccmd = pl.tex.deccmd
+ --
+ --_NumBkts = pl.tex._NumBkts
+ --opencmd = pl.tex.opencmd
+ --reset_bkt_cnt = pl.tex.reset_bkt_cnt
+ --add_bkt_cnt = pl.tex.add_bkt_cnt
+ --close_bkt_cnt = pl.tex.close_bkt_cnt
+
+end
+
+
+
+
+
+-- graveyard
+
+
+-- luakeys parses individual keys as ipairs, this changes the list to a pure map
+--function pl.luakeystomap(t)
+-- local t_new = {}
+-- for k, v in pairs(t) do
+-- if type(k) == 'number' then
+-- t_new[v] = true
+-- else
+-- t_new[k] = v
+-- end
+-- end
+-- return t_new
+--end
+--if luakeys then -- if luakeys is already loaded
+-- function luakeys.parseN(s, ...)
+-- local t = luakeys.parse(s,...)
+-- t = pl.luakeystomap(t)
+-- return t
+-- end
+--end
+-- might not be needed
+
+
+ --local func = check_func(func)
+--local function check_func(func) -- check if a function is a PE, if so, make it a function
+-- if type(func) ~= 'function' then
+-- return I(func)
+-- end
+-- return func
+--end
+
+-- -- -- -- -- -- --
+-- -- -- -- functions below extend the array2d module
+
+
+--function pl.array2d.map_slice1(func, L, i1, i2) -- map a function to a slice of an array, can use PlcExpr
+-- i2 = i2 or i1
+-- local len = #L
+-- i1 = check_index(i1, len)
+-- i2 = check_index(i2, len)
+-- func = check_func(func)
+-- for i in pl.seq.range(i1,i2) do
+-- L[i] = func(L[i])
+-- end
+-- return L
+--end
+
+ -- used this below when iter was not working..
+ --i1, j1, i2, j2 = check_slice(M, i1, j1, i2, j2)
+ --for i in pl.seq.range(i1,i2) do
+ -- for j in pl.seq.range(j1,j2) do
+ --end
+ -- penlight may have fixed this
+--local function check_index(ij, rc) -- converts array index to positive value if negative
+-- if type(ij) ~= 'number' then
+-- return 1
+-- else
+-- if ij < 0 then
+-- ij = rc + ij + 1
+-- elseif ij > rc then
+-- ij = rc
+-- elseif ij == 0 then
+-- ij = 1
+-- end
+-- return ij
+-- end
+--end
+--local function check_slice(M, i1, j1, i2, j2) -- ensure a slice is valid; i.e. all positive numbers
+-- r, c = pl.array2d.size(M)
+-- i1 = check_index(i1 or 1, r)
+-- i2 = check_index(i2 or r, r)
+-- j1 = check_index(j1 or 1, c)
+-- j2 = check_index(j2 or c, c)
+-- return i1, j1, i2, j2
+--end
+