diff options
author | Karl Berry <karl@freefriends.org> | 2022-02-27 21:34:18 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2022-02-27 21:34:18 +0000 |
commit | 15d14b27c3e1c705e0817962384b1a6d6c1c4b6e (patch) | |
tree | d3dc5ef5d3fd3d6bbd2ef63653f5feb979713510 /Master/texmf-dist/tex | |
parent | b9647d1bd4696d68edbcad33b6f6498688f515ea (diff) |
penlight (27feb22) (branch)
git-svn-id: svn://tug.org/texlive/branches/branch2021.final@62255 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/tex')
-rw-r--r-- | Master/texmf-dist/tex/luatex/penlight/penlight.lua | 2256 | ||||
-rw-r--r-- | Master/texmf-dist/tex/luatex/penlight/penlight.sty | 12 | ||||
-rw-r--r-- | Master/texmf-dist/tex/luatex/penlight/penlightextras.lua | 738 |
3 files changed, 1954 insertions, 1052 deletions
diff --git a/Master/texmf-dist/tex/luatex/penlight/penlight.lua b/Master/texmf-dist/tex/luatex/penlight/penlight.lua index bea93160718..4b2436be1b3 100644 --- a/Master/texmf-dist/tex/luatex/penlight/penlight.lua +++ b/Master/texmf-dist/tex/luatex/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 = { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" }; - function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end - _M.xml_escape = xml_escape; + local escape_table = { + ["'"] = "'", + ['"'] = """, + ["<"] = "<", + [">"] = ">", + ["&"] = "&", + } + + --- 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([["'<>&]]) --> ""'<>&" + 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(""'<>&") --> [["'<>&]] + 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 `<?xml version='1.0'?>` 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="<?xml version='1.0'?>"] 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/Master/texmf-dist/tex/luatex/penlight/penlight.sty b/Master/texmf-dist/tex/luatex/penlight/penlight.sty index 9f0ead874fa..b9f5a9490a9 100644 --- a/Master/texmf-dist/tex/luatex/penlight/penlight.sty +++ b/Master/texmf-dist/tex/luatex/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/Master/texmf-dist/tex/luatex/penlight/penlightextras.lua b/Master/texmf-dist/tex/luatex/penlight/penlightextras.lua index 3479eb3fa04..0cdec037631 100644 --- a/Master/texmf-dist/tex/luatex/penlight/penlightextras.lua +++ b/Master/texmf-dist/tex/luatex/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 + |