summaryrefslogtreecommitdiff
path: root/macros/luatex/generic
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2022-02-28 03:02:40 +0000
committerNorbert Preining <norbert@preining.info>2022-02-28 03:02:40 +0000
commit7a8d1925127ad121a8f591db931524f9b8eacb93 (patch)
treedf5084184ea4d6732d6a9fae1d263b6915dbb36d /macros/luatex/generic
parenta911d39178540078c8ff5bc880418478d7433c40 (diff)
CTAN sync 202202280302
Diffstat (limited to 'macros/luatex/generic')
-rw-r--r--macros/luatex/generic/luaotfload/NEWS6
-rw-r--r--macros/luatex/generic/luaotfload/README.md4
-rw-r--r--macros/luatex/generic/luaotfload/filegraph.pdfbin40019 -> 40007 bytes
-rw-r--r--macros/luatex/generic/luaotfload/fontloader-basics-gen.lua2
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-auxiliary.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-colors.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-conf.pdfbin77690 -> 77520 bytes
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-configuration.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-database.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-diagnostics.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-dvi.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-embolden.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-fallback.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-features.lua9
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-filelist.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-glyphlist.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-harf-define.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-harf-var-cff2.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-harf-var-t2-writer.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-harf-var-ttf.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-init.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-latex.pdfbin494376 -> 493487 bytes
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-letterspace.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-loaders.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-log.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-main.tex10
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-multiscript.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-notdef.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-parsers.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-resolvers.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-scripts.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-status.lua62
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-suppress.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-szss.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-t3.lua142
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-tool.12
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-tool.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-tool.pdfbin76959 -> 76788 bytes
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-tool.rst4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-tounicode.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-unicode.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload.conf.52
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload.conf.rst4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload.lua4
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload.sty2
-rw-r--r--macros/luatex/generic/luaotfload/scripts-demo.pdfbin22075 -> 21673 bytes
-rw-r--r--macros/luatex/generic/luaotfload/shaper-demo-graphite.pdfbin8560 -> 8548 bytes
-rw-r--r--macros/luatex/generic/luaotfload/shaper-demo.pdfbin8481 -> 8469 bytes
-rw-r--r--macros/luatex/generic/lutabulartools/README.md5
-rw-r--r--macros/luatex/generic/lutabulartools/lutabulartools.lua177
-rw-r--r--macros/luatex/generic/lutabulartools/lutabulartools.pdfbin59544 -> 83452 bytes
-rw-r--r--macros/luatex/generic/lutabulartools/lutabulartools.sty102
-rw-r--r--macros/luatex/generic/lutabulartools/lutabulartools.tex198
-rw-r--r--macros/luatex/generic/penlight/penlight.lua2256
-rw-r--r--macros/luatex/generic/penlight/penlight.pdfbin29642 -> 38237 bytes
-rw-r--r--macros/luatex/generic/penlight/penlight.sty12
-rw-r--r--macros/luatex/generic/penlight/penlight.tex109
-rw-r--r--macros/luatex/generic/penlight/penlightextras.lua738
59 files changed, 2468 insertions, 1498 deletions
diff --git a/macros/luatex/generic/luaotfload/NEWS b/macros/luatex/generic/luaotfload/NEWS
index 44b749c352..3945ef2f89 100644
--- a/macros/luatex/generic/luaotfload/NEWS
+++ b/macros/luatex/generic/luaotfload/NEWS
@@ -1,5 +1,11 @@
Change History
--------------
+2022-02-27 luaotfload v3.20
+
+ * A bug in luaotfload-tool has been corrected.
+ * The directory for the font name database has been corrected and no
+ longer uses the development directory.
+
2022-02-24 luaotfload v3.19
* When used with LuaTeX 1.15.0 or newer, variable fonts are supported
diff --git a/macros/luatex/generic/luaotfload/README.md b/macros/luatex/generic/luaotfload/README.md
index 565733e6c5..ff3302131f 100644
--- a/macros/luatex/generic/luaotfload/README.md
+++ b/macros/luatex/generic/luaotfload/README.md
@@ -1,8 +1,8 @@
# The Luaotfload Package
-VERSION: 3.19
+VERSION: 3.20
-DATE: 2022-02-24
+DATE: 2022-02-27
## Description
diff --git a/macros/luatex/generic/luaotfload/filegraph.pdf b/macros/luatex/generic/luaotfload/filegraph.pdf
index b455173c6b..d18d43f57d 100644
--- a/macros/luatex/generic/luaotfload/filegraph.pdf
+++ b/macros/luatex/generic/luaotfload/filegraph.pdf
Binary files differ
diff --git a/macros/luatex/generic/luaotfload/fontloader-basics-gen.lua b/macros/luatex/generic/luaotfload/fontloader-basics-gen.lua
index 1cc8537168..5a6e90ceee 100644
--- a/macros/luatex/generic/luaotfload/fontloader-basics-gen.lua
+++ b/macros/luatex/generic/luaotfload/fontloader-basics-gen.lua
@@ -189,7 +189,7 @@ local readables = { }
local usingjit = jit
if not caches.namespace or caches.namespace == "" or caches.namespace == "context" then
- caches.namespace = 'generic-dev'
+ caches.namespace = 'generic'
end
do
diff --git a/macros/luatex/generic/luaotfload/luaotfload-auxiliary.lua b/macros/luatex/generic/luaotfload/luaotfload-auxiliary.lua
index 86aff98fb4..379ad5bb78 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-auxiliary.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-auxiliary.lua
@@ -6,8 +6,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-auxiliary",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / auxiliary functions",
license = "GPL v2.0"
}
diff --git a/macros/luatex/generic/luaotfload/luaotfload-colors.lua b/macros/luatex/generic/luaotfload/luaotfload-colors.lua
index e0bd759de5..8371de7222 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-colors.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-colors.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-colors",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / color",
license = "GPL v2.0",
author = "Khaled Hosny, Elie Roux, Philipp Gesang, Dohyun Kim, David Carlisle",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-conf.pdf b/macros/luatex/generic/luaotfload/luaotfload-conf.pdf
index 9e3b6c6557..4abde8cbb9 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-conf.pdf
+++ b/macros/luatex/generic/luaotfload/luaotfload-conf.pdf
Binary files differ
diff --git a/macros/luatex/generic/luaotfload/luaotfload-configuration.lua b/macros/luatex/generic/luaotfload/luaotfload-configuration.lua
index 50618d72af..b970ce353e 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-configuration.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-configuration.lua
@@ -7,8 +7,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-configuration",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / config file reader",
license = "GPL v2.0"
}
diff --git a/macros/luatex/generic/luaotfload/luaotfload-database.lua b/macros/luatex/generic/luaotfload/luaotfload-database.lua
index 8f0ef41fc7..8d981a3f2c 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-database.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-database.lua
@@ -5,8 +5,8 @@
do -- block to avoid to many local variables error
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-database",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / database",
license = "GPL v2.0",
author = "Khaled Hosny, Elie Roux, Philipp Gesang, Marcel Krüger",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-diagnostics.lua b/macros/luatex/generic/luaotfload/luaotfload-diagnostics.lua
index 622714d14b..230384f981 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-diagnostics.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-diagnostics.lua
@@ -7,8 +7,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-diagnostics",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload-tool submodule / diagnostics",
license = "GPL v2.0"
}
diff --git a/macros/luatex/generic/luaotfload/luaotfload-dvi.lua b/macros/luatex/generic/luaotfload/luaotfload-dvi.lua
index 2f8abefa93..75f6564d42 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-dvi.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-dvi.lua
@@ -6,8 +6,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-dvi",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / DVI",
license = "GPL v2.0",
author = "Marcel Krüger",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-embolden.lua b/macros/luatex/generic/luaotfload/luaotfload-embolden.lua
index d0a2e60555..bca6eb45d8 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-embolden.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-embolden.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-embolden",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / embolden",
license = "GPL v2.0",
author = "Marcel Krüger"
diff --git a/macros/luatex/generic/luaotfload/luaotfload-fallback.lua b/macros/luatex/generic/luaotfload/luaotfload-fallback.lua
index a3f28e77a4..b45f454576 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-fallback.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-fallback.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-fallback",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / fallback",
license = "GPL v2.0",
author = "Marcel Krüger"
diff --git a/macros/luatex/generic/luaotfload/luaotfload-features.lua b/macros/luatex/generic/luaotfload/luaotfload-features.lua
index eecf1aa0f5..d297061a7d 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-features.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-features.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-features",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / features",
license = "GPL v2.0",
author = "Hans Hagen, Khaled Hosny, Elie Roux, Philipp Gesang, Marcel Krüger",
@@ -843,7 +843,10 @@ local normalize_lookup = setmetatable({}, {__index = function(t, f)
t[fontdir] = result
return result
end})
-local normalize_funcs = {
+-- When this is loaded as part of luaotfload-tool, then we can't access nodes
+-- and therefore uni_normalize doesn't exists. In that case we don't need it
+-- anyway, so just skip it then.
+local normalize_funcs = uni_normalize and {
nfc = uni_normalize.NFC,
nfd = uni_normalize.NFD,
nfkd = uni_normalize.NFKD,
diff --git a/macros/luatex/generic/luaotfload/luaotfload-filelist.lua b/macros/luatex/generic/luaotfload/luaotfload-filelist.lua
index abd6458db9..d0b1530830 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-filelist.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-filelist.lua
@@ -6,8 +6,8 @@
if luaotfload_module then luaotfload_module {
name = "luaotfload-filelist",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / filelist",
license = "GPL v2.0"
} end
diff --git a/macros/luatex/generic/luaotfload/luaotfload-glyphlist.lua b/macros/luatex/generic/luaotfload/luaotfload-glyphlist.lua
index ce3cc314b4..f291b2d60e 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-glyphlist.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-glyphlist.lua
@@ -1,8 +1,8 @@
if luaotfload_module then
luaotfload_module {
name = "luaotfload-glyphlist",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / autogenerated glyphlist",
copyright = "derived from https://raw.githubusercontent.com/adobe-type-tools/agl-aglfn/master/glyphlist.txt",
original = "Adobe Glyph List, version 2.0, September 20, 2002",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-harf-define.lua b/macros/luatex/generic/luaotfload/luaotfload-harf-define.lua
index d7ee7c11bb..add95cdf68 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-harf-define.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-harf-define.lua
@@ -5,8 +5,8 @@
do -- block to avoid to many local variables error
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-harf-define",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / HarfBuzz font loading",
license = "GPL v2.0",
author = "Khaled Hosny, Marcel Krüger",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua b/macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua
index 634da68786..222c4334e8 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-harf-plug.lua
@@ -4,8 +4,8 @@
-----------------------------------------------------------------------
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-harf-plug",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / HarfBuzz shaping",
license = "GPL v2.0",
author = "Khaled Hosny, Marcel Krüger",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-harf-var-cff2.lua b/macros/luatex/generic/luaotfload/luaotfload-harf-var-cff2.lua
index 9c05b7a6c7..38fa6b57d9 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-harf-var-cff2.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-harf-var-cff2.lua
@@ -5,8 +5,8 @@
do
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-harf-var-cff2",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / CFF2 table processing",
license = "GPL v2.0",
author = "Marcel Krüger",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-harf-var-t2-writer.lua b/macros/luatex/generic/luaotfload/luaotfload-harf-var-t2-writer.lua
index f10bf6d860..5964b61865 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-harf-var-t2-writer.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-harf-var-t2-writer.lua
@@ -5,8 +5,8 @@
do
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-harf-var-t2-writer",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / Type 2 charstring writer",
license = "GPL v2.0",
author = "Marcel Krüger",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-harf-var-ttf.lua b/macros/luatex/generic/luaotfload/luaotfload-harf-var-ttf.lua
index 80cf6768ae..41fb412102 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-harf-var-ttf.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-harf-var-ttf.lua
@@ -5,8 +5,8 @@
do
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-harf-var-ttf",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / gvar table processing",
license = "GPL v2.0",
author = "Marcel Krüger",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-init.lua b/macros/luatex/generic/luaotfload/luaotfload-init.lua
index 73a6544e72..3d00816e1a 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-init.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-init.lua
@@ -7,8 +7,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-init",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / initialization",
license = "GPL v2.0"
}
diff --git a/macros/luatex/generic/luaotfload/luaotfload-latex.pdf b/macros/luatex/generic/luaotfload/luaotfload-latex.pdf
index b87b90e0c8..61e35f01a4 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-latex.pdf
+++ b/macros/luatex/generic/luaotfload/luaotfload-latex.pdf
Binary files differ
diff --git a/macros/luatex/generic/luaotfload/luaotfload-letterspace.lua b/macros/luatex/generic/luaotfload/luaotfload-letterspace.lua
index 2e69cb92a6..bb5a4a5e70 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-letterspace.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-letterspace.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-letterspace",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / letterspacing",
license = "GPL v2.0",
copyright = "PRAGMA ADE / ConTeXt Development Team",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-loaders.lua b/macros/luatex/generic/luaotfload/luaotfload-loaders.lua
index 5a84113a1f..ca3730f00d 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-loaders.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-loaders.lua
@@ -8,8 +8,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-loaders",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / callback handling",
license = "GPL v2.0"
}
diff --git a/macros/luatex/generic/luaotfload/luaotfload-log.lua b/macros/luatex/generic/luaotfload/luaotfload-log.lua
index 41f9f4b877..c4ecd75075 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-log.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-log.lua
@@ -6,8 +6,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-log",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / logging",
license = "GPL v2.0",
author = "Khaled Hosny, Elie Roux, Philipp Gesang",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-main.tex b/macros/luatex/generic/luaotfload/luaotfload-main.tex
index cdf5590057..d48481a744 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-main.tex
+++ b/macros/luatex/generic/luaotfload/luaotfload-main.tex
@@ -32,7 +32,7 @@
\beginfrontmatter
\setdocumenttitle {The \identifier{luaotfload} package}
- \setdocumentdate {2022-02-24 v3.19}
+ \setdocumentdate {2022-02-27 v3.20}
\setdocumentauthor {LaTeX3 Project\\
Elie Roux · Khaled Hosny · Philipp Gesang · Ulrike Fischer · Marcel Krüger\\
Home: \hyperlink {https://github.com/latex3/luaotfload}}
@@ -62,6 +62,14 @@ Supported is the \identifier{luatex} versions of a current TeXLive 2019 (and a c
\endsection
\beginsection{Changes}
+\beginsubsection {New in version 3.20 (by Ulrike Fischer/Marcel Krüger)}
+\begin{itemize}
+ \item A bug in luaotfload-tool has been corrected.
+ \item The directory for the font name database has been corrected and no
+ longer uses the development directory.
+\end{itemize}
+\endsubsection
+
\beginsubsection {New in version 3.19 (by Ulrike Fischer/Marcel Krüger)}
\begin{itemize}
\item When used with Lua\TeX\ 1.15.0 or newer, variable fonts are supported
diff --git a/macros/luatex/generic/luaotfload/luaotfload-multiscript.lua b/macros/luatex/generic/luaotfload/luaotfload-multiscript.lua
index ab688fe704..9b3b3fd836 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-multiscript.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-multiscript.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-multiscript",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / multiscript",
license = "GPL v2.0",
author = "Marcel Krüger"
diff --git a/macros/luatex/generic/luaotfload/luaotfload-notdef.lua b/macros/luatex/generic/luaotfload/luaotfload-notdef.lua
index 28a371dd35..e32521daea 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-notdef.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-notdef.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-notdef",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / notdef",
license = "GPL v2.0",
author = "Marcel Krüger"
diff --git a/macros/luatex/generic/luaotfload/luaotfload-parsers.lua b/macros/luatex/generic/luaotfload/luaotfload-parsers.lua
index 099363fad5..da41fa8b30 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-parsers.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-parsers.lua
@@ -7,8 +7,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-parsers",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / parsers",
license = "GPL v2.0"
}
diff --git a/macros/luatex/generic/luaotfload/luaotfload-resolvers.lua b/macros/luatex/generic/luaotfload/luaotfload-resolvers.lua
index a91216b79d..5d3f456739 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-resolvers.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-resolvers.lua
@@ -11,8 +11,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-resolvers",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / resolvers",
license = "GPL v2.0"
}
diff --git a/macros/luatex/generic/luaotfload/luaotfload-scripts.lua b/macros/luatex/generic/luaotfload/luaotfload-scripts.lua
index c3c918ef3b..2accacfd64 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-scripts.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-scripts.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-script",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / Script helpers",
license = "CC0 1.0 Universal",
author = "Marcel Krüger"
diff --git a/macros/luatex/generic/luaotfload/luaotfload-status.lua b/macros/luatex/generic/luaotfload/luaotfload-status.lua
index d722bfe03b..02286a891b 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-status.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-status.lua
@@ -54,46 +54,46 @@ return {
{ "fontloader-font-ini.lua", "5343de60613adb24cdd7a8d2ee1d153c" },
{ "fontloader-data-con.lua", "23a0f5386d46a1c157ae82a373e35fa0" },
{ "fontloader-reference.lua", "066680a4fe38fad14eb27f56d4616104" },
- { "fontloader-basics-gen.lua", "e470c13b1ed41e38932e93518219ad74" },
+ { "fontloader-basics-gen.lua", "4bc43036e9796fccb0b8869f4b05bf2e" },
{ "mktests", "918cb50be9ee8bd645ac1a27dc501e8c" },
- { "mkstatus", "3a71ea95e4daeac77ead843edb0ce283" },
+ { "mkstatus", "a49a287defff8b8241faedb7c6a45621" },
{ "mkcharacters", "bfca0e96e1e351afe7077502f9595af9" },
{ "mkglyphlist", "7a8a8fdc0eac7fac2f13ec8f4e8e7aa9" },
{ "mkimport", "7d62f1042e548f8b9368f26b9ba4be5c" },
- { "luaotfload-glyphlist.lua", "dc0ed15ca157aec56a509819d4c24046" },
+ { "luaotfload-glyphlist.lua", "37275559314b6ced2c01f0d0e58fd68f" },
{ "luaotfload-characters.lua", "76415ca3ede312cad6e09038323489ae" },
- { "luaotfload-dvi.lua", "f4e4d6cc443f477820521abcd87ef3f8" },
- { "luaotfload-tounicode.lua", "79795ade0ccdb99cd5e6cf989770db69" },
- { "luaotfload-unicode.lua", "68904dd143491dc0a4a1c0a063a118f9" },
- { "luaotfload-resolvers.lua", "1e844bc5d9b3d207f8914a11254afd67" },
- { "luaotfload-parsers.lua", "b0c087c6dbf75939d7be9523b7a0ca34" },
- { "luaotfload-fallback.lua", "88ed544bc0429f94606f72dace5704f0" },
- { "luaotfload-szss.lua", "7266e9d2122fb0785b595f9605627640" },
- { "luaotfload-scripts.lua", "c6c11811531c7e42194304d65bed7877" },
- { "luaotfload-multiscript.lua", "f9d7bedf75629d83f8d99397d15fdf86" },
- { "luaotfload-loaders.lua", "b2ef32297527eec59131de3b56f4d846" },
- { "luaotfload-harf-plug.lua", "7f3739c1b32de1f8e76275c317f5ed9a" },
- { "luaotfload-harf-define.lua", "1c2ce18281caede23d93c8b825df7239" },
- { "luaotfload-notdef.lua", "8b448aed817a764b675d1ead6bf932a7" },
- { "luaotfload-embolden.lua", "00ae296b36724c4f8dd4da62051039b2" },
- { "luaotfload-letterspace.lua", "f98fbda3ea174058b515b9234dfba495" },
- { "luaotfload-features.lua", "5c0d7d66953c7ecdd2a610d758d70f0d" },
- { "luaotfload-database.lua", "2ac2d6e5960257a7c5688c931bb91924" },
- { "luaotfload-configuration.lua", "05e6c5032b9ac6f05230d06c866161d6" },
- { "luaotfload-colors.lua", "3e1cd496c920d0e8296a9e99357f27eb" },
- { "luaotfload-auxiliary.lua", "32c6b48ab66a2ddf2a7d3063529e85f8" },
- { "luaotfload-filelist.lua", "b80ed418d9d297370182e9fcc9e497e5" },
- { "luaotfload-tool.lua", "2a6a1c510e44c4432724657259b12cbf" },
- { "luaotfload-diagnostics.lua", "e831068d26e0266cd081de8585c3cbc9" },
- { "luaotfload-log.lua", "889508edccd01035ecbeafdeeff60e35" },
- { "luaotfload-init.lua", "56ac12fac2e63d1a901463850062dd58" },
+ { "luaotfload-dvi.lua", "17763f980ea431ef33578799e36121bd" },
+ { "luaotfload-tounicode.lua", "861919847a52fcbe4e625c8cda7826a2" },
+ { "luaotfload-unicode.lua", "5c90a8c752518c7eae0aa21f5d4e6cb5" },
+ { "luaotfload-resolvers.lua", "a3a547c332dcc51885b6611debcecb53" },
+ { "luaotfload-parsers.lua", "90411c0301b37730abb40a8b49507c8f" },
+ { "luaotfload-fallback.lua", "febc05a8a5701b4e9b73a9a24e994f33" },
+ { "luaotfload-szss.lua", "256bc3115f09638b0acdf4f59b06e4d1" },
+ { "luaotfload-scripts.lua", "9356ba71cf8a174e8e592213aa680297" },
+ { "luaotfload-multiscript.lua", "8f77beea00a1319d53e10ce95f6cc561" },
+ { "luaotfload-loaders.lua", "b0b23c070c570d81a32913d2afc1bb2a" },
+ { "luaotfload-harf-plug.lua", "098a6a6f9c17d2a99a76ce890e8c9dc6" },
+ { "luaotfload-harf-define.lua", "0d8692db5d29664eb32e6c5fb25b049f" },
+ { "luaotfload-notdef.lua", "c323ab19ea5e81c5c9c4bbbc6d62839a" },
+ { "luaotfload-embolden.lua", "50aa1bdac170cdb287345b9532520cbf" },
+ { "luaotfload-letterspace.lua", "79d2efd35a43d1bfba84ce7c5a211680" },
+ { "luaotfload-features.lua", "2348361d796bde017da72421a0e68ce6" },
+ { "luaotfload-database.lua", "d37c8efaed4dbe9bb566cbd8e9ba9216" },
+ { "luaotfload-configuration.lua", "cfdee94e34f3294b8ef627086ac464a3" },
+ { "luaotfload-colors.lua", "01070f297400b9d0bb94a321b16a3cc6" },
+ { "luaotfload-auxiliary.lua", "2a69d5f008d6c79b703ea6bd88f545f3" },
+ { "luaotfload-filelist.lua", "261c4306787e506da0ca5c15b09913f5" },
+ { "luaotfload-tool.lua", "252bdfdf017c7b2a1e1a92d129de05c2" },
+ { "luaotfload-diagnostics.lua", "146886b07d0f4f57b82ed77407d32066" },
+ { "luaotfload-log.lua", "7856e6afce049f6196ef94b2d2057ff6" },
+ { "luaotfload-init.lua", "3c3b19cd2f8bbbd8bdbc99c50535bff7" },
{ "luaotfload-main.lua", "b19c9cc34cf1d676c39f872cfb41aef6" },
},
["notes"]={
["committer"]="Marcel Fabian Krüger <tex@2krueger.de>",
- ["description"]="v3.19/2022-02-24",
+ ["description"]="v3.20/2022-02-27",
["loader"]="fontloader-2022-02-24.lua",
- ["revision"]="ef3a55e0da3709e95551c62376f31149771dcc18",
- ["timestamp"]="2022-02-24 14:16:28 +0100",
+ ["revision"]="b6b01d231706a79c8456c4c890d62a65897b49e1",
+ ["timestamp"]="2022-02-27 14:15:30 +0100",
},
} \ No newline at end of file
diff --git a/macros/luatex/generic/luaotfload/luaotfload-suppress.lua b/macros/luatex/generic/luaotfload/luaotfload-suppress.lua
index e0f3989c22..b0ba8dccc1 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-suppress.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-suppress.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-suppress",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / suppress",
license = "GPL v2.0",
author = "Marcel Krüger"
diff --git a/macros/luatex/generic/luaotfload/luaotfload-szss.lua b/macros/luatex/generic/luaotfload/luaotfload-szss.lua
index dcd8eacf65..6786226257 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-szss.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-szss.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-szss",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / Special feature handling",
license = "GPL v2.0",
author = "Marcel Krüger"
diff --git a/macros/luatex/generic/luaotfload/luaotfload-t3.lua b/macros/luatex/generic/luaotfload/luaotfload-t3.lua
deleted file mode 100644
index 2588346724..0000000000
--- a/macros/luatex/generic/luaotfload/luaotfload-t3.lua
+++ /dev/null
@@ -1,142 +0,0 @@
------------------------------------------------------------------------
--- FILE: luaotfload-t3.lua
--- DESCRIPTION: part of luaotfload / Type 3
------------------------------------------------------------------------
-do -- block to avoid to many local variables error
- assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
- name = "luaotfload-t3",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
- description = "luaotfload submodule / Type 3 font handling",
- license = "GPL v2.0",
- author = "Marcel Krüger",
- copyright = "Luaotfload Development Team",
- }
-end
-
---[[
-local function u8_to_utf16be(s)
- local d = {}
- local i = 0
- for _, cp, cp2 in utf8.codes(s) do
- i = i + 1
- if cp > 0x10000 then
- cp = cp - 0x10000
- local high = (cp>>10) | 0xD800
- local low = (cp&0x3FF) | 0xDC00
- d[i] = string.format('%04X%04X', high, low)
- else
- d[i] = string.format('%04X', cp)
- end
- end
- return table.concat(d, '', 1, i)
-end
-]]
-
-local function t3factory(basename, designsize, callback)
- local count = 0
- local gid_to_t3 = {}
- local t3_to_gid = {}
- return function(size)
- local fids = {}
- return function(gid, width, unicode)
- local t3id = gid_to_t3[gid]
- if not t3id then
- t3id = count
- count = count + 1
- gid_to_t3[gid] = t3id
- t3_to_gid[t3id] = gid
- end
- local t3fid, t3cid = t3id >> 8, ~(t3id & 0xFF)
- local fid = fids[t3fid + 1]
- local fontdir = fid and font.getfont(fid)
- local characters = fontdir and fontdir.characters
- if not (characters and characters[t3cid]) then
- characters = characters or {}
- -- First create the character
- local char = {
- width = width,
- height = 0, -- We never look at these two anyway
- depth = 0,
- tounicode = unicode,
- }
- characters[t3cid] = char
- if fid then -- Font already exists
- font.addcharacters(fid, {
- characters = {
- [t3cid] = char,
- },
- })
- else
- fontdir = {
- name = basename .. '_' .. basesize .. '_' .. t3fid,
- format = 'type3',
- psname = 'none',
- filename = 'not_a_real_filename', -- Can't be null to ensure reuse
- fullname = basename .. basesize .. '_' .. t3fid,
- characters = characters,
- designsize = basesize,
- size = size,
- cidinfo = {}, -- Can't be null to ensure reuse
- attributes = '/FontDescriptor<<\z
- /Type/FontDescriptor\z
- /FontName/VirtualLuaFont\z
- /Flags 4\z
- /ItalicAngle 0\z
- >>', -- TODO: Needs work
- t3_handler = function(stage, _, cid)
- cid = cid and assert(t3_to_gid[(t3fid << 8) | ~cid])
- return callback(stage, cid)
- end
- }
- fid = font.define(fontdir)
- fids[t3fid + 1] = fid
- end
- end
- vf.font(fid)
- vf.char(t3cid)
- end
- end
-end
-
-luatexbase.add_to_callback('provide_charproc_data', function(stage, fid, ...)
- local fontdir = font.getfont(fid)
- if not fontdir then error'Unknown font' end
- local handler = fontdir.t3_handler
- if not handler then error'Missing t3handler in Type 3 font' end
- return handler(stage, fid, ...)
-end, 'luaotfload.t3')
-
-return t3factory
---[===[
- function callback(stage, cid)
- if stage == 1 then
- local char = assert(characters[cid])
- local head = assert(char.head)
- local resource = tex.saveboxresource(head, nil--[[attributes]], nil--[[resources]], true--[[immediate]], 0--[[mode]], nil--[[margin]])
- local name = string.format('/F%i', pdf.getxformname(resource))
- local resources = characters.resources
- if resources then
- characters.resources = string.format('%s %s %i 0 R', resources, name, resource)
- else
- characters.resources = string.format('%s %i 0 R', name, resource)
- end
- char.resource = name
- elseif stage == 2 then
- local char = assert(characters[cid])
- width = char.width/65781
- return pdf.obj{
- type = 'stream',
- immediate = true,
- -- objnum =
- -- attr =
- -- compresslevel =
- string = string.format('%g 0 d0 %s Do', width, char.resource)
- }, width
- elseif stage == 3 then
- return 65781/655360, string.format('/XObject<<%s>>', characters.resources)
- end
- print(stage, fid, cid)
- end,
- }
-]===]
diff --git a/macros/luatex/generic/luaotfload/luaotfload-tool.1 b/macros/luatex/generic/luaotfload/luaotfload-tool.1
index 3f2782eb18..d0f56801c6 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-tool.1
+++ b/macros/luatex/generic/luaotfload/luaotfload-tool.1
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
-.TH LUAOTFLOAD-TOOL 1 "2022-02-24" "3.19" "text processing"
+.TH LUAOTFLOAD-TOOL 1 "2022-02-27" "3.20" "text processing"
.SH NAME
luaotfload-tool \- generate and query the Luaotfload font names database
.
diff --git a/macros/luatex/generic/luaotfload/luaotfload-tool.lua b/macros/luatex/generic/luaotfload/luaotfload-tool.lua
index 808afe0313..6be778a69a 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-tool.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-tool.lua
@@ -9,8 +9,8 @@
local ProvidesLuaModule = {
name = "luaotfload-tool",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload-tool / database functionality",
license = "GPL v2.0"
}
diff --git a/macros/luatex/generic/luaotfload/luaotfload-tool.pdf b/macros/luatex/generic/luaotfload/luaotfload-tool.pdf
index a218f77367..4862fd03f1 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-tool.pdf
+++ b/macros/luatex/generic/luaotfload/luaotfload-tool.pdf
Binary files differ
diff --git a/macros/luatex/generic/luaotfload/luaotfload-tool.rst b/macros/luatex/generic/luaotfload/luaotfload-tool.rst
index 1eddd510f1..d407a5fba5 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-tool.rst
+++ b/macros/luatex/generic/luaotfload/luaotfload-tool.rst
@@ -6,9 +6,9 @@
generate and query the Luaotfload font names database
-----------------------------------------------------------------------
-:Date: 2022-02-24
+:Date: 2022-02-27
:Copyright: GPL v2.0
-:Version: 3.19
+:Version: 3.20
:Manual section: 1
:Manual group: text processing
diff --git a/macros/luatex/generic/luaotfload/luaotfload-tounicode.lua b/macros/luatex/generic/luaotfload/luaotfload-tounicode.lua
index 09ca40adbe..9ef921ffd0 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-tounicode.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-tounicode.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-tounicode",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / tounicode",
license = "GPL v2.0",
author = "Hans Hagen, Khaled Hosny, Elie Roux, Philipp Gesang, Marcel Krüger",
diff --git a/macros/luatex/generic/luaotfload/luaotfload-unicode.lua b/macros/luatex/generic/luaotfload/luaotfload-unicode.lua
index 25a7b2046e..d63d16989c 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-unicode.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-unicode.lua
@@ -5,8 +5,8 @@
assert(luaotfload_module, "This is a part of luaotfload and should not be loaded independently") {
name = "luaotfload-unicode",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "luaotfload submodule / Unicode helpers",
license = "CC0 1.0 Universal",
author = "Marcel Krüger"
diff --git a/macros/luatex/generic/luaotfload/luaotfload.conf.5 b/macros/luatex/generic/luaotfload/luaotfload.conf.5
index f7d8aa8ae4..272a3abd54 100644
--- a/macros/luatex/generic/luaotfload/luaotfload.conf.5
+++ b/macros/luatex/generic/luaotfload/luaotfload.conf.5
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
-.TH LUAOTFLOAD.CONF 5 "2022-02-24" "3.19" "text processing"
+.TH LUAOTFLOAD.CONF 5 "2022-02-27" "3.20" "text processing"
.SH NAME
luaotfload.conf \- Luaotfload configuration file
.
diff --git a/macros/luatex/generic/luaotfload/luaotfload.conf.rst b/macros/luatex/generic/luaotfload/luaotfload.conf.rst
index 4e0347b907..c40088b5b5 100644
--- a/macros/luatex/generic/luaotfload/luaotfload.conf.rst
+++ b/macros/luatex/generic/luaotfload/luaotfload.conf.rst
@@ -6,9 +6,9 @@
Luaotfload configuration file
-----------------------------------------------------------------------
-:Date: 2022-02-24
+:Date: 2022-02-27
:Copyright: GPL v2.0
-:Version: 3.19
+:Version: 3.20
:Manual section: 5
:Manual group: text processing
diff --git a/macros/luatex/generic/luaotfload/luaotfload.lua b/macros/luatex/generic/luaotfload/luaotfload.lua
index f21ded5ae6..20eaad3eff 100644
--- a/macros/luatex/generic/luaotfload/luaotfload.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload.lua
@@ -20,8 +20,8 @@ local authors = "\z
-- version number is used below!
local ProvidesLuaModule = {
name = "luaotfload",
- version = "3.19", --TAGVERSION
- date = "2022-02-24", --TAGDATE
+ version = "3.20", --TAGVERSION
+ date = "2022-02-27", --TAGDATE
description = "Lua based OpenType font support",
author = authors,
copyright = authors,
diff --git a/macros/luatex/generic/luaotfload/luaotfload.sty b/macros/luatex/generic/luaotfload/luaotfload.sty
index 7dbccb1f8c..2426c2e0f5 100644
--- a/macros/luatex/generic/luaotfload/luaotfload.sty
+++ b/macros/luatex/generic/luaotfload/luaotfload.sty
@@ -39,7 +39,7 @@
\fi
\ifdefined\ProvidesPackage
\ProvidesPackage{luaotfload}%
- [2022/02/24 v3.19 OpenType layout system]
+ [2022/02/27 v3.20 OpenType layout system]
\fi
\directlua{
if xpcall(function ()%
diff --git a/macros/luatex/generic/luaotfload/scripts-demo.pdf b/macros/luatex/generic/luaotfload/scripts-demo.pdf
index e119a1ef16..1f04d9d7bc 100644
--- a/macros/luatex/generic/luaotfload/scripts-demo.pdf
+++ b/macros/luatex/generic/luaotfload/scripts-demo.pdf
Binary files differ
diff --git a/macros/luatex/generic/luaotfload/shaper-demo-graphite.pdf b/macros/luatex/generic/luaotfload/shaper-demo-graphite.pdf
index ded892e99b..ce126ad4b1 100644
--- a/macros/luatex/generic/luaotfload/shaper-demo-graphite.pdf
+++ b/macros/luatex/generic/luaotfload/shaper-demo-graphite.pdf
Binary files differ
diff --git a/macros/luatex/generic/luaotfload/shaper-demo.pdf b/macros/luatex/generic/luaotfload/shaper-demo.pdf
index d889e43e79..18a3c1b69f 100644
--- a/macros/luatex/generic/luaotfload/shaper-demo.pdf
+++ b/macros/luatex/generic/luaotfload/shaper-demo.pdf
Binary files differ
diff --git a/macros/luatex/generic/lutabulartools/README.md b/macros/luatex/generic/lutabulartools/README.md
index 90b5a86b15..ba443802b8 100644
--- a/macros/luatex/generic/lutabulartools/README.md
+++ b/macros/luatex/generic/lutabulartools/README.md
@@ -1,12 +1,13 @@
# lutabulartools
-This LuaLaTeX package offers some useful macros for tabular matter. The `\MC` command it provides allows one to specify a multi-row and or multi-column cell with minimal syntax. The package loads `booktabs` and adds a gray midrule, and allows a user to specify multiple `\cmidrules` with one command.
+This LuaLaTeX package offers some useful macros for tabular matter. The `\MC` command it provides allows one to specify a multi-row and or multi-column cell with minimal syntax. The package loads `booktabs` and adds a gray midrule, and allows a user to specify multiple `\cmidrules` with one command. The [github repo](https://github.com/kalekje/lutabulartools) has an MS Excel
+macro-enabled spreadsheet named "LaTeX-Tabular-Generator.xlsm" to help make tables in LaTeX.
# License (MIT)
-Copyright (C) 2021 Kale Ewasiuk
+Copyright (C) 2021-2022 Kale Ewasiuk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/macros/luatex/generic/lutabulartools/lutabulartools.lua b/macros/luatex/generic/lutabulartools/lutabulartools.lua
index ca6b1aa0b4..30165f89a9 100644
--- a/macros/luatex/generic/lutabulartools/lutabulartools.lua
+++ b/macros/luatex/generic/lutabulartools/lutabulartools.lua
@@ -1,6 +1,6 @@
--% Kale Ewasiuk (kalekje@gmail.com)
---% 2021-11-07
---% Copyright (C) 2021 Kale Ewasiuk
+--% 2022-02-27
+--% Copyright (C) 2021-2022 Kale Ewasiuk
--%
--% Permission is hereby granted, free of charge, to any person obtaining a copy
--% of this software and associated documentation files (the "Software"), to deal
@@ -24,31 +24,19 @@
local ltt = {}
---todo clean up the code, make some stuff local
---todo maybe add the penlight lua table to latex tabular?
+ltt.col_spec1 = {} -- column spec if one column wide (since makcell nests a tabular, preserve col_spec below)
+ltt.col_spec = {} -- tab column spec if above 1
+ltt.col = '' -- current column spec, single char, only applies to tabular with more than 1 column
+ltt.col_num = 1 -- current column number
---integrate current cell
---todo check if S[] column cells work!!!
---todo might be smarter to just append this to tabular commands and read argument
--- Initialize counter variable
-ltt.NumTabCols = 0
-ltt.NumTabColsAbv = 0 -- num of tab columns if above 1
-ltt.TabColSpec = ''
-ltt.TabColSpecAbv = '' -- tab column spec if above 1
-ltt.CurTabColAbv = '' -- current tab column spec if above 1
-ltt.TabColNum = 0
-ltt.NumBkts = 0
-ltt.NumTabColsMX = 0
-
-
---todo re=enable
local glue_t, unset_t, tabskip_st = node.id'glue', node.id'unset'
local tabskip_st = table.swapped(node.subtypes'glue').tabskip
assert(tabskip_st)
-function ltt.get_TabColNum()
+function ltt.set_col_num()
+ -- register current column info (column number and specification)
local nest
for i = tex.nest.ptr, 1, -1 do
local tail = tex.nest[i].tail
@@ -62,57 +50,32 @@ function ltt.get_TabColNum()
for _, sub in node.traverse_id(unset_t, nest.head) do
col = col + sub + 1
end
- ltt.TabColNum = col
+ ltt.col_num = col
else
- ltt.TabColNum = 1
+ ltt.col_num = 1
end
- ltt.CurTabColAbv = ltt.TabColSpecAbv:sub(ltt.TabColNum,ltt.TabColNum)
- ltt.NumTabColsMX = math.max(ltt.NumTabColsMX, ltt.TabColNum) -- a different way to calculate total cols
- return ltt.TabColNum
+ ltt.col = ltt.col_spec[ltt.col_num]
end
-function ltt.get_TabRowNum()
- return tex.count['c@RowNumCnt'] -- access latex counter
-end
-
--- The next function computes the number of columns from
--- contents of string 'zz'
-function ltt.calc_NumTabCols( zz )
- --help_wrt(zz, 'col spec OG')
- -- -- NOT NEEDED: zz = zz:gsub ( "^.-(%b{}).-$", "%1" ) -- retain the first "{....}" substring
+function ltt.set_col_spec(zz)
+ -- contents of string 'zz'
+ -- register the table column specification
zz = zz:gsub ( "%*%s-{(%d-)}%s-(%b{})" , -- expand expressions such as "*{5}{l}" to "lllll"
function(y, z ) z = z:sub (2 , -2) return string.rep (z, y) end ) --
zz = zz:gsub ( "%b{}" , "" ) -- omit all stuff in curly braces and square
zz = zz:gsub ( "%b[]" , "" )
zz = zz:gsub ( "[@!|><%s%*\']" , "" ) -- some more characters to ignore
- -- todo: any columns defined that break into more than one column should be expanded here
- --help_wrt(zz, 'col spec CLEAN')
- ltt.TabColSpec = zz -- stripped column spec with no {} or <
- ltt.NumTabCols = string.len(ltt.TabColSpec)
- if ltt.NumTabCols > 1 then
- ltt.TabColSpecAbv = ltt.TabColSpec
- ltt.NumTabColsAbv = ltt.NumTabCols
+ zz = zz:gsub('%a', ltt.col_replaces) -- sub extra column
+ _col_spec = zz:totable() -- requires pl extras
+ --help_wrt(_col_spec, 'helpme')
+ if #_col_spec > 1 then
+ ltt.col_spec = _col_spec
+ else
+ ltt.col_spec1 = _col_spec
end
end
---todo fix square brackets after letters, expand multiple
---calc_NumTabCols('ss')
---calc_NumTabCols('*{6}{s}')
---calc_NumTabCols('l*{6}{l}')
---calc_NumTabCols('lll')
---calc_NumTabCols('ll[]')
---calc_NumTabCols('ll[]*{6}{l}')
---calc_NumTabCols('*{6}{l}')
---calc_NumTabCols('y*{6}{sq}x')
---print(_TabColSpec.. '<---Column spec')
-
---http://ctan.mirror.rafal.ca/macros/latex/contrib/multirow/multirow.pdf
---http://ctan.mirror.colo-serv.net/macros/latex/contrib/makecell/makecell.pdf
--- todo CONSIDER THIS
--- https://tex.stackexchange.com/questions/331716/newline-in-multirow-environment
-
-
--todo
-- if p{} column, and multirow is 1, use {=} instead of {*}
@@ -122,6 +85,8 @@ end
function ltt.MagicCell(s0,spec,mcspec,pre,content)
--
+ ltt.set_col_num() -- register current column number and column spec
+
local STR = ''
reset_bkt_cnt()
@@ -132,8 +97,8 @@ function ltt.MagicCell(s0,spec,mcspec,pre,content)
h, mcspec, c = ltt.get_HColSpec(h, mcspec, c) -- infer horizontal alignment, num columns
--help_wrt(_CurTabColAbv,'current column')
- if s0 == _xTrue or (pl.List({'S', 'Q', 'L', 'R'}):contains(ltt.CurTabColAbv) -- special columns for SI
- and c == '') then -- multicolumn cannot have {} around it todo test with siunitx, num rows, num columns
+ if s0 == _xTrue or (pl.List(ltt.SI_cols):contains(ltt.col) -- special columns for SI
+ and c == '') then -- multicolumn cannot have {} around it
STR = STR .. '{' -- multirow and makcell must have {} around it S column is used
add_bkt_cnt()
end
@@ -167,7 +132,7 @@ end
function ltt.parse_MagicCell_spec(spec)
- local mrowsym = '*'
+ local mrowsym = '*' -- * = natural width, = will match p{2cm} for example
local skipmakecell = false
if string.find(spec, '=') then
spec = spec:gsub('=', '')
@@ -175,9 +140,9 @@ function ltt.parse_MagicCell_spec(spec)
skipmakecell = true
end
- spec = spec:lower():gsub('%s','') -- take lower case and remove
+ spec = spec:lower():gsub('%s','') -- take lower case and remove space
local vh, rc = spec:gextract('%a') -- extract characters
- local v = vh:gfirst({'t', 'm', 'b'}) or 't'
+ local v = vh:gfirst({'t', 'm', 'b'}) or ltt.col_ver_repl[ltt.col] or 't'
local h = vh:gfirst({'l', 'c', 'r'}) or ''
v = v:gsub('m', 'c')
@@ -191,50 +156,71 @@ function ltt.parse_MagicCell_spec(spec)
end
-ltt.TabColMapping = { -- horizontal cell alignment that multicolumn should use if () or [hori] not passed to func
- l = 'l',
- c = 'c',
- r = 'r',
- p = 'p',
- P = 'c',
- X = 'l',
- Y = 'c',
- Z = 'l',
- N = 'c',
- L = 'l',
- R = 'r',
- C = 'c',
-}
-
function ltt.get_HColSpec(h, mcspec, c) -- take horizontal alignment
-- c is num columns, h is horizontal alginment,
--Assumes _TabColNum was calculated previosly
- ltt.get_TabColNum()
if c == '+' then -- fill row to end
- c = tostring(ltt.NumTabColsAbv - ltt.TabColNum + 1)
+ c = tostring(#ltt.col_spec - ltt.col_num + 1)
end
if h == '' then -- if horizontal not provided, use declared column
- h = ltt.TabColMapping[ltt.CurTabColAbv] or 'l'
+ h = ltt.col_hor_repl[ltt.col] or 'l'
end
if c ~= '' then -- only make new mcspec if column nums > 0
if mcspec == '' then -- and if no mcspec was passed
mcspec = h
- if ltt.TabColNum == 1 then -- if first column, auto detect padding
+ if ltt.col_num == 1 then -- if first column, auto detect padding
mcspec = '@{}'..mcspec
end
- if (ltt.TabColNum + tonumber(c) - 1) == ltt.NumTabColsAbv then -- if end on last column
+ if (ltt.col_num + tonumber(c) - 1) == #ltt.col_spec then -- if end on last column
mcspec = mcspec..'@{}'
end
else -- if mcspec if given, extract the alignment
- ltt.calc_NumTabCols(mcspec)
- h = ltt.TabColSpec -- get 1 character column spec from mcspec and override h
+ ltt.set_col_spec(mcspec)
+ h = ltt.col_spec1[1] -- get 1 character column spec from mcspec and override h
end
end
return h, mcspec, c
end
--- midrule stuff
+
+ltt.col_ver_repl = {
+m = 'm',
+M = 'm',
+b = 'b',
+}
+
+ltt.col_hor_repl = { -- horizontal cell alignment that multicolumn should use if () or [hori] not passed to func
+ l = 'l',
+ c = 'c',
+ r = 'r',
+ p = 'l',
+ P = 'c',
+ X = 'l',
+ Y = 'c',
+ Z = 'l',
+ N = 'c',
+ L = 'l',
+ R = 'r',
+ C = 'c',
+}
+
+-- allow user to place their own replacements in for a table, say if they define a column that expands to multiple
+ltt.col_replaces = {
+--x = 'lll'
+}
+
+ltt.SI_cols = {'S', 'N', 'Q', 'L', 'R'}
+--ltt.SI_cols = {'S'}
+--ltt.SI_cols = {'N'}
+
+
+
+
+
+-----
+--- midrule stuff
+-----
function ltt.get_midrule_col(s)
if string.find(s, '+') then
@@ -242,14 +228,11 @@ function ltt.get_midrule_col(s)
if (s == '') or (s == '0') then
s = 1
end
- s = tostring(ltt.NumTabColsAbv - tonumber(s) + 1) -- use number of tabular columns above 0,
+ s = tostring(#ltt.col_spec - tonumber(s) + 1) -- use number of tabular columns above 0,
end
return s
end
---todo if comma present, create multiple according to spec. Also allow LR in []
-
---- midrule stuff ---
function ltt.make1cmidrule(s, r, c, cmd) -- s=square r=round c=curly
cmd = '\\'..cmd
@@ -283,4 +266,16 @@ function ltt.makecmidrules(s, r, c, cmd)
end
-return ltt -- lutabulartools \ No newline at end of file
+--help_wrt('TEST COL ')
+--for _, s in ipairs{ 'll', '*{6}{s}', 'l*{6}{l}', 'lll', 'll[]', 'll[]*{6}{l}', '*{6}{l}', 'y*{6}{sq}x', } do
+-- ltt. set_col_spec(s)
+-- help_wrt(ltt.col_spec,s)
+--end
+
+return ltt -- lutabulartools
+
+
+
+--http://ctan.mirror.rafal.ca/macros/latex/contrib/multirow/multirow.pdf
+--http://ctan.mirror.colo-serv.net/macros/latex/contrib/makecell/makecell.pdf
+-- https://tex.stackexchange.com/questions/331716/newline-in-multirow-environment
diff --git a/macros/luatex/generic/lutabulartools/lutabulartools.pdf b/macros/luatex/generic/lutabulartools/lutabulartools.pdf
index 96488bd83a..770fb8045b 100644
--- a/macros/luatex/generic/lutabulartools/lutabulartools.pdf
+++ b/macros/luatex/generic/lutabulartools/lutabulartools.pdf
Binary files differ
diff --git a/macros/luatex/generic/lutabulartools/lutabulartools.sty b/macros/luatex/generic/lutabulartools/lutabulartools.sty
index b4eda9c3cf..bb72b660a8 100644
--- a/macros/luatex/generic/lutabulartools/lutabulartools.sty
+++ b/macros/luatex/generic/lutabulartools/lutabulartools.sty
@@ -1,6 +1,6 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-11-07
-% Copyright (C) 2021 Kale Ewasiuk
+% 2022-02-27
+% Copyright (C) 2021-2022 Kale Ewasiuk
%
% Permission is hereby granted, free of charge, to any person obtaining a copy
% of this software and associated documentation files (the "Software"), to deal
@@ -23,7 +23,7 @@
% OR OTHER DEALINGS IN THE SOFTWARE.
\NeedsTeXFormat{LaTeX2e}
-\ProvidesPackage{lutabulartools}[2021-11-07]
+\ProvidesPackage{lutabulartools}[2022-02-27]
\RequirePackage{booktabs}
\RequirePackage{multirow}
@@ -38,12 +38,19 @@
\luadirect{lutabt = require('lutabulartools')}
+\newcolumntype{\ltt@ltrim}{@{}}
+\newcolumntype{\ltt@rtrim}{@{}}
+\DeclareOption{notrim}{
+ \newcolumntype{\ltt@ltrim}{}
+ \newcolumntype{\ltt@rtrim}{}
+}
-% DOES NOT WORK WITH colortbl package ???
%% adapted from
% https://tex.stackexchange.com/questions/182953/add-midrule-every-5-lines-automatically
+%% https://tex.stackexchange.com/questions/145084/what-is-the-function-of-noalign
+
\def\midruleXstep{5}
\def\midruleXrule{\gmidrule}
@@ -67,32 +74,18 @@
}
\newcommand*{\resetmidruleX}{\setcounter{midruleX}{0}} %%% todo maybe put noalign in here
-
\resetmidruleX
-%% https://tex.stackexchange.com/questions/145084/what-is-the-function-of-noalign
-%\begin{tabular}{rclc@{\midruleX}} % inject midrule
-%\toprule
-%Number & Student Id & Name & Score\\\midrule\resetmidruleX % reset to 0
-%1 & 14-001 & Andy & 75 \\
-%2 & & & \\
-%3 & & & \\
-%4 & & & \\
-%5 & & & \\
-%6 & & & \\
-%7 & & & \\
-%8 & & & \\
-%9 & & & \\\resetmidruleX
-%10 & & & \\%\noalign{\resetmidruleX} % noalig required to not cause error with bottomrule, but that's oka because it should be used on row 9 anyway
-%\bottomrule
-%\end{tabular}
+
%%%%
%%% gray midrule and gray cmidrule
-
-\providecommand{\gmidrule}{\arrayrulecolor{lightgray}\specialrule{\lightrulewidth}{0.4\aboverulesep}{0.6\belowrulesep}\arrayrulecolor{black}}
+\aboverulesep=0.0ex
+\belowrulesep=0.5ex
+\providecommand{\gmidrule}{\arrayrulecolor{lightgray}\specialrule{\lightrulewidth}{0.5\aboverulesep}{0.5\belowrulesep}\arrayrulecolor{black}}
+\cmidrulewidth=\lightrulewidth
%%% use gcmidrule for a gray "c" midrule
% Copy \cmidrule from
@@ -113,7 +106,7 @@
\global\advance\@cmidlb-\@cmidla
\global\@thisrulewidth=#3
\@setrulekerning{#4}
- \ifnum\@lastruleclass=\z@\vskip 0.4\aboverulesep\fi
+ \ifnum\@lastruleclass=\z@\vskip 0.5\aboverulesep\fi
\ifnum0=`{\fi}\@gtempa
\noalign{\ifnum0=`}\fi\futurenonspacelet\@tempa\@xgcmidrule}
\def\@xgcmidrule{%
@@ -123,7 +116,7 @@
\else \ifx\@tempa\morecmidrules
\vskip \cmidrulesep
\global\@lastruleclass=\@ne\else
- \vskip 0.6\belowrulesep
+ \vskip 0.5\belowrulesep
\global\@lastruleclass=\z@
\fi\fi
\ifnum0=`{\fi}
@@ -167,30 +160,17 @@
-
-
-%%% tabular commands
-
-\newcommand{\TabColNum}{\luadirect{tex.print(lutabt.get_TabColNum())}} % prints the current column number of tabular
-\newcommand{\NumTabCols}{\luadirect{tex.print(lutabt.NumTabCols)}} % prints the total num columns of tabular
-\newcounter{RowNumCnt}
-\setcounter{RowNumCnt}{1}
-
\let\oldtabular\tabular
\RenewExpandableDocumentCommand{\tabular}{ O{c} m }{%
- \setcounter{RowNumCnt}{1}%
- \luadirect{lutabt.calc_NumTabCols(\luastringN{#2})}%
- \luadirect{lutabt.NumTabColsMX = 0}% fall back if calc doesnt work for some reason
- \oldtabular[#1]{@{\stepcounter{RowNumCnt}}#2@{\luadirect{lutabt.get_TabColNum()}}}%
+ \luadirect{lutabt.set_col_spec(\luastringN{#2})}%
+ \oldtabular[#1]{\ltt@ltrim#2\ltt@rtrim}%
}
\expandafter\let\expandafter\oldtabulars\csname tabular*\endcsname
\expandafter\let\expandafter\endoldtabulars\csname endtabular*\endcsname
\RenewDocumentEnvironment{tabular*}{ m O{c} m }{%
- \setcounter{RowNumCnt}{1}%
- \luadirect{lutabt.calc_NumTabCols(\luastringN{#3})}%
- \luadirect{lutabt.NumTabColsMX = 0}% fall back if calc doesnt work for some reason
- \begin{oldtabulars}{#1}[#2]{@{\stepcounter{RowNumCnt}}#3@{\luadirect{lutabt.get_TabColNum()}}}
+ \luadirect{lutabt.set_col_spec(\luastringN{#3})}%
+ \begin{oldtabulars}{#1}[#2]{\ltt@ltrim#3\ltt@rtrim}%
}{%
\end{oldtabulars}%
}
@@ -211,7 +191,45 @@
)}}
+\NewDocumentCommand{\setMCrepl}{m m}{\luadirect{
+ lutabt.col_replaces.#1 = '#2'
+}}
+\NewDocumentCommand{\setMChordef}{ m m}{\luadirect{
+ lutabt.col_hor_repl.#1 = '#2'
+}}
+\NewDocumentCommand{\setMCverdef}{ m m}{\luadirect{
+ lutabt.col_ver_repl.#1 = '#2'
+}}
+\NewDocumentCommand{\addMCsicol}{ m }{\luadirect{
+ lutabt.SI_cols[\string##lutabt.SI_cols+1] = '#1'
+}}
+
\ProcessOptions
+
+
+
+
+
+
+%%% OLD STUFF,
+%% before I had a fall-back for counting columns (just calculate current column on last column once)
+%% and kept track of row numbers. I felt that this wasn't needed, so I've removed.
+%\newcommand{\TabColNum}{\luadirect{tex.print(lutabt.get_TabColNum())}} % prints the current column number of tabular
+%\newcommand{\NumTabCols}{\luadirect{tex.print(lutabt.NumTabCols)}} % prints the total num columns of tabular
+%\newcounter{RowNumCnt}
+%\setcounter{RowNumCnt}{1}
+% \setcounter{RowNumCnt}{1}%
+% \luadirect{lutabt.NumTabColsMX = 0}% fall back if calc doesnt work for some reason
+% \setcounter{RowNumCnt}{1}%
+% \luadirect{lutabt.NumTabColsMX = 0}% fall back if calc doesnt work for some reason
+%\DeclareOption{notrimx}{
+% \global\newcolumntype{\ltt@ltrim}{!{\stepcounter{RowNumCnt}}}
+%% \global\newcolumntype{\ltt@rtrim}{!{\luadirect{lutabt.get_TabColNum()}}}
+%}
+%\DeclareOption{trimx}{
+% \newcolumntype{\ltt@ltrim}{@{\stepcounter{RowNumCnt}}}
+%% \newcolumntype{\ltt@rtrim}{@{\luadirect{lutabt.get_TabColNum()}}}
+%}
diff --git a/macros/luatex/generic/lutabulartools/lutabulartools.tex b/macros/luatex/generic/lutabulartools/lutabulartools.tex
index 65c7f2f694..54af32f78d 100644
--- a/macros/luatex/generic/lutabulartools/lutabulartools.tex
+++ b/macros/luatex/generic/lutabulartools/lutabulartools.tex
@@ -1,6 +1,6 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-11-07
-% Copyright (C) 2021 Kale Ewasiuk
+% 2022-02-27
+% Copyright (C) 2021-2022 Kale Ewasiuk
%
% Permission is hereby granted, free of charge, to any person obtaining a copy
% of this software and associated documentation files (the "Software"), to deal
@@ -61,11 +61,20 @@
\title{lutabulartools}
\subtitle{some useful tabular tools (LuaLaTeX-based)}
+\usepackage{tabularx}
+\usepackage{siunitx}
+
\begin{document}
+
+% todo: can improve naming... set col spec to a table instead of a string
+
+
\maketitle
lutabulartools is a package that contains a few useful macros to help with tables.
-Most functions require LuaLaTeX. The following packages are loaded:
+Most functions require LuaLaTeX.
+This package redefines the {\ttfamily tabular} and {\ttfamily tabular*} environments.
+The following packages are loaded:
\texttt{
{booktabs},
{multirow},
@@ -78,51 +87,148 @@ Most functions require LuaLaTeX. The following packages are loaded:
{penlight},
}
+\section{Options}
+The author likes tables with left and right-most columns flush to the end.
+Since the \cmd{tabular} env is redefined in this, the author took the liberty to
+automatically pad the \cmd{tabular} cell spec with \cmd{@{}} on each end.
+If you don't want this, you can pass \cmd{notrim} to the package. Or,
+you can manually fix it in a \cmd{tabular} with \cmd{!{}} like so:
+
+\begin{LTXexample}
+\begin{tabular}{!{}c!{}}\toprule
+Xyz\\\bottomrule \end{tabular}
+\end{LTXexample}
+
+
\section{\texttt{\textbackslash MC} -- Magic Cell}
\texttt{\textbackslash MC} (magic cell) combines the facilities of
\cmd{\multirow} and \cmd{\multicolumn} from the \texttt{multirow} package, and \cmd{\makcell} from the titular package.
With the help of LuaLaTeX, it takes an easy-to-use cell specification and employs said commands as required.
+\cmd{\MC} will not work properly if your table is only 1 column wide (you probably don't need \cmd{MC} in that case anyway...).
Here is the usage:
\texttt{\textbackslash MC * [cell spec] <cell format> (override multicolumn col) \{contents\} }
\llcmd{*}This will wrap the entire command in \{\}. This is necessary for \texttt{siunitx} single-column width columns.
-However, the \tMC\ command attempts to detect this automatically.
+However, the \cmd{\MC} command attempts to detect this automatically.
\llcmd{[cell spec]}%
Any letters placed in this argument are used for cell alignment.
You can use one of three: \qcmd{t}, \qcmd{m}, \qcmd{b} for top, middle, bottom (vertical alignment),
or \qcmd{l}, \qcmd{c}, \qcmd{r} for horizontal alignment.
-By default, \tMC\ will try to autodetect the horizontal alignment based on the current column.
+By default, \cmd{\MC} will try to autodetect the horizontal alignment based on the current column.
If it can't, it will be left-aligned.
-The default vertical alginment is top.
+The default vertical alginment is top. More on this in section \ref{s.def}.
This argument can also contain two integers, separated by a comma (if two are used).
\qcmd{C,R}, \qcmd{C}, or \qcmd{,R} are a valid inputs,
-where R=rows (int), and C=columns, (int).
+where \cmd{R}=rows (int), and \cmd{C}=columns, (int).
If you want a 1 column wide, multirow cell,
-you can pass \qcmd{,R}. These numbers can be negative.
-If no spec is passed, (argument empty), \tMC\
+you can pass \qcmd{,R}. These numbers can be negative (positive numbers occupy columns to the right
+and rows below, and negative numbers occupy columns to the left and rows above).
+If no spec is passed, (argument empty), \cmd{\MC}
acts like a \texttt{makecell}.
Additionally, you can pass \qcmd{+} in place of C (number of columns wide),
and it will make the cell width fill until the end of the current row.
Examples:\\
\qcmd{\MC[2,2]} means two columns wide, two rows tall.\\
-\qcmd{\MC[2,1]} or \qcmd{\MC[2]} or means two columns wide, one row tall.\\
+\qcmd{\MC[2,1]} or \qcmd{\MC[2]} means two columns wide, one row tall.\\
\qcmd{\MC[1,2]} or \qcmd{\MC[,2]} means one column wide, two rows tall.\\
+\qcmd{\MC[+,2]}, if placed in the first column, occupies the entire row and is two rows tall.\\
+\qcmd{\MC[+,2]}, if placed in the second column, occupies the second column to the end of the table and is two rows tall.\\
In any of these examples, you can place the alignment letters anywhere.
+So, \qcmd{\MC[l1,2b]} and \qcmd{\MC[1,2 lb]} are both left-bottom aligned (spaces are ignored).
\llcmd{(override}%
You may want to adjust the column specification of a multicolumn cell,\\
\llcmd{multicolumn)}for example, using
-\texttt{@\{\}c@\{\}} to remove padding between the cell.
+\texttt{(@\{\}c@\{\})} to remove padding between the cell.
+
\llcmd{<cell format>}%
You can place formatting like \cmd{\bfseries} here.
-Here's an example.
+
+\subsection{Defaults}\label{s.def}
+The \cmd{tabular[*]} environment is re-defined to use Lua pattern matching
+to parse the column specification of the table. This is done to determine how many columns there are,
+and what the current column type is, even if specifications like \cmd{r@{.}l*{3}{r}} are used.
+If you have defined a column that expands many, you should register it with
+\cmd{\setMCrepl{?}{??}} where \cmd{?} is your column and \cmd{??} is what it expands to.
+You can also specify default horizontal and vertical alignments (ie if alignment not passed to \cmd{\MC})
+for an arbitrary column by \cmd{\setMChordef{?}{l|r|c}} and\\
+\cmd{\setMChordef{?}{t|m|b}}, where \cmd{?} is the column.
+To add a column that should be surrounded by brackets for \cmd{siunitx} purposes, do so with \cmd{\addMCsicol{?}}.
+S is included by default.
+
+
+\newpage
+
+\subsection{Examples}
+
+\let\nl\\
+
+
+\subsubsection{A good use for headers}
+\begin{LTXexample}
+\begin{tabular}{ l l l }\toprule
+\MC[+m]<\itshape>{A Decent
+ Example}\\\midrule
+ & \MC[2m]{Heading} \\\cmidrule{2-}
+\MC[b,-2]{Multi\\Line} & A & B \\\midrule
+\end{tabular}
+\end{LTXexample}
+
+\subsubsection{A small example}
+
+
+\begin{LTXexample}
+\begin{tabular}{ l l l }\midrule
+ A & \MC[mc2,2]{Lttrs} \\
+ B & \\
+ 1 & A & B \\
+\end{tabular}
+\end{LTXexample}
+
+
+\subsubsection{A small bad example}
+Notice that the top-aligned p-column
+doesn't play particularly well with the middle aligned \cmd{\MC}
+\begin{LTXexample}
+\begin{tabular}{ p{1cm} l }\toprule
+ hello\newline world
+ & \MC[mr]{11\\2} \\
+\end{tabular}
+\end{LTXexample}
+
+
+\subsubsection{If you insist on vertical lines}
+\begin{LTXexample}
+\begin{tabular}{|c|c|c|} \hline
+1 & 2 & 3\\\hline
+4 & \MC[2,2cm](@{}c@{}|)%
+ <\ttfamily>{5}\\\cline{1-1}
+ & \MC[2](r|){} \\\hline%hacky fix
+6 & 7 & 8\\\hline
+\end{tabular}
+\end{LTXexample}
+
+
+\subsubsection{A perhaps useful example}
+
+\begin{LTXexample}
+\begin{tabularx}{\linewidth}{S[table-format=2.1,table-alignment=left]X}
+% err & ... \\% ERROR, not wrap
+ \MC{Error,\%} & Comment \\% MC helps
+ 3.1 & \MC[,2]{multi-line\\comment}\\
+ 10.1& \\
+ \MC[2c]{... ...} \\
+\end{tabularx}
+\end{LTXexample}
+
+\subsubsection{A messy example}
\begin{LTXexample}
\begin{tabular}{| c | c | c | c | c | c |}\toprule
@@ -136,23 +242,39 @@ Here's an example.
\end{LTXexample}
+
+
+
%%% https://tex.stackexchange.com/questions/287346/width-of-column-after-multicolumn-header
%\textbackslash MC[<spec>]{<contents}
-\subsection{Notes}
-This package redefines the {\ttfamily tabular} and {\ttfamily tabular*} environments.
-It uses Lua pattern matching to parse the column specification of the table to know how many columns there are,
-and what the current column type is. If you have defined a column that creates many, it will not work.
-This will be worked out in later package revisions.
-%But, to know how many columns there are you, you will have
-%either have to manually set todo the lua variable before the table, or,
-%in the last column of the first row, \texttt{get\_tab\_colnum ()}.
-%The above Lua function can be used to get the current column number.
-%You will also remove the ability for this package to know what the current column spec is
+
+%\subsubsection{And another one}
+%
+%\begin{LTXexample}
+%\setMChordef{l}{r}
+%\setMCverdef{l}{b}
+%% change deflt alignment ^
+%\newcolumntype{e}{ll}
+%\setMCrepl{e}{ll}
+%% e -> l l ^
+%\begin{tabular}{l l l e}\toprule
+%aaaaa & bbb & ccc & eee & fff\\
+%\MC{aaaaa}\\
+%\MC{a}\\\midrule
+%\MC{a\\bbbbb} & x\\\midrule
+%\makecell[br]{a\\b} & x\\\midrule
+%\MC[+]{--- ---}\\
+%\end{tabular}
+%\end{LTXexample}
+%% WHY ISNT THIS RIGHT?
+\newpage
+
+
\section{Some additional rules}
This package also redefines the \texttt{booktabs} midrules.\\
\llcmd{\gmidrule}is a full gray midrule.\\
@@ -171,11 +293,23 @@ override it for a specific rule by placing \qcmd{r} or \qcmd{l} with the span sp
Here's an example:
\begin{LTXexample}
-\begin{tabular}{c c c c c c}\toprule
- 1 & 2 & 3 & 4 & 5 & 6\\ \cmidrule{+1} % rule on last column
- 1 & 2 & 3 & 4 & 5 & 6\\ \cmidrules{1,3-+3,+} % rule on first col, third to third last col, and last col
- 1 & 2 & 3 & 4 & 5 & 6\\ \cmidrules{1,3-+3rl,+} % same as above, but trim middle
- 1 & 2 & 3 & 4 & 5 & 6\\ \cmidrules(l){1,r3-+3,+1}% trim left for all, but only trim right for middle rule
+\begin{tabular}{c c c c c c}
+ 1 & 2 & 3 & 4 & 5 & 6\\
+ \cmidrule{+1} % rule on last column
+ 1 & 2 & 3 & 4 & 5 & 6\\
+ \cmidrules{1,3-+3,+} % rule on first col, third to third last col, and last col
+ 1 & 2 & 3 & 4 & 5 & 6\\
+ \cmidrules{1,3-+3rl,+} % same as above, but trim middle
+ 1 & 2 & 3 & 4 & 5 & 6\\
+ \cmidrules(l){1,r3-+3,+1}% trim left for all, but only trim right for middle rule
+ 1 & 2 & 3 & 4 & 5 & 6\\
+ \gcmidrule{+1} % rule on last column
+ 1 & 2 & 3 & 4 & 5 & 6\\
+ \gcmidrules{1,3-+3,+} % rule on first col, third to third last col, and last col
+ 1 & 2 & 3 & 4 & 5 & 6\\
+ \gcmidrules{1,3-+3rl,+} % same as above, but trim middle
+ 1 & 2 & 3 & 4 & 5 & 6\\
+ \gcmidrules(l){1,r3-+3,+1}% trim left for all, but only trim right for middle rule
\end{tabular}
\end{LTXexample}
@@ -192,13 +326,15 @@ You can change the step size and what kind of midrule you prefer.
\end{verbatim}
-Usage: Insert midrulex at the end of each row using the column spec.
+To use, insert \cmd{@{\midrulex}} at the end of each row using the tabular column spec.
Before you want counting to begin, you should
-apply resetmidruleX (also to avoid header rows).
+apply \cmd{\resetmidruleX} to avoid header rows being counted.
+Use \cmd{\noalign{\resetmiduleX}} if you need place a rule on the same line or in a cell.
+
\begin{LTXexample}
-\def\midruleXstep{4}
-\def\midruleXrule{\cmidrules{1,3-4}}
+\def\midruleXstep{3}
+\def\midruleXrule{\cmidrules{1,3-4r}}
\begin{tabular}{rclc@{\midruleX}}
% ^^^ inject midrule
\toprule
diff --git a/macros/luatex/generic/penlight/penlight.lua b/macros/luatex/generic/penlight/penlight.lua
index bea9316071..4b2436be1b 100644
--- a/macros/luatex/generic/penlight/penlight.lua
+++ b/macros/luatex/generic/penlight/penlight.lua
@@ -21,7 +21,7 @@ do
--
-- This implements some useful things on [LOM](http://matthewwild.co.uk/projects/luaexpat/lom.html) documents, such as returned by `lxp.lom.parse`.
-- In particular, it can convert LOM back into XML text, with optional pretty-printing control.
--- It is s based on stanza.lua from [Prosody](http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua)
+-- It is based on stanza.lua from [Prosody](http://hg.prosody.im/trunk/file/4621c92d2368/util/stanza.lua)
--
-- > d = xml.parse "<nodes><node id='1'>alice</node></nodes>"
-- > = d
@@ -49,493 +49,880 @@ do
-- @module pl.xml
local utils = require 'pl.utils'
-local split = utils.split;
-local t_insert = table.insert;
-local t_concat = table.concat;
-local t_remove = table.remove;
-local s_match = string.match;
-local tostring = tostring;
-local setmetatable = setmetatable;
-local getmetatable = getmetatable;
-local pairs = pairs;
-local ipairs = ipairs;
-local type = type;
-local next = next;
-local print = print;
-local unpack = utils.unpack;
-local s_gsub = string.gsub;
-local s_find = string.find;
-local pcall,require,io = pcall,require,io
+local split = utils.split
+local t_insert = table.insert
+local t_concat = table.concat
+local t_remove = table.remove
+local s_match = string.match
+local tostring = tostring
+local setmetatable = setmetatable
+local getmetatable = getmetatable
+local pairs = pairs
+local ipairs = ipairs
+local type = type
+local next = next
+local print = print
+local unpack = utils.unpack
+local s_gsub = string.gsub
+local s_sub = string.sub
+local s_find = string.find
+local pcall = pcall
+local require = require
+
+
+utils.raise_deprecation {
+ source = "Penlight " .. utils._VERSION,
+ message = "the contents of module 'pl.xml' has been deprecated, please use a more specialized library instead",
+ version_removed = "2.0.0",
+ deprecated_after = "1.11.0",
+ no_trace = true,
+}
+
+
local _M = {}
local Doc = { __type = "doc" };
Doc.__index = Doc;
+
+local function is_text(s) return type(s) == 'string' end
+local function is_tag(d) return type(d) == 'table' and is_text(d.tag) end
+
+
+
--- create a new document node.
--- @param tag the tag name
--- @param attr optional attributes (table of name-value pairs)
+-- @tparam string tag the tag name
+-- @tparam[opt={}] table attr attributes (table of name-value pairs)
+-- @return the Node object
+-- @see xml.elem
+-- @usage
+-- local doc = xml.new("main", { hello = "world", answer = "42" })
+-- print(doc) --> <main hello='world' answer='42'/>
function _M.new(tag, attr)
- local doc = { tag = tag, attr = attr or {}, last_add = {}};
- return setmetatable(doc, Doc);
+ if type(tag) ~= "string" then
+ error("expected 'tag' to be a string value, got: " .. type(tag), 2)
+ end
+ attr = attr or {}
+ if type(attr) ~= "table" then
+ error("expected 'attr' to be a table value, got: " .. type(attr), 2)
+ end
+
+ local doc = { tag = tag, attr = attr, last_add = {}};
+ return setmetatable(doc, Doc);
end
---- parse an XML document. By default, this uses lxp.lom.parse, but
--- falls back to basic_parse, or if use_basic is true
--- @param text_or_file file or string representation
+
+--- parse an XML document. By default, this uses lxp.lom.parse, but
+-- falls back to basic_parse, or if `use_basic` is truthy
+-- @param text_or_filename file or string representation
-- @param is_file whether text_or_file is a file name or not
-- @param use_basic do a basic parse
-- @return a parsed LOM document with the document metatatables set
-- @return nil, error the error can either be a file error or a parse error
-function _M.parse(text_or_file, is_file, use_basic)
- local parser,status,lom
- if use_basic then parser = _M.basic_parse
+function _M.parse(text_or_filename, is_file, use_basic)
+ local parser,status,lom
+ if use_basic then
+ parser = _M.basic_parse
+ else
+ status,lom = pcall(require,'lxp.lom')
+ if not status then
+ parser = _M.basic_parse
else
- status,lom = pcall(require,'lxp.lom')
- if not status then parser = _M.basic_parse else parser = lom.parse end
+ parser = lom.parse
end
- if is_file then
- local f,err = io.open(text_or_file)
- if not f then return nil,err end
- text_or_file = f:read '*a'
- f:close()
+ end
+
+ if is_file then
+ local text_or_filename, err = utils.readfile(text_or_filename)
+ if not text_or_filename then
+ return nil, err
end
- local doc,err = parser(text_or_file)
- if not doc then return nil,err end
- if lom then
- _M.walk(doc,false,function(_,d)
- setmetatable(d,Doc)
- end)
+ end
+
+ local doc, err = parser(text_or_filename)
+ if not doc then
+ return nil, err
+ end
+
+ if lom then
+ _M.walk(doc, false, function(_, d)
+ setmetatable(d, Doc)
+ end)
+ end
+ return doc
+end
+
+
+--- Create a Node with a set of children (text or Nodes) and attributes.
+-- @tparam string tag a tag name
+-- @tparam table|string items either a single child (text or Node), or a table where the hash
+-- part is the attributes and the list part is the children (text or Nodes).
+-- @return the new Node
+-- @see xml.new
+-- @see xml.tags
+-- @usage
+-- local doc = xml.elem("top", "hello world") -- <top>hello world</top>
+-- local doc = xml.elem("main", xml.new("child")) -- <main><child/></main>
+-- local doc = xml.elem("main", { "this ", "is ", "nice" }) -- <main>this is nice</main>
+-- local doc = xml.elem("main", { xml.new "this",
+-- xml.new "is",
+-- xml.new "nice" }) -- <main><this/><is/><nice/></main>
+-- local doc = xml.elem("main", { hello = "world" }) -- <main hello='world'/>
+-- local doc = xml.elem("main", {
+-- "prefix",
+-- xml.elem("child", { "this ", "is ", "nice"}),
+-- "postfix",
+-- attrib = "value"
+-- }) -- <main attrib='value'>prefix<child>this is nice</child>postfix</main>"
+function _M.elem(tag, items)
+ local s = _M.new(tag)
+ if is_text(items) then items = {items} end
+ if is_tag(items) then
+ t_insert(s,items)
+ elseif type(items) == 'table' then
+ for k,v in pairs(items) do
+ if is_text(k) then
+ s.attr[k] = v
+ t_insert(s.attr,k)
+ else
+ s[k] = v
+ end
+ end
+ end
+ return s
+end
+
+
+--- given a list of names, return a number of element constructors.
+-- If passing a comma-separated string, then whitespace surrounding the values
+-- will be stripped.
+--
+-- The returned constructor functions are a shortcut to `xml.elem` where you
+-- no longer provide the tag-name, but only the `items` table.
+-- @tparam string|table list a list of names, or a comma-separated string.
+-- @return (multiple) constructor functions; `function(items)`. For the `items`
+-- parameter see `xml.elem`.
+-- @see xml.elem
+-- @usage
+-- local new_parent, new_child = xml.tags 'mom, kid'
+-- doc = new_parent {new_child 'Bob', new_child 'Annie'}
+-- -- <mom><kid>Bob</kid><kid>Annie</kid></mom>
+function _M.tags(list)
+ local ctors = {}
+ if is_text(list) then
+ list = split(list:match("^%s*(.-)%s*$"),'%s*,%s*')
+ end
+ for i,tag in ipairs(list) do
+ local function ctor(items)
+ return _M.elem(tag,items)
end
- return doc
+ ctors[i] = ctor
+ end
+ return unpack(ctors)
end
----- convenient function to add a document node, This updates the last inserted position.
--- @param tag a tag name
--- @param attrs optional set of attributes (name-string pairs)
+
+--- Adds a document Node, at current position.
+-- This updates the last inserted position to the new Node.
+-- @tparam string tag the tag name
+-- @tparam[opt={}] table attrs attributes (table of name-value pairs)
+-- @return the current node (`self`)
+-- @usage
+-- local doc = xml.new("main")
+-- doc:addtag("penlight", { hello = "world"})
+-- doc:addtag("expat") -- added to 'penlight' since position moved
+-- print(doc) --> <main><penlight hello='world'><expat/></penlight></main>
function Doc:addtag(tag, attrs)
- local s = _M.new(tag, attrs);
- (self.last_add[#self.last_add] or self):add_direct_child(s);
- t_insert(self.last_add, s);
- return self;
+ local s = _M.new(tag, attrs)
+ self:add_child(s)
+ t_insert(self.last_add, s)
+ return self
end
---- convenient function to add a text node. This updates the last inserted position.
--- @param text a string
+
+--- Adds a text node, at current position.
+-- @tparam string text a string
+-- @return the current node (`self`)
+-- @usage
+-- local doc = xml.new("main")
+-- doc:text("penlight")
+-- doc:text("expat")
+-- print(doc) --> <main><penlightexpat</main>
function Doc:text(text)
- (self.last_add[#self.last_add] or self):add_direct_child(text);
- return self;
+ self:add_child(text)
+ return self
end
----- go up one level in a document
+
+--- Moves current position up one level.
+-- @return the current node (`self`)
function Doc:up()
- t_remove(self.last_add);
- return self;
+ t_remove(self.last_add)
+ return self
end
+
+--- Resets current position to top level.
+-- Resets to the `self` node.
+-- @return the current node (`self`)
function Doc:reset()
- local last_add = self.last_add;
- for i = 1,#last_add do
- last_add[i] = nil;
- end
- return self;
+ local last_add = self.last_add
+ for i = 1,#last_add do
+ last_add[i] = nil
+ end
+ return self
end
---- append a child to a document directly.
+
+--- Append a child to the currrent Node (ignoring current position).
-- @param child a child node (either text or a document)
+-- @return the current node (`self`)
+-- @usage
+-- local doc = xml.new("main")
+-- doc:add_direct_child("dog")
+-- doc:add_direct_child(xml.new("child"))
+-- doc:add_direct_child("cat")
+-- print(doc) --> <main>dog<child/>cat</main>
function Doc:add_direct_child(child)
- t_insert(self, child);
+ t_insert(self, child)
+ return self
end
---- append a child to a document at the last element added
+
+--- Append a child at the current position (without changing position).
-- @param child a child node (either text or a document)
+-- @return the current node (`self`)
+-- @usage
+-- local doc = xml.new("main")
+-- doc:addtag("one")
+-- doc:add_child(xml.new("item1"))
+-- doc:add_child(xml.new("item2"))
+-- doc:add_child(xml.new("item3"))
+-- print(doc) --> <main><one><item1/><item2/><item3/></one></main>
function Doc:add_child(child)
- (self.last_add[#self.last_add] or self):add_direct_child(child);
- return self;
+ (self.last_add[#self.last_add] or self):add_direct_child(child)
+ return self
end
+
--accessing attributes: useful not to have to expose implementation (attr)
--but also can allow attr to be nil in any future optimizations
---- set attributes of a document node.
--- @param t a table containing attribute/value pairs
-function Doc:set_attribs (t)
- for k,v in pairs(t) do
- self.attr[k] = v
- end
+
+--- Set attributes of a document node.
+-- Will add/overwite values, but will not remove existing ones.
+-- Operates on the Node itself, will not take position into account.
+-- @tparam table t a table containing attribute/value pairs
+-- @return the current node (`self`)
+function Doc:set_attribs(t)
+ -- TODO: keep array part in sync
+ for k,v in pairs(t) do
+ self.attr[k] = v
+ end
+ return self
end
---- set a single attribute of a document node.
+
+--- Set a single attribute of a document node.
+-- Operates on the Node itself, will not take position into account.
-- @param a attribute
--- @param v its value
+-- @param v its value, pass in `nil` to delete the attribute
+-- @return the current node (`self`)
function Doc:set_attrib(a,v)
- self.attr[a] = v
+ -- TODO: keep array part in sync
+ self.attr[a] = v
+ return self
end
---- access the attributes of a document node.
+
+--- Gets the attributes of a document node.
+-- Operates on the Node itself, will not take position into account.
+-- @return table with attributes (attribute/value pairs)
function Doc:get_attribs()
- return self.attr
+ return self.attr
end
-local function is_text(s) return type(s) == 'string' end
---- function to create an element with a given tag name and a set of children.
--- @param tag a tag name
--- @param items either text or a table where the hash part is the attributes and the list part is the children.
-function _M.elem(tag,items)
- local s = _M.new(tag)
- if is_text(items) then items = {items} end
- if _M.is_tag(items) then
- t_insert(s,items)
- elseif type(items) == 'table' then
- for k,v in pairs(items) do
- if is_text(k) then
- s.attr[k] = v
- t_insert(s.attr,k)
- else
- s[k] = v
- end
- end
- end
- return s
-end
---- given a list of names, return a number of element constructors.
--- @param list a list of names, or a comma-separated string.
--- @usage local parent,children = doc.tags 'parent,children' <br>
--- doc = parent {child 'one', child 'two'}
-function _M.tags(list)
- local ctors = {}
- if is_text(list) then list = split(list,'%s*,%s*') end
- for _,tag in ipairs(list) do
- local ctor = function(items) return _M.elem(tag,items) end
- t_insert(ctors,ctor)
- end
- return unpack(ctors)
-end
+local template_cache do
+ local templ_cache = {}
-local templ_cache = {}
-
-local function template_cache (templ)
+ -- @param templ a template, a string being valid xml to be parsed, or a Node object
+ function template_cache(templ)
if is_text(templ) then
- if templ_cache[templ] then
- templ = templ_cache[templ]
- else
- local str,err = templ
- templ,err = _M.parse(str,false,true)
- if not templ then return nil,err end
- templ_cache[str] = templ
+ if templ_cache[templ] then
+ -- cache hit
+ return templ_cache[templ]
+
+ else
+ -- parse and cache
+ local ptempl, err = _M.parse(templ,false,true)
+ if not ptempl then
+ return nil, err
end
- elseif not _M.is_tag(templ) then
- return nil, "template is not a document"
+ templ_cache[templ] = ptempl
+ return ptempl
+ end
end
- return templ
+
+ if is_tag(templ) then
+ return templ
+ end
+
+ return nil, "template is not a document"
+ end
end
-local function is_data(data)
+
+do
+ local function is_data(data)
return #data == 0 or type(data[1]) ~= 'table'
-end
+ end
-local function prepare_data(data)
+
+ local function prepare_data(data)
-- a hack for ensuring that $1 maps to first element of data, etc.
-- Either this or could change the gsub call just below.
for i,v in ipairs(data) do
- data[tostring(i)] = v
+ data[tostring(i)] = v
+ end
+ end
+
+ --- create a substituted copy of a document,
+ -- @param template may be a document or a string representation which will be parsed and cached
+ -- @param data a table of name-value pairs or a list of such tables
+ -- @return an XML document
+ function Doc.subst(template, data)
+ if type(data) ~= 'table' or not next(data) then
+ return nil, "data must be a non-empty table"
end
-end
---- create a substituted copy of a document,
--- @param templ may be a document or a string representation which will be parsed and cached
--- @param data a table of name-value pairs or a list of such tables
--- @return an XML document
-function Doc.subst(templ, data)
- local err
- if type(data) ~= 'table' or not next(data) then return nil, "data must be a non-empty table" end
if is_data(data) then
- prepare_data(data)
+ prepare_data(data)
end
- templ,err = template_cache(templ)
- if err then return nil, err end
+
+ local templ, err = template_cache(template)
+ if err then
+ return nil, err
+ end
+
local function _subst(item)
- return _M.clone(templ,function(s)
- return s:gsub('%$(%w+)',item)
- end)
+ return _M.clone(templ, function(s)
+ return s:gsub('%$(%w+)', item)
+ end)
end
- if is_data(data) then return _subst(data) end
+
+ if is_data(data) then
+ return _subst(data)
+ end
+
local list = {}
- for _,item in ipairs(data) do
- prepare_data(item)
- t_insert(list,_subst(item))
+ for _, item in ipairs(data) do
+ prepare_data(item)
+ t_insert(list, _subst(item))
end
+
if data.tag then
- list = _M.elem(data.tag,list)
+ list = _M.elem(data.tag,list)
end
return list
+ end
end
---- get the first child with a given tag name.
+--- Return the first child with a given tag name (non-recursive).
-- @param tag the tag name
+-- @return the child Node found or `nil` if not found
function Doc:child_with_name(tag)
- for _, child in ipairs(self) do
- if child.tag == tag then return child; end
+ for _, child in ipairs(self) do
+ if child.tag == tag then
+ return child
end
+ end
end
-local _children_with_name
-function _children_with_name(self,tag,list,recurse)
- for _, child in ipairs(self) do if type(child) == 'table' then
- if child.tag == tag then t_insert(list,child) end
- if recurse then _children_with_name(child,tag,list,recurse) end
- end end
-end
---- get all elements in a document that have a given tag.
--- @param tag a tag name
--- @param dont_recurse optionally only return the immediate children with this tag name
--- @return a list of elements
-function Doc:get_elements_with_name(tag,dont_recurse)
+do
+ -- @param self document node to traverse
+ -- @param tag tag-name to look for
+ -- @param list array table to add the matching ones to
+ -- @param recurse if truthy, recursivly search the node
+ local function _children_with_name(self, tag, list, recurse)
+ -- TODO: protect against recursion
+ for _, child in ipairs(self) do
+ if type(child) == 'table' then
+ if child.tag == tag then
+ t_insert(list, child)
+ end
+ if recurse then
+ _children_with_name(child, tag, list, recurse)
+ end
+ end
+ end
+ end
+
+ --- Returns all elements in a document that have a given tag.
+ -- @tparam string tag a tag name
+ -- @tparam[opt=false] boolean dont_recurse optionally only return the immediate children with this tag name
+ -- @return a list of elements found, list will be empty if none was found.
+ function Doc:get_elements_with_name(tag, dont_recurse)
local res = {}
- _children_with_name(self,tag,res,not dont_recurse)
+ _children_with_name(self, tag, res, not dont_recurse)
return res
+ end
end
--- iterate over all children of a document node, including text nodes.
+
+
+--- Iterator over all children of a document node, including text nodes.
+-- This function is not recursive, so returns only direct child nodes.
+-- @return iterator that returns a single Node per iteration.
function Doc:children()
- local i = 0;
- return function (a)
- i = i + 1
- return a[i];
- end, self, i;
+ local i = 0;
+ return function (a)
+ i = i + 1
+ return a[i];
+ end, self, i;
end
--- return the first child element of a node, if it exists.
+
+--- Return the first child element of a node, if it exists.
+-- This will skip text nodes.
+-- @return first child Node or `nil` if there is none.
function Doc:first_childtag()
- if #self == 0 then return end
- for _,t in ipairs(self) do
- if type(t) == 'table' then return t end
+ if #self == 0 then
+ return
+ end
+ for _, t in ipairs(self) do
+ if is_tag(t) then
+ return t
end
+ end
end
+
+--- Iterator that matches tag names, and a namespace (non-recursive).
+-- @tparam[opt=nil] string tag tag names to return. Returns all tags if not provided.
+-- @tparam[opt=nil] string xmlns the namespace value ('xmlns' attribute) to return. If not
+-- provided will match all namespaces.
+-- @return iterator that returns a single Node per iteration.
function Doc:matching_tags(tag, xmlns)
- xmlns = xmlns or self.attr.xmlns;
- local tags = self;
- local start_i, max_i, v = 1, #tags;
- return function ()
- for i=start_i,max_i do
- v = tags[i];
- if (not tag or v.tag == tag)
- and (not xmlns or xmlns == v.attr.xmlns) then
- start_i = i+1;
- return v;
- end
- end
- end, tags, start_i;
+ -- TODO: this doesn't make sense??? namespaces are not "xmnls", as matched below
+ -- but "xmlns:name"... so should be a string-prefix match if anything...
+ xmlns = xmlns or self.attr.xmlns;
+ local tags = self
+ local next_i = 1
+ local max_i = #tags
+ local node
+ return function ()
+ for i = next_i, max_i do
+ node = tags[i];
+ if (not tag or node.tag == tag) and
+ (not xmlns or xmlns == node.attr.xmlns) then
+ next_i = i + 1
+ return node
+ end
+ end
+ end, tags, next_i
end
---- iterate over all child elements of a document node.
+
+--- Iterator over all child tags of a document node. This will skip over
+-- text nodes.
+-- @return iterator that returns a single Node per iteration.
function Doc:childtags()
- local i = 0;
- return function (a)
- local v
- repeat
- i = i + 1
- v = self[i]
- if v and type(v) == 'table' then return v; end
- until not v
- end, self[1], i;
+ local i = 0;
+ return function (a)
+ local v
+ repeat
+ i = i + 1
+ v = self[i]
+ if v and type(v) == 'table' then
+ return v
+ end
+ until not v
+ end, self[1], i;
end
---- visit child element of a node and call a function, possibility modifying the document.
--- @param callback a function passed the node (text or element). If it returns nil, that node will be removed.
--- If it returns a value, that will replace the current node.
+
+--- Visit child Nodes of a node and call a function, possibly modifying the document.
+-- Text elements will be skipped.
+-- This is not recursive, so only direct children will be passed.
+-- @tparam function callback a function with signature `function(node)`, passed the node.
+-- The element will be updated with the returned value, or deleted if it returns `nil`.
function Doc:maptags(callback)
- local is_tag = _M.is_tag
- local i = 1;
- while i <= #self do
- if is_tag(self[i]) then
- local ret = callback(self[i]);
- if ret == nil then
- t_remove(self, i);
- else
- self[i] = ret;
- i = i + 1;
- end
- end
+ local i = 1;
+
+ while i <= #self do
+ if is_tag(self[i]) then
+ local ret = callback(self[i]);
+ if ret == nil then
+ -- remove it
+ t_remove(self, i);
+
+ else
+ -- update it
+ self[i] = ret;
+ i = i + 1;
+ end
+ else
+ i = i + 1
end
- return self;
+ end
+
+ return self;
end
-local xml_escape
+
do
- local escape_table = { ["'"] = "&apos;", ["\""] = "&quot;", ["<"] = "&lt;", [">"] = "&gt;", ["&"] = "&amp;" };
- function xml_escape(str) return (s_gsub(str, "['&<>\"]", escape_table)); end
- _M.xml_escape = xml_escape;
+ local escape_table = {
+ ["'"] = "&apos;",
+ ['"'] = "&quot;",
+ ["<"] = "&lt;",
+ [">"] = "&gt;",
+ ["&"] = "&amp;",
+ }
+
+ --- Escapes a string for safe use in xml.
+ -- Handles quotes(single+double), less-than, greater-than, and ampersand.
+ -- @tparam string str string value to escape
+ -- @return escaped string
+ -- @usage
+ -- local esc = xml.xml_escape([["'<>&]]) --> "&quot;&apos;&lt;&gt;&amp;"
+ function _M.xml_escape(str)
+ return (s_gsub(str, "['&<>\"]", escape_table))
+ end
+end
+local xml_escape = _M.xml_escape
+
+do
+ local escape_table = {
+ quot = '"',
+ apos = "'",
+ lt = "<",
+ gt = ">",
+ amp = "&",
+ }
+
+ --- Unescapes a string from xml.
+ -- Handles quotes(single+double), less-than, greater-than, and ampersand.
+ -- @tparam string str string value to unescape
+ -- @return unescaped string
+ -- @usage
+ -- local unesc = xml.xml_escape("&quot;&apos;&lt;&gt;&amp;") --> [["'<>&]]
+ function _M.xml_unescape(str)
+ return (str:gsub( "&(%a+);", escape_table))
+ end
end
+local xml_unescape = _M.xml_unescape
-- pretty printing
-- if indent, then put each new tag on its own line
-- if attr_indent, put each new attribute on its own line
-local function _dostring(t, buf, self, xml_escape, parentns, idn, indent, attr_indent)
- local nsid = 0;
- local tag = t.tag
- local lf,alf = ""," "
- if indent then lf = '\n'..idn end
- if attr_indent then alf = '\n'..idn..attr_indent end
- t_insert(buf, lf.."<"..tag);
- local function write_attr(k,v)
- if s_find(k, "\1", 1, true) then
- local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$");
- nsid = nsid + 1;
- t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'");
- elseif not(k == "xmlns" and v == parentns) then
- t_insert(buf, alf..k.."='"..xml_escape(v).."'");
- end
- end
- -- it's useful for testing to have predictable attribute ordering, if available
- if #t.attr > 0 then
- for _,k in ipairs(t.attr) do
- write_attr(k,t.attr[k])
- end
- else
- for k, v in pairs(t.attr) do
- write_attr(k,v)
- end
+local function _dostring(t, buf, parentns, block_indent, tag_indent, attr_indent)
+ local nsid = 0
+ local tag = t.tag
+
+ local lf = ""
+ if tag_indent then
+ lf = '\n'..block_indent
+ end
+
+ local alf = " "
+ if attr_indent then
+ alf = '\n'..block_indent..attr_indent
+ end
+
+ t_insert(buf, lf.."<"..tag)
+
+ local function write_attr(k,v)
+ if s_find(k, "\1", 1, true) then
+ nsid = nsid + 1
+ local ns, attrk = s_match(k, "^([^\1]*)\1?(.*)$")
+ t_insert(buf, " xmlns:ns"..nsid.."='"..xml_escape(ns).."' ".."ns"..nsid..":"..attrk.."='"..xml_escape(v).."'")
+
+ elseif not (k == "xmlns" and v == parentns) then
+ t_insert(buf, alf..k.."='"..xml_escape(v).."'");
end
- local len,has_children = #t;
- if len == 0 then
- local out = "/>"
- if attr_indent then out = '\n'..idn..out end
- t_insert(buf, out);
- else
- t_insert(buf, ">");
- for n=1,len do
- local child = t[n];
- if child.tag then
- self(child, buf, self, xml_escape, t.attr.xmlns,idn and idn..indent, indent, attr_indent );
- has_children = true
- else -- text element
- t_insert(buf, xml_escape(child));
- end
- end
- t_insert(buf, (has_children and lf or '').."</"..tag..">");
+ end
+
+ -- it's useful for testing to have predictable attribute ordering, if available
+ if #t.attr > 0 then
+ -- TODO: the key-value list is leading, what if they are not in-sync
+ for _,k in ipairs(t.attr) do
+ write_attr(k,t.attr[k])
+ end
+ else
+ for k, v in pairs(t.attr) do
+ write_attr(k,v)
+ end
+ end
+
+ local len = #t
+ local has_children
+
+ if len == 0 then
+ t_insert(buf, attr_indent and '\n'..block_indent.."/>" or "/>")
+
+ else
+ t_insert(buf, ">");
+
+ for n = 1, len do
+ local child = t[n]
+
+ if child.tag then
+ has_children = true
+ _dostring(child, buf, t.attr.xmlns, block_indent and block_indent..tag_indent, tag_indent, attr_indent)
+
+ else
+ -- text element
+ t_insert(buf, xml_escape(child))
+ end
end
+
+ t_insert(buf, (has_children and lf or '').."</"..tag..">");
+ end
end
----- pretty-print an XML document
---- @param t an XML document
---- @param idn an initial indent (indents are all strings)
---- @param indent an indent for each level
---- @param attr_indent if given, indent each attribute pair and put on a separate line
---- @param xml force prefacing with default or custom <?xml...>
---- @return a string representation
-function _M.tostring(t,idn,indent, attr_indent, xml)
- local buf = {};
- if xml then
- if type(xml) == "string" then
- buf[1] = xml
- else
- buf[1] = "<?xml version='1.0'?>"
- end
+--- Function to pretty-print an XML document.
+-- @param doc an XML document
+-- @tparam[opt] string|int b_ind an initial block-indent (required when `t_ind` is set)
+-- @tparam[opt] string|int t_ind an tag-indent for each level (required when `a_ind` is set)
+-- @tparam[opt] string|int a_ind if given, indent each attribute pair and put on a separate line
+-- @tparam[opt] string|bool xml_preface force prefacing with default or custom <?xml...>, if truthy then `&lt;?xml version='1.0'?&gt;` will be used as default.
+-- @return a string representation
+-- @see Doc:tostring
+function _M.tostring(doc, b_ind, t_ind, a_ind, xml_preface)
+ local buf = {}
+
+ if type(b_ind) == "number" then b_ind = (" "):rep(b_ind) end
+ if type(t_ind) == "number" then t_ind = (" "):rep(t_ind) end
+ if type(a_ind) == "number" then a_ind = (" "):rep(a_ind) end
+
+ if xml_preface then
+ if type(xml_preface) == "string" then
+ buf[1] = xml_preface
+ else
+ buf[1] = "<?xml version='1.0'?>"
end
- _dostring(t, buf, _dostring, xml_escape, nil,idn,indent, attr_indent);
- return t_concat(buf);
+ end
+
+ _dostring(doc, buf, nil, b_ind, t_ind, a_ind, xml_preface)
+
+ return t_concat(buf)
end
+
Doc.__tostring = _M.tostring
---- get the full text value of an element
+
+--- Method to pretty-print an XML document.
+-- Invokes `xml.tostring`.
+-- @tparam[opt] string|int b_ind an initial indent (required when `t_ind` is set)
+-- @tparam[opt] string|int t_ind an indent for each level (required when `a_ind` is set)
+-- @tparam[opt] string|int a_ind if given, indent each attribute pair and put on a separate line
+-- @tparam[opt="&lt;?xml version='1.0'?&gt;"] string xml_preface force prefacing with default or custom <?xml...>
+-- @return a string representation
+-- @see xml.tostring
+function Doc:tostring(b_ind, t_ind, a_ind, xml_preface)
+ return _M.tostring(self, b_ind, t_ind, a_ind, xml_preface)
+end
+
+
+--- get the full text value of an element.
+-- @return a single string with all text elements concatenated
+-- @usage
+-- local doc = xml.new("main")
+-- doc:text("one")
+-- doc:add_child(xml.elem "two")
+-- doc:text("three")
+--
+-- local t = doc:get_text() --> "onethree"
function Doc:get_text()
- local res = {}
- for i,el in ipairs(self) do
- if is_text(el) then t_insert(res,el) end
- end
- return t_concat(res);
+ local res = {}
+ for i,el in ipairs(self) do
+ if is_text(el) then t_insert(res,el) end
+ end
+ return t_concat(res);
end
---- make a copy of a document
--- @param doc the original document
--- @param strsubst an optional function for handling string copying which could do substitution, etc.
-function _M.clone(doc, strsubst)
- local lookup_table = {};
- local function _copy(object,kind,parent)
- if type(object) ~= "table" then
- if strsubst and is_text(object) then return strsubst(object,kind,parent)
- else return object
- end
- elseif lookup_table[object] then
- return lookup_table[object]
- end
- local new_table = {};
- lookup_table[object] = new_table
- local tag = object.tag
- new_table.tag = _copy(tag,'*TAG',parent)
- if object.attr then
- local res = {}
- for attr,value in pairs(object.attr) do
- res[attr] = _copy(value,attr,object)
- end
- new_table.attr = res
- end
- for index = 1,#object do
- local v = _copy(object[index],'*TEXT',object)
- t_insert(new_table,v)
+
+do
+ local function _copy(object, kind, parent, strsubst, lookup_table)
+ if type(object) ~= "table" then
+ if strsubst and is_text(object) then
+ return strsubst(object, kind, parent)
+ else
+ return object
+ end
+ end
+
+ if lookup_table[object] then
+ error("recursion detected")
+ end
+ lookup_table[object] = true
+
+ local new_table = {}
+ lookup_table[object] = new_table
+
+ local tag = object.tag
+ new_table.tag = _copy(tag, '*TAG', parent, strsubst, lookup_table)
+
+ if object.attr then
+ local res = {}
+ for attr, value in pairs(object.attr) do
+ if type(attr) == "string" then
+ res[attr] = _copy(value, attr, object, strsubst, lookup_table)
end
- return setmetatable(new_table, getmetatable(object))
+ end
+ new_table.attr = res
end
- return _copy(doc)
+ for index = 1, #object do
+ local v = _copy(object[index], '*TEXT', object, strsubst, lookup_table)
+ t_insert(new_table,v)
+ end
+
+ return setmetatable(new_table, getmetatable(object))
+ end
+
+ --- Returns a copy of a document.
+ -- The `strsubst` parameter is a callback with signature `function(object, kind, parent)`.
+ --
+ -- Param `kind` has the following values, and parameters:
+ --
+ -- - `"*TAG"`: `object` is the tag-name, `parent` is the Node object. Returns the new tag name.
+ --
+ -- - `"*TEXT"`: `object` is the text-element, `parent` is the Node object. Returns the new text value.
+ --
+ -- - other strings not prefixed with `*`: `kind` is the attribute name, `object` is the
+ -- attribute value, `parent` is the Node object. Returns the new attribute value.
+ --
+ -- @tparam Node|string doc a Node object or string (text node)
+ -- @tparam[opt] function strsubst an optional function for handling string copying
+ -- which could do substitution, etc.
+ -- @return copy of the document
+ -- @see Doc:filter
+ function _M.clone(doc, strsubst)
+ return _copy(doc, nil, nil, strsubst, {})
+ end
end
+
+--- Returns a copy of a document.
+-- This is the method version of `xml.clone`.
+-- @see xml.clone
+-- @name Doc:filter
+-- @tparam[opt] function strsubst an optional function for handling string copying
Doc.filter = _M.clone -- also available as method
---- compare two documents.
--- @param t1 any value
--- @param t2 any value
-function _M.compare(t1,t2)
+do
+ local function _compare(t1, t2, recurse_check)
+
local ty1 = type(t1)
local ty2 = type(t2)
- if ty1 ~= ty2 then return false, 'type mismatch' end
+
+ if ty1 ~= ty2 then
+ return false, 'type mismatch'
+ end
+
if ty1 == 'string' then
- return t1 == t2 and true or 'text '..t1..' ~= text '..t2
+ if t1 == t2 then
+ return true
+ else
+ return false, 'text '..t1..' ~= text '..t2
+ end
+ end
+
+ if ty1 ~= 'table' or ty2 ~= 'table' then
+ return false, 'not a document'
+ end
+
+ if recurse_check[t1] then
+ return false, "recursive document"
+ end
+ recurse_check[t1] = true
+
+ if t1.tag ~= t2.tag then
+ return false, 'tag '..t1.tag..' ~= tag '..t2.tag
+ end
+
+ if #t1 ~= #t2 then
+ return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag
end
- if ty1 ~= 'table' or ty2 ~= 'table' then return false, 'not a document' end
- if t1.tag ~= t2.tag then return false, 'tag '..t1.tag..' ~= tag '..t2.tag end
- if #t1 ~= #t2 then return false, 'size '..#t1..' ~= size '..#t2..' for tag '..t1.tag end
+
-- compare attributes
for k,v in pairs(t1.attr) do
- if t2.attr[k] ~= v then return false, 'mismatch attrib' end
+ local t2_value = t2.attr[k]
+ if type(k) == "string" then
+ if t2_value ~= v then return false, 'mismatch attrib' end
+ else
+ if t2_value ~= nil and t2_value ~= v then return false, "mismatch attrib order" end
+ end
end
for k,v in pairs(t2.attr) do
- if t1.attr[k] ~= v then return false, 'mismatch attrib' end
+ local t1_value = t1.attr[k]
+ if type(k) == "string" then
+ if t1_value ~= v then return false, 'mismatch attrib' end
+ else
+ if t1_value ~= nil and t1_value ~= v then return false, "mismatch attrib order" end
+ end
end
+
-- compare children
- for i = 1,#t1 do
- local yes,err = _M.compare(t1[i],t2[i])
- if not yes then return err end
+ for i = 1, #t1 do
+ local ok, err = _compare(t1[i], t2[i], recurse_check)
+ if not ok then
+ return ok, err
+ end
end
return true
+ end
+
+ --- Compare two documents or elements.
+ -- Equality is based on tag, child nodes (text and tags), attributes and order
+ -- of those (order only fails if both are given, and not equal).
+ -- @tparam Node|string t1 a Node object or string (text node)
+ -- @tparam Node|string t2 a Node object or string (text node)
+ -- @treturn boolean `true` when the Nodes are equal.
+ function _M.compare(t1,t2)
+ return _compare(t1, t2, {})
+ end
end
+
--- is this value a document element?
-- @param d any value
-function _M.is_tag(d)
- return type(d) == 'table' and is_text(d.tag)
-end
+-- @treturn boolean `true` if it is a `table` with property `tag` being a string value.
+-- @name is_tag
+_M.is_tag = is_tag
+
---- call the desired function recursively over the document.
--- @param doc the document
--- @param depth_first visit child notes first, then the current node
--- @param operation a function which will receive the current tag name and current node.
-function _M.walk (doc, depth_first, operation)
- if not depth_first then operation(doc.tag,doc) end
+do
+ local function _walk(doc, depth_first, operation, recurse_check)
+ if not depth_first then operation(doc.tag, doc) end
for _,d in ipairs(doc) do
- if _M.is_tag(d) then
- _M.walk(d,depth_first,operation)
- end
+ if is_tag(d) then
+ assert(not recurse_check[d], "recursion detected")
+ recurse_check[d] = true
+ _walk(d, depth_first, operation, recurse_check)
+ end
end
- if depth_first then operation(doc.tag,doc) end
+ if depth_first then operation(doc.tag, doc) end
+ end
+
+ --- Calls a function recursively over Nodes in the document.
+ -- Will only call on tags, it will skip text nodes.
+ -- The function signature for `operation` is `function(tag_name, Node)`.
+ -- @tparam Node|string doc a Node object or string (text node)
+ -- @tparam boolean depth_first visit child nodes first, then the current node
+ -- @tparam function operation a function which will receive the current tag name and current node.
+ function _M.walk(doc, depth_first, operation)
+ return _walk(doc, depth_first, operation, {})
+ end
end
+
local html_empty_elements = { --lists all HTML empty (void) elements
br = true,
img = true,
@@ -553,13 +940,10 @@ local html_empty_elements = { --lists all HTML empty (void) elements
embed = true,
}
-local escapes = { quot = "\"", apos = "'", lt = "<", gt = ">", amp = "&" }
-local function unescape(str) return (str:gsub( "&(%a+);", escapes)); end
-
--- Parse a well-formed HTML file as a string.
-- Tags are case-insenstive, DOCTYPE is ignored, and empty elements can be .. empty.
-- @param s the HTML
-function _M.parsehtml (s)
+function _M.parsehtml(s)
return _M.basic_parse(s,false,true)
end
@@ -567,9 +951,7 @@ end
-- @param s the XML document to be parsed.
-- @param all_text if true, preserves all whitespace. Otherwise only text containing non-whitespace is included.
-- @param html if true, uses relaxed HTML rules for parsing
-function _M.basic_parse(s,all_text,html)
- local t_insert,t_remove = table.insert,table.remove
- local s_find,s_sub = string.find,string.sub
+function _M.basic_parse(s, all_text, html)
local stack = {}
local top = {}
@@ -577,12 +959,12 @@ function _M.basic_parse(s,all_text,html)
local arg = {}
s:gsub("([%w:%-_]+)%s*=%s*([\"'])(.-)%2", function (w, _, a)
if html then w = w:lower() end
- arg[w] = unescape(a)
+ arg[w] = xml_unescape(a)
end)
if html then
s:gsub("([%w:%-_]+)%s*=%s*([^\"']+)%s*", function (w, a)
w = w:lower()
- arg[w] = unescape(a)
+ arg[w] = xml_unescape(a)
end)
end
return arg
@@ -617,7 +999,7 @@ function _M.basic_parse(s,all_text,html)
if html_empty_elements[label] then empty = "/" end
end
if all_text or not s_find(text, "^%s*$") then
- t_insert(top, unescape(text))
+ t_insert(top, xml_unescape(text))
end
if empty == "/" then -- empty element tag
t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc))
@@ -640,7 +1022,7 @@ function _M.basic_parse(s,all_text,html)
end
local text = s_sub(s, i)
if all_text or not s_find(text, "^%s*$") then
- t_insert(stack[#stack], unescape(text))
+ t_insert(stack[#stack], xml_unescape(text))
end
if #stack > 1 then
error("unclosed "..stack[#stack].tag)
@@ -649,145 +1031,151 @@ function _M.basic_parse(s,all_text,html)
return is_text(res[1]) and res[2] or res[1]
end
-local function empty(attr) return not attr or not next(attr) end
-local function is_element(d) return type(d) == 'table' and d.tag ~= nil end
+do
+ local match do
--- returns the key,value pair from a table if it has exactly one entry
-local function has_one_element(t)
- local key,value = next(t)
- if next(t,key) ~= nil then return false end
- return key,value
-end
+ local function empty(attr) return not attr or not next(attr) end
-local function append_capture(res,tbl)
- if not empty(tbl) then -- no point in capturing empty tables...
- local key
- if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table
- key = tbl._
- tbl._ = nil
- if empty(tbl) then return end
- end
- -- a table with only one pair {[0]=value} shall be reduced to that value
- local numkey,val = has_one_element(tbl)
- if numkey == 0 then tbl = val end
- if key then
- res[key] = tbl
- else -- otherwise, we append the captured table
- t_insert(res,tbl)
- end
+ local append_capture do
+ -- returns the key,value pair from a table if it has exactly one entry
+ local function has_one_element(t)
+ local key,value = next(t)
+ if next(t,key) ~= nil then return false end
+ return key,value
+ end
+
+ function append_capture(res,tbl)
+ if not empty(tbl) then -- no point in capturing empty tables...
+ local key
+ if tbl._ then -- if $_ was set then it is meant as the top-level key for the captured table
+ key = tbl._
+ tbl._ = nil
+ if empty(tbl) then return end
+ end
+ -- a table with only one pair {[0]=value} shall be reduced to that value
+ local numkey,val = has_one_element(tbl)
+ if numkey == 0 then tbl = val end
+ if key then
+ res[key] = tbl
+ else -- otherwise, we append the captured table
+ t_insert(res,tbl)
+ end
+ end
+ end
end
-end
-local function make_number(pat)
- if pat:find '^%d+$' then -- $1 etc means use this as an array location
- pat = tonumber(pat)
+ local function make_number(pat)
+ if pat:find '^%d+$' then -- $1 etc means use this as an array location
+ pat = tonumber(pat)
+ end
+ return pat
end
- return pat
-end
-local function capture_attrib(res,pat,value)
- pat = make_number(pat:sub(2))
- res[pat] = value
- return true
-end
+ local function capture_attrib(res,pat,value)
+ pat = make_number(pat:sub(2))
+ res[pat] = value
+ return true
+ end
-local match
-function match(d,pat,res,keep_going)
- local ret = true
- if d == nil then d = '' end --return false end
- -- attribute string matching is straight equality, except if the pattern is a $ capture,
- -- which always succeeds.
- if is_text(d) then
- if not is_text(pat) then return false end
- if _M.debug then print(d,pat) end
- if pat:find '^%$' then
- return capture_attrib(res,pat,d)
+ function match(d,pat,res,keep_going)
+ local ret = true
+ if d == nil then d = '' end --return false end
+ -- attribute string matching is straight equality, except if the pattern is a $ capture,
+ -- which always succeeds.
+ if is_text(d) then
+ if not is_text(pat) then return false end
+ if _M.debug then print(d,pat) end
+ if pat:find '^%$' then
+ return capture_attrib(res,pat,d)
+ else
+ return d == pat
+ end
else
- return d == pat
- end
- else
- if _M.debug then print(d.tag,pat.tag) end
- -- this is an element node. For a match to succeed, the attributes must
- -- match as well.
- -- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute
- local tagpat = pat.tag:match '^(.-)%-$'
- if tagpat then
- tagpat = make_number(tagpat)
- res[tagpat] = d.tag
- end
- if d.tag == pat.tag or tagpat then
-
- if not empty(pat.attr) then
- if empty(d.attr) then ret = false
- else
- for prop,pval in pairs(pat.attr) do
- local dval = d.attr[prop]
- if not match(dval,pval,res) then ret = false; break end
- end
- end
+ if _M.debug then print(d.tag,pat.tag) end
+ -- this is an element node. For a match to succeed, the attributes must
+ -- match as well.
+ -- a tagname in the pattern ending with '-' is a wildcard and matches like an attribute
+ local tagpat = pat.tag:match '^(.-)%-$'
+ if tagpat then
+ tagpat = make_number(tagpat)
+ res[tagpat] = d.tag
end
- -- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..}
- if ret and #pat > 0 then
- local i,j = 1,1
- local function next_elem()
- j = j + 1 -- next child element of data
- if is_text(d[j]) then j = j + 1 end
- return j <= #d
- end
- repeat
- local p = pat[i]
- -- repeated {{<...>}} patterns shall match one or more elements
- -- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X}
- if is_element(p) and p.repeated then
- local found
- repeat
- local tbl = {}
- ret = match(d[j],p,tbl,false)
- if ret then
- found = false --true
- append_capture(res,tbl)
- end
- until not next_elem() or (found and not ret)
- i = i + 1
+ if d.tag == pat.tag or tagpat then
+
+ if not empty(pat.attr) then
+ if empty(d.attr) then ret = false
else
- ret = match(d[j],p,res,false)
- if ret then i = i + 1 end
+ for prop,pval in pairs(pat.attr) do
+ local dval = d.attr[prop]
+ if not match(dval,pval,res) then ret = false; break end
+ end
end
- until not next_elem() or i > #pat -- run out of elements or patterns to match
- -- if every element in our pattern matched ok, then it's been a successful match
- if i > #pat then return true end
+ end
+ -- the pattern may have child nodes. We match partially, so that {P1,P2} shall match {X,P1,X,X,P2,..}
+ if ret and #pat > 0 then
+ local i,j = 1,1
+ local function next_elem()
+ j = j + 1 -- next child element of data
+ if is_text(d[j]) then j = j + 1 end
+ return j <= #d
+ end
+ repeat
+ local p = pat[i]
+ -- repeated {{<...>}} patterns shall match one or more elements
+ -- so e.g. {P+} will match {X,X,P,P,X,P,X,X,X}
+ if is_tag(p) and p.repeated then
+ local found
+ repeat
+ local tbl = {}
+ ret = match(d[j],p,tbl,false)
+ if ret then
+ found = false --true
+ append_capture(res,tbl)
+ end
+ until not next_elem() or (found and not ret)
+ i = i + 1
+ else
+ ret = match(d[j],p,res,false)
+ if ret then i = i + 1 end
+ end
+ until not next_elem() or i > #pat -- run out of elements or patterns to match
+ -- if every element in our pattern matched ok, then it's been a successful match
+ if i > #pat then return true end
+ end
+ if ret then return true end
+ else
+ ret = false
end
- if ret then return true end
- else
- ret = false
- end
- -- keep going anyway - look at the children!
- if keep_going then
- for child in d:childtags() do
- ret = match(child,pat,res,keep_going)
- if ret then break end
+ -- keep going anyway - look at the children!
+ if keep_going then
+ for child in d:childtags() do
+ ret = match(child,pat,res,keep_going)
+ if ret then break end
+ end
end
end
+ return ret
end
- return ret
-end
+ end
-function Doc:match(pat)
- local err
- pat,err = template_cache(pat)
- if not pat then return nil, err end
- _M.walk(pat,false,function(_,d)
- if is_text(d[1]) and is_element(d[2]) and is_text(d[3]) and
- d[1]:find '%s*{{' and d[3]:find '}}%s*' then
- t_remove(d,1)
- t_remove(d,2)
- d[1].repeated = true
- end
- end)
+ --- does something...
+ function Doc:match(pat)
+ local err
+ pat,err = template_cache(pat)
+ if not pat then return nil, err end
+ _M.walk(pat,false,function(_,d)
+ if is_text(d[1]) and is_tag(d[2]) and is_text(d[3]) and
+ d[1]:find '%s*{{' and d[3]:find '}}%s*' then
+ t_remove(d,1)
+ t_remove(d,2)
+ d[1].repeated = true
+ end
+ end)
- local res = {}
- local ret = match(self,pat,res,true)
- return res,ret
+ local res = {}
+ local ret = match(self,pat,res,true)
+ return res,ret
+ end
end
@@ -1416,14 +1804,14 @@ do
error('bad cell specifier: '..s)
end
- --- parse a spreadsheet range.
- -- The range can be specified either as 'A1:B2' or 'R1C1:R2C2';
- -- a special case is a single element (e.g 'A1' or 'R1C1')
+ --- parse a spreadsheet range or cell.
+ -- The range/cell can be specified either as 'A1:B2' or 'R1C1:R2C2' or for
+ -- single cells as 'A1' or 'R1C1'.
-- @string s a range (case insensitive).
-- @treturn int start row
-- @treturn int start col
- -- @treturn int end row
- -- @treturn int end col
+ -- @treturn int end row (or `nil` if the range was a single cell)
+ -- @treturn int end col (or `nil` if the range was a single cell)
function array2d.parse_range (s)
assert_arg(1,s,'string')
s = s:upper()
@@ -1439,15 +1827,11 @@ do
end
end
---- get a slice of a 2D array using spreadsheet range notation. @see parse_range
--- @array2d t a 2D array
--- @string rstr range expression
--- @return a slice
--- @see array2d.parse_range
--- @see array2d.slice
-function array2d.range (t,rstr)
- assert_arg(1,t,'table')
- return array2d.slice(t,array2d.parse_range(rstr))
+--- get a slice of a 2D array.
+-- Same as `slice`.
+-- @see slice
+function array2d.range (...)
+ return array2d.slice(...)
end
local default_range do
@@ -1465,12 +1849,16 @@ local default_range do
-- Negative indices will be counted from the end, too low, or too high
-- will be limited by the array sizes.
-- @array2d t a 2D array
- -- @int i1 start row (default 1)
- -- @int j1 start col (default 1)
- -- @int i2 end row (default N)
- -- @int j2 end col (default M)
- -- return i1, j1, i2, j2
+ -- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+ -- @tparam[opt=1] int j1 start col
+ -- @tparam[opt=N] int i2 end row
+ -- @tparam[opt=M] int j2 end col
+ -- @see parse_range
+ -- @return i1, j1, i2, j2
function array2d.default_range (t,i1,j1,i2,j2)
+ if (type(i1) == 'string') and not (j1 or i2 or j2) then
+ i1, j1, i2, j2 = array2d.parse_range(i1)
+ end
local nr, nc = array2d.size(t)
i1 = norm_value(i1 or 1, nr)
j1 = norm_value(j1 or 1, nc)
@@ -1484,10 +1872,11 @@ end
--- get a slice of a 2D array. Note that if the specified range has
-- a 1D result, the rank of the result will be 1.
-- @array2d t a 2D array
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
-- @return an array, 2D in general but 1D in special cases.
function array2d.slice (t,i1,j1,i2,j2)
assert_arg(1,t,'table')
@@ -1513,10 +1902,11 @@ end
--- set a specified range of an array to a value.
-- @array2d t a 2D array
-- @param value the value (may be a function, called as `val(i,j)`)
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
-- @see tablex.set
function array2d.set (t,value,i1,j1,i2,j2)
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
@@ -1537,10 +1927,11 @@ end
-- @array2d t a 2D array
-- @param f a file object (default stdout)
-- @string fmt a format string (default is just to use tostring)
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
function array2d.write (t,f,fmt,i1,j1,i2,j2)
assert_arg(1,t,'table')
f = f or stdout
@@ -1560,10 +1951,11 @@ end
-- @array2d t 2D array
-- @func row_op function to call on each value; `row_op(row,j)`
-- @func end_row_op function to call at end of each row; `end_row_op(i)`
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
function array2d.forall (t,row_op,end_row_op,i1,j1,i2,j2)
assert_arg(1,t,'table')
i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
@@ -1581,10 +1973,11 @@ end
-- @int di start row in dest
-- @int dj start col in dest
-- @array2d src a 2D array
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
function array2d.move (dest,di,dj,src,i1,j1,i2,j2)
assert_arg(1,dest,'table')
assert_arg(4,src,'table')
@@ -1604,10 +1997,11 @@ end
--- iterate over all elements in a 2D array, with optional indices.
-- @array2d a 2D array
-- @bool indices with indices (default false)
--- @int i1 start row (default 1)
--- @int j1 start col (default 1)
--- @int i2 end row (default N)
--- @int j2 end col (default M)
+-- @tparam[opt=1] int|string i1 start row or spreadsheet range passed to `parse_range`
+-- @tparam[opt=1] int j1 start col
+-- @tparam[opt=N] int i2 end row
+-- @tparam[opt=M] int j2 end col
+-- @see parse_range
-- @return either `value` or `i,j,value` depending on the value of `indices`
function array2d.iter(a,indices,i1,j1,i2,j2)
assert_arg(1,a,'table')
@@ -1647,7 +2041,7 @@ function array2d.columns(a)
end
--- iterate over all rows.
--- Returns a copy of the row, for read-only purrposes directly iterating
+-- Returns a copy of the row, for read-only purposes directly iterating
-- is more performant; `ipairs(a)`
-- @array2d a a 2D array
-- @return row, row-index
@@ -2129,7 +2523,7 @@ end
-- See `utils.unpack` for a version that is nil-safe.
-- @param t table to unpack
-- @param[opt] i index from which to start unpacking, defaults to 1
--- @param[opt] t index of the last element to unpack, defaults to #t
+-- @param[opt] j index of the last element to unpack, defaults to #t
-- @return multiple return values from the table
-- @function table.unpack
-- @see utils.unpack
@@ -2191,7 +2585,7 @@ end
-- @param ... any arguments
if not warn then -- luacheck: ignore
local enabled = false
- function warn(arg1, ...) -- luacheck: ignore
+ local function warn(arg1, ...) -- luacheck: ignore
if type(arg1) == "string" and arg1:sub(1, 1) == "@" then
-- control message
if arg1 == "@on" then
@@ -2209,6 +2603,8 @@ if not warn then -- luacheck: ignore
io.stderr:write("\n")
end
end
+ -- use rawset to bypass OpenResty's protection of global scope
+ rawset(_G, "warn", warn)
end
return compat
@@ -4122,7 +4518,7 @@ end
--- Return a list of all file names within an array which match a pattern.
-- @tab filenames An array containing file names.
--- @string pattern A shell pattern.
+-- @string pattern A shell pattern (see `fnmatch`).
-- @treturn List(string) List of matching file names.
-- @raise dir and mask must be strings
function dir.filter(filenames,pattern)
@@ -4152,11 +4548,12 @@ local function _listfiles(dirname,filemode,match)
end
--- return a list of all files in a directory which match a shell pattern.
--- @string dirname A directory. If not given, all files in current directory are returned.
--- @string mask A shell pattern. If not given, all files are returned.
+-- @string[opt='.'] dirname A directory.
+-- @string[opt] mask A shell pattern (see `fnmatch`). If not given, all files are returned.
-- @treturn {string} list of files
-- @raise dirname and mask must be strings
function dir.getfiles(dirname,mask)
+ dirname = dirname or '.'
assert_dir(1,dirname)
if mask then assert_string(2,mask) end
local match
@@ -4170,10 +4567,11 @@ function dir.getfiles(dirname,mask)
end
--- return a list of all subdirectories of the directory.
--- @string dirname A directory
+-- @string[opt='.'] dirname A directory.
-- @treturn {string} a list of directories
--- @raise dir must be a a valid directory
+-- @raise dir must be a valid directory
function dir.getdirectories(dirname)
+ dirname = dirname or '.'
assert_dir(1,dirname)
return _listfiles(dirname,false)
end
@@ -4567,13 +4965,14 @@ function dir.dirtree( d )
end
---- Recursively returns all the file starting at _path_. It can optionally take a shell pattern and
--- only returns files that match _shell_pattern_. If a pattern is given it will do a case insensitive search.
--- @string start_path A directory. If not given, all files in current directory are returned.
--- @string shell_pattern A shell pattern. If not given, all files are returned.
--- @treturn List(string) containing all the files found recursively starting at _path_ and filtered by _shell_pattern_.
+--- Recursively returns all the file starting at 'path'. It can optionally take a shell pattern and
+-- only returns files that match 'shell_pattern'. If a pattern is given it will do a case insensitive search.
+-- @string[opt='.'] start_path A directory.
+-- @string[opt='*'] shell_pattern A shell pattern (see `fnmatch`).
+-- @treturn List(string) containing all the files found recursively starting at 'path' and filtered by 'shell_pattern'.
-- @raise start_path must be a directory
function dir.getallfiles( start_path, shell_pattern )
+ start_path = start_path or '.'
assert_dir(1,start_path)
shell_pattern = shell_pattern or "*"
@@ -5113,7 +5512,7 @@ return function(env)
if prevenvmt then
_prev_index = prevenvmt.__index
if prevenvmt.__newindex then
- gmt.__index = prevenvmt.__newindex
+ gmt.__newindex = prevenvmt.__newindex
end
end
@@ -5634,7 +6033,7 @@ function lapp.process_options_string(str,args)
local enump = '|' .. enums .. '|'
vtype = 'string'
constraint = function(s)
- lapp.assert(enump:match('|'..s..'|'),
+ lapp.assert(enump:find('|'..s..'|', 1, true),
"value '"..s.."' not in "..enums
)
end
@@ -10428,9 +10827,10 @@ end,
--
-- See @{03-strings.md|the Guide}
--
--- Dependencies: `pl.utils`
+-- Dependencies: `pl.utils`, `pl.types`
-- @module pl.stringx
local utils = require 'pl.utils'
+local is_callable = require 'pl.types'.is_callable
local string = string
local find = string.find
local type,setmetatable,ipairs = type,setmetatable,ipairs
@@ -10441,10 +10841,13 @@ local sub = string.sub
local reverse = string.reverse
local concat = table.concat
local append = table.insert
+local remove = table.remove
local escape = utils.escape
local ceil, max = math.ceil, math.max
local assert_arg,usplit = utils.assert_arg,utils.split
local lstrip
+local unpack = utils.unpack
+local pack = utils.pack
local function assert_string (n,s)
assert_arg(n,s,'string')
@@ -10489,7 +10892,8 @@ function stringx.isalnum(s)
return find(s,'^%w+$') == 1
end
---- does s only contain spaces?
+--- does s only contain whitespace?
+-- Matches on pattern '%s' so matches space, newline, tabs, etc.
-- @string s a string
function stringx.isspace(s)
assert_string(1,s)
@@ -10631,10 +11035,14 @@ end
-- @usage stringx.expandtabs('\tone,two,three', 4) == ' one,two,three'
-- @usage stringx.expandtabs(' \tone,two,three', 4) == ' one,two,three'
function stringx.expandtabs(s,tabsize)
- assert_string(1,s)
- tabsize = tabsize or 8
- return (s:gsub("([^\t\r\n]*)\t", function(before_tab)
+ assert_string(1,s)
+ tabsize = tabsize or 8
+ return (s:gsub("([^\t\r\n]*)\t", function(before_tab)
+ if tabsize == 0 then
+ return before_tab
+ else
return before_tab .. (" "):rep(tabsize - #before_tab % tabsize)
+ end
end))
end
@@ -10808,36 +11216,37 @@ local function _strip(s,left,right,chrs)
return sub(s,f,t)
end
---- trim any whitespace on the left of s.
+--- trim any characters on the left of s.
-- @string s the string
-- @string[opt='%s'] chrs default any whitespace character,
--- but can be a string of characters to be trimmed
+-- but can be a string of characters to be trimmed
function stringx.lstrip(s,chrs)
assert_string(1,s)
return _strip(s,true,false,chrs)
end
lstrip = stringx.lstrip
---- trim any whitespace on the right of s.
+--- trim any characters on the right of s.
-- @string s the string
-- @string[opt='%s'] chrs default any whitespace character,
--- but can be a string of characters to be trimmed
+-- but can be a string of characters to be trimmed
function stringx.rstrip(s,chrs)
assert_string(1,s)
return _strip(s,false,true,chrs)
end
---- trim any whitespace on both left and right of s.
+--- trim any characters on both left and right of s.
-- @string s the string
-- @string[opt='%s'] chrs default any whitespace character,
--- but can be a string of characters to be trimmed
+-- but can be a string of characters to be trimmed
+-- @usage stringx.strip(' --== Hello ==-- ', "- =") --> 'Hello'
function stringx.strip(s,chrs)
assert_string(1,s)
return _strip(s,true,true,chrs)
end
---- Partioning Strings
--- @section partioning
+--- Partitioning Strings
+-- @section partitioning
--- split a string using a pattern. Note that at least one value will be returned!
-- @string s the string
@@ -10864,7 +11273,7 @@ end
--- partition the string using first occurance of a delimiter
-- @string s the string
--- @string ch delimiter
+-- @string ch delimiter (match as plain string, no patterns)
-- @return part before ch
-- @return ch
-- @return part after ch
@@ -10878,7 +11287,7 @@ end
--- partition the string p using last occurance of a delimiter
-- @string s the string
--- @string ch delimiter
+-- @string ch delimiter (match as plain string, no patterns)
-- @return part before ch
-- @return ch
-- @return part after ch
@@ -10904,6 +11313,250 @@ function stringx.at(s,idx)
return sub(s,idx,idx)
end
+
+--- Text handling
+-- @section text
+
+
+--- indent a multiline string.
+-- @tparam string s the (multiline) string
+-- @tparam integer n the size of the indent
+-- @tparam[opt=' '] string ch the character to use when indenting
+-- @return indented string
+function stringx.indent (s,n,ch)
+ assert_arg(1,s,'string')
+ assert_arg(2,n,'number')
+ local lines = usplit(s ,'\n')
+ local prefix = string.rep(ch or ' ',n)
+ for i, line in ipairs(lines) do
+ lines[i] = prefix..line
+ end
+ return concat(lines,'\n')..'\n'
+end
+
+
+--- dedent a multiline string by removing any initial indent.
+-- useful when working with [[..]] strings.
+-- Empty lines are ignored.
+-- @tparam string s the (multiline) string
+-- @return a string with initial indent zero.
+-- @usage
+-- local s = dedent [[
+-- One
+--
+-- Two
+--
+-- Three
+-- ]]
+-- assert(s == [[
+-- One
+--
+-- Two
+--
+-- Three
+-- ]])
+function stringx.dedent (s)
+ assert_arg(1,s,'string')
+ local lst = usplit(s,'\n')
+ if #lst>0 then
+ local ind_size = math.huge
+ for i, line in ipairs(lst) do
+ local i1, i2 = lst[i]:find('^%s*[^%s]')
+ if i1 and i2 < ind_size then
+ ind_size = i2
+ end
+ end
+ for i, line in ipairs(lst) do
+ lst[i] = lst[i]:sub(ind_size, -1)
+ end
+ end
+ return concat(lst,'\n')..'\n'
+end
+
+
+
+do
+ local buildline = function(words, size, breaklong)
+ -- if overflow is set, a word longer than size, will overflow the size
+ -- otherwise it will be chopped in line-length pieces
+ local line = {}
+ if #words[1] > size then
+ -- word longer than line
+ if not breaklong then
+ line[1] = words[1]
+ remove(words, 1)
+ else
+ line[1] = words[1]:sub(1, size)
+ words[1] = words[1]:sub(size + 1, -1)
+ end
+ else
+ local len = 0
+ while words[1] and (len + #words[1] <= size) or
+ (len == 0 and #words[1] == size) do
+ if words[1] ~= "" then
+ line[#line+1] = words[1]
+ len = len + #words[1] + 1
+ end
+ remove(words, 1)
+ end
+ end
+ return stringx.strip(concat(line, " ")), words
+ end
+
+ --- format a paragraph into lines so that they fit into a line width.
+ -- It will not break long words by default, so lines can be over the length
+ -- to that extent.
+ -- @tparam string s the string to format
+ -- @tparam[opt=70] integer width the margin width
+ -- @tparam[opt=false] boolean breaklong if truthy, words longer than the width given will be forced split.
+ -- @return a list of lines (List object), use `fill` to return a string instead of a `List`.
+ -- @see pl.List
+ -- @see fill
+ stringx.wrap = function(s, width, breaklong)
+ s = s:gsub('\n',' ') -- remove line breaks
+ s = stringx.strip(s) -- remove leading/trailing whitespace
+ if s == "" then
+ return { "" }
+ end
+ width = width or 70
+ local out = {}
+ local words = usplit(s, "%s")
+ while words[1] do
+ out[#out+1], words = buildline(words, width, breaklong)
+ end
+ return makelist(out)
+ end
+end
+
+--- format a paragraph so that it fits into a line width.
+-- @tparam string s the string to format
+-- @tparam[opt=70] integer width the margin width
+-- @tparam[opt=false] boolean breaklong if truthy, words longer than the width given will be forced split.
+-- @return a string, use `wrap` to return a list of lines instead of a string.
+-- @see wrap
+function stringx.fill (s,width,breaklong)
+ return concat(stringx.wrap(s,width,breaklong),'\n') .. '\n'
+end
+
+--- Template
+-- @section Template
+
+
+local function _substitute(s,tbl,safe)
+ local subst
+ if is_callable(tbl) then
+ subst = tbl
+ else
+ function subst(f)
+ local s = tbl[f]
+ if not s then
+ if safe then
+ return f
+ else
+ error("not present in table "..f)
+ end
+ else
+ return s
+ end
+ end
+ end
+ local res = gsub(s,'%${([%w_]+)}',subst)
+ return (gsub(res,'%$([%w_]+)',subst))
+end
+
+
+
+local Template = {}
+stringx.Template = Template
+Template.__index = Template
+setmetatable(Template, {
+ __call = function(obj,tmpl)
+ return Template.new(tmpl)
+ end
+})
+
+--- Creates a new Template class.
+-- This is a shortcut to `Template.new(tmpl)`.
+-- @tparam string tmpl the template string
+-- @function Template
+-- @treturn Template
+function Template.new(tmpl)
+ assert_arg(1,tmpl,'string')
+ local res = {}
+ res.tmpl = tmpl
+ setmetatable(res,Template)
+ return res
+end
+
+--- substitute values into a template, throwing an error.
+-- This will throw an error if no name is found.
+-- @tparam table tbl a table of name-value pairs.
+-- @return string with place holders substituted
+function Template:substitute(tbl)
+ assert_arg(1,tbl,'table')
+ return _substitute(self.tmpl,tbl,false)
+end
+
+--- substitute values into a template.
+-- This version just passes unknown names through.
+-- @tparam table tbl a table of name-value pairs.
+-- @return string with place holders substituted
+function Template:safe_substitute(tbl)
+ assert_arg(1,tbl,'table')
+ return _substitute(self.tmpl,tbl,true)
+end
+
+--- substitute values into a template, preserving indentation. <br>
+-- If the value is a multiline string _or_ a template, it will insert
+-- the lines at the correct indentation. <br>
+-- Furthermore, if a template, then that template will be substituted
+-- using the same table.
+-- @tparam table tbl a table of name-value pairs.
+-- @return string with place holders substituted
+function Template:indent_substitute(tbl)
+ assert_arg(1,tbl,'table')
+ if not self.strings then
+ self.strings = usplit(self.tmpl,'\n')
+ end
+
+ -- the idea is to substitute line by line, grabbing any spaces as
+ -- well as the $var. If the value to be substituted contains newlines,
+ -- then we split that into lines and adjust the indent before inserting.
+ local function subst(line)
+ return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
+ local subtmpl
+ local s = tbl[f]
+ if not s then error("not present in table "..f) end
+ if getmetatable(s) == Template then
+ subtmpl = s
+ s = s.tmpl
+ else
+ s = tostring(s)
+ end
+ if s:find '\n' then
+ local lines = usplit(s, '\n')
+ for i, line in ipairs(lines) do
+ lines[i] = sp..line
+ end
+ s = concat(lines, '\n') .. '\n'
+ end
+ if subtmpl then
+ return _substitute(s, tbl)
+ else
+ return s
+ end
+ end)
+ end
+
+ local lines = {}
+ for i, line in ipairs(self.strings) do
+ lines[i] = subst(line)
+ end
+ return concat(lines,'\n')..'\n'
+end
+
+
+
--- Miscelaneous
-- @section misc
@@ -10936,79 +11589,148 @@ end
stringx.capitalize = stringx.title
-local ellipsis = '...'
-local n_ellipsis = #ellipsis
-
---- Return a shortened version of a string.
--- Fits string within w characters. Removed characters are marked with ellipsis.
--- @string s the string
--- @int w the maxinum size allowed
--- @bool tail true if we want to show the end of the string (head otherwise)
--- @usage ('1234567890'):shorten(8) == '12345...'
--- @usage ('1234567890'):shorten(8, true) == '...67890'
--- @usage ('1234567890'):shorten(20) == '1234567890'
-function stringx.shorten(s,w,tail)
- assert_string(1,s)
- if #s > w then
- if w < n_ellipsis then return ellipsis:sub(1,w) end
- if tail then
- local i = #s - w + 1 + n_ellipsis
- return ellipsis .. s:sub(i)
- else
- return s:sub(1,w-n_ellipsis) .. ellipsis
- end
- end
- return s
+do
+ local ellipsis = '...'
+ local n_ellipsis = #ellipsis
+
+ --- Return a shortened version of a string.
+ -- Fits string within w characters. Removed characters are marked with ellipsis.
+ -- @string s the string
+ -- @int w the maxinum size allowed
+ -- @bool tail true if we want to show the end of the string (head otherwise)
+ -- @usage ('1234567890'):shorten(8) == '12345...'
+ -- @usage ('1234567890'):shorten(8, true) == '...67890'
+ -- @usage ('1234567890'):shorten(20) == '1234567890'
+ function stringx.shorten(s,w,tail)
+ assert_string(1,s)
+ if #s > w then
+ if w < n_ellipsis then return ellipsis:sub(1,w) end
+ if tail then
+ local i = #s - w + 1 + n_ellipsis
+ return ellipsis .. s:sub(i)
+ else
+ return s:sub(1,w-n_ellipsis) .. ellipsis
+ end
+ end
+ return s
+ end
end
---- Utility function that finds any patterns that match a long string's an open or close.
--- Note that having this function use the least number of equal signs that is possible is a harder algorithm to come up with.
--- Right now, it simply returns the greatest number of them found.
--- @param s The string
--- @return 'nil' if not found. If found, the maximum number of equal signs found within all matches.
-local function has_lquote(s)
- local lstring_pat = '([%[%]])(=*)%1'
- local equals, new_equals, _
- local finish = 1
- repeat
- _, finish, _, new_equals = s:find(lstring_pat, finish)
- if new_equals then
- equals = max(equals or 0, #new_equals)
- end
- until not new_equals
- return equals
+do
+ -- Utility function that finds any patterns that match a long string's an open or close.
+ -- Note that having this function use the least number of equal signs that is possible is a harder algorithm to come up with.
+ -- Right now, it simply returns the greatest number of them found.
+ -- @param s The string
+ -- @return 'nil' if not found. If found, the maximum number of equal signs found within all matches.
+ local function has_lquote(s)
+ local lstring_pat = '([%[%]])(=*)%1'
+ local equals, new_equals, _
+ local finish = 1
+ repeat
+ _, finish, _, new_equals = s:find(lstring_pat, finish)
+ if new_equals then
+ equals = max(equals or 0, #new_equals)
+ end
+ until not new_equals
+
+ return equals
+ end
+
+ --- Quote the given string and preserve any control or escape characters, such that reloading the string in Lua returns the same result.
+ -- @param s The string to be quoted.
+ -- @return The quoted string.
+ function stringx.quote_string(s)
+ assert_string(1,s)
+ -- Find out if there are any embedded long-quote sequences that may cause issues.
+ -- This is important when strings are embedded within strings, like when serializing.
+ -- Append a closing bracket to catch unfinished long-quote sequences at the end of the string.
+ local equal_signs = has_lquote(s .. "]")
+
+ -- Note that strings containing "\r" can't be quoted using long brackets
+ -- as Lua lexer converts all newlines to "\n" within long strings.
+ if (s:find("\n") or equal_signs) and not s:find("\r") then
+ -- If there is an embedded sequence that matches a long quote, then
+ -- find the one with the maximum number of = signs and add one to that number.
+ equal_signs = ("="):rep((equal_signs or -1) + 1)
+ -- Long strings strip out leading newline. We want to retain that, when quoting.
+ if s:find("^\n") then s = "\n" .. s end
+ local lbracket, rbracket =
+ "[" .. equal_signs .. "[",
+ "]" .. equal_signs .. "]"
+ s = lbracket .. s .. rbracket
+ else
+ -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly.
+ s = ("%q"):format(s):gsub("\r", "\\r")
+ end
+ return s
+ end
end
---- Quote the given string and preserve any control or escape characters, such that reloading the string in Lua returns the same result.
--- @param s The string to be quoted.
--- @return The quoted string.
-function stringx.quote_string(s)
- assert_string(1,s)
- -- Find out if there are any embedded long-quote sequences that may cause issues.
- -- This is important when strings are embedded within strings, like when serializing.
- -- Append a closing bracket to catch unfinished long-quote sequences at the end of the string.
- local equal_signs = has_lquote(s .. "]")
-
- -- Note that strings containing "\r" can't be quoted using long brackets
- -- as Lua lexer converts all newlines to "\n" within long strings.
- if (s:find("\n") or equal_signs) and not s:find("\r") then
- -- If there is an embedded sequence that matches a long quote, then
- -- find the one with the maximum number of = signs and add one to that number.
- equal_signs = ("="):rep((equal_signs or -1) + 1)
- -- Long strings strip out leading newline. We want to retain that, when quoting.
- if s:find("^\n") then s = "\n" .. s end
- local lbracket, rbracket =
- "[" .. equal_signs .. "[",
- "]" .. equal_signs .. "]"
- s = lbracket .. s .. rbracket
+
+--- Python-style formatting operator.
+-- Calling `text.format_operator()` overloads the % operator for strings to give
+-- Python/Ruby style formated output.
+-- This is extended to also do template-like substitution for map-like data.
+--
+-- Note this goes further than the original, and will allow these cases:
+--
+-- 1. a single value
+-- 2. a list of values
+-- 3. a map of var=value pairs
+-- 4. a function, as in gsub
+--
+-- For the second two cases, it uses $-variable substituion.
+--
+-- When called, this function will monkey-patch the global `string` metatable by
+-- adding a `__mod` method.
+--
+-- See <a href="http://lua-users.org/wiki/StringInterpolation">the lua-users wiki</a>
+--
+-- @usage
+-- require 'pl.text'.format_operator()
+-- local out1 = '%s = %5.3f' % {'PI',math.pi} --> 'PI = 3.142'
+-- local out2 = '$name = $value' % {name='dog',value='Pluto'} --> 'dog = Pluto'
+function stringx.format_operator()
+
+ local format = string.format
+
+ -- a more forgiving version of string.format, which applies
+ -- tostring() to any value with a %s format.
+ local function formatx (fmt,...)
+ local args = pack(...)
+ local i = 1
+ for p in fmt:gmatch('%%.') do
+ if p == '%s' and type(args[i]) ~= 'string' then
+ args[i] = tostring(args[i])
+ end
+ i = i + 1
+ end
+ return format(fmt,unpack(args))
+ end
+
+ local function basic_subst(s,t)
+ return (s:gsub('%$([%w_]+)',t))
+ end
+
+ getmetatable("").__mod = function(a, b)
+ if b == nil then
+ return a
+ elseif type(b) == "table" and getmetatable(b) == nil then
+ if #b == 0 then -- assume a map-like table
+ return _substitute(a,b,true)
+ else
+ return formatx(a,unpack(b))
+ end
+ elseif type(b) == 'function' then
+ return basic_subst(a,b)
else
- -- Escape funny stuff. Lua 5.1 does not handle "\r" correctly.
- s = ("%q"):format(s):gsub("\r", "\\r")
+ return formatx(a,b)
end
- return s
+ end
end
+--- import the stringx functions into the global string (meta)table
function stringx.import()
utils.import(stringx,string)
end
@@ -12413,246 +13135,26 @@ end,
-- libraries, see string.Template). It also provides similar functions to those
-- found in the textwrap module.
--
--- See @{03-strings.md.String_Templates|the Guide}.
---
--- Calling `text.format_operator()` overloads the % operator for strings to give Python/Ruby style formated output.
--- This is extended to also do template-like substitution for map-like data.
+-- IMPORTANT: this module has been deprecated and will be removed in a future
+-- version (2.0). The contents of this module have moved to the `pl.stringx`
+-- module.
--
--- > require 'pl.text'.format_operator()
--- > = '%s = %5.3f' % {'PI',math.pi}
--- PI = 3.142
--- > = '$name = $value' % {name='dog',value='Pluto'}
--- dog = Pluto
+-- See @{03-strings.md.String_Templates|the Guide}.
--
--- Dependencies: `pl.utils`, `pl.types`
+-- Dependencies: `pl.stringx`, `pl.utils`
-- @module pl.text
-local gsub = string.gsub
-local concat,append = table.concat,table.insert
-local utils = require 'pl.utils'
-local bind1,usplit,assert_arg = utils.bind1,utils.split,utils.assert_arg
-local is_callable = require 'pl.types'.is_callable
-local unpack = utils.unpack
-
-local text = {}
-
-
-local function makelist(l)
- return setmetatable(l, require('pl.List'))
-end
-
-local function lstrip(str) return (str:gsub('^%s+','')) end
-local function strip(str) return (lstrip(str):gsub('%s+$','')) end
-local function split(s,delim) return makelist(usplit(s,delim)) end
-
-local function imap(f,t,...)
- local res = {}
- for i = 1,#t do res[i] = f(t[i],...) end
- return res
-end
-
-local function _indent (s,sp)
- local sl = split(s,'\n')
- return concat(imap(bind1('..',sp),sl),'\n')..'\n'
-end
-
---- indent a multiline string.
--- @param s the string
--- @param n the size of the indent
--- @param ch the character to use when indenting (default ' ')
--- @return indented string
-function text.indent (s,n,ch)
- assert_arg(1,s,'string')
- assert_arg(2,n,'number')
- return _indent(s,string.rep(ch or ' ',n))
-end
-
---- dedent a multiline string by removing any initial indent.
--- useful when working with [[..]] strings.
--- @param s the string
--- @return a string with initial indent zero.
-function text.dedent (s)
- assert_arg(1,s,'string')
- local sl = split(s,'\n')
- local _,i2 = (#sl>0 and sl[1] or ''):find('^%s*')
- sl = imap(string.sub,sl,i2+1)
- return concat(sl,'\n')..'\n'
-end
-
---- format a paragraph into lines so that they fit into a line width.
--- It will not break long words, so lines can be over the length
--- to that extent.
--- @param s the string
--- @param width the margin width, default 70
--- @return a list of lines (List object)
--- @see pl.List
-function text.wrap (s,width)
- assert_arg(1,s,'string')
- width = width or 70
- s = s:gsub('\n',' ')
- local i,nxt = 1
- local lines,line = {}
- while i < #s do
- nxt = i+width
- if s:find("[%w']",nxt) then -- inside a word
- nxt = s:find('%W',nxt+1) -- so find word boundary
- end
- line = s:sub(i,nxt)
- i = i + #line
- append(lines,strip(line))
- end
- return makelist(lines)
-end
-
---- format a paragraph so that it fits into a line width.
--- @param s the string
--- @param width the margin width, default 70
--- @return a string
--- @see wrap
-function text.fill (s,width)
- return concat(text.wrap(s,width),'\n') .. '\n'
-end
-
-local Template = {}
-text.Template = Template
-Template.__index = Template
-setmetatable(Template, {
- __call = function(obj,tmpl)
- return Template.new(tmpl)
- end})
-
-function Template.new(tmpl)
- assert_arg(1,tmpl,'string')
- local res = {}
- res.tmpl = tmpl
- setmetatable(res,Template)
- return res
-end
-
-local function _substitute(s,tbl,safe)
- local subst
- if is_callable(tbl) then
- subst = tbl
- else
- function subst(f)
- local s = tbl[f]
- if not s then
- if safe then
- return f
- else
- error("not present in table "..f)
- end
- else
- return s
- end
- end
- end
- local res = gsub(s,'%${([%w_]+)}',subst)
- return (gsub(res,'%$([%w_]+)',subst))
-end
-
---- substitute values into a template, throwing an error.
--- This will throw an error if no name is found.
--- @param tbl a table of name-value pairs.
-function Template:substitute(tbl)
- assert_arg(1,tbl,'table')
- return _substitute(self.tmpl,tbl,false)
-end
-
---- substitute values into a template.
--- This version just passes unknown names through.
--- @param tbl a table of name-value pairs.
-function Template:safe_substitute(tbl)
- assert_arg(1,tbl,'table')
- return _substitute(self.tmpl,tbl,true)
-end
-
---- substitute values into a template, preserving indentation. <br>
--- If the value is a multiline string _or_ a template, it will insert
--- the lines at the correct indentation. <br>
--- Furthermore, if a template, then that template will be subsituted
--- using the same table.
--- @param tbl a table of name-value pairs.
-function Template:indent_substitute(tbl)
- assert_arg(1,tbl,'table')
- if not self.strings then
- self.strings = split(self.tmpl,'\n')
- end
- -- the idea is to substitute line by line, grabbing any spaces as
- -- well as the $var. If the value to be substituted contains newlines,
- -- then we split that into lines and adjust the indent before inserting.
- local function subst(line)
- return line:gsub('(%s*)%$([%w_]+)',function(sp,f)
- local subtmpl
- local s = tbl[f]
- if not s then error("not present in table "..f) end
- if getmetatable(s) == Template then
- subtmpl = s
- s = s.tmpl
- else
- s = tostring(s)
- end
- if s:find '\n' then
- s = _indent(s,sp)
- end
- if subtmpl then return _substitute(s,tbl)
- else return s
- end
- end)
- end
- local lines = imap(subst,self.strings)
- return concat(lines,'\n')..'\n'
-end
-
-------- Python-style formatting operator ------
--- (see <a href="http://lua-users.org/wiki/StringInterpolation">the lua-users wiki</a>) --
-
-function text.format_operator()
-
- local format = string.format
+local utils = require("pl.utils")
- -- a more forgiving version of string.format, which applies
- -- tostring() to any value with a %s format.
- local function formatx (fmt,...)
- local args = {...}
- local i = 1
- for p in fmt:gmatch('%%.') do
- if p == '%s' and type(args[i]) ~= 'string' then
- args[i] = tostring(args[i])
- end
- i = i + 1
- end
- return format(fmt,unpack(args))
- end
-
- local function basic_subst(s,t)
- return (s:gsub('%$([%w_]+)',t))
- end
-
- -- Note this goes further than the original, and will allow these cases:
- -- 1. a single value
- -- 2. a list of values
- -- 3. a map of var=value pairs
- -- 4. a function, as in gsub
- -- For the second two cases, it uses $-variable substituion.
- getmetatable("").__mod = function(a, b)
- if b == nil then
- return a
- elseif type(b) == "table" and getmetatable(b) == nil then
- if #b == 0 then -- assume a map-like table
- return _substitute(a,b,true)
- else
- return formatx(a,unpack(b))
- end
- elseif type(b) == 'function' then
- return basic_subst(a,b)
- else
- return formatx(a,b)
- end
- end
-end
+utils.raise_deprecation {
+ source = "Penlight " .. utils._VERSION,
+ message = "the contents of module 'pl.text' has moved into 'pl.stringx'",
+ version_removed = "2.0.0",
+ deprecated_after = "1.11.0",
+ no_trace = true,
+}
-return text
+return require "pl.stringx"
end,
@@ -12919,7 +13421,10 @@ local format = string.format
local compat = require 'pl.compat'
local stdout = io.stdout
local append = table.insert
+local concat = table.concat
local _unpack = table.unpack -- always injected by 'compat'
+local find = string.find
+local sub = string.sub
local is_windows = compat.is_windows
local err_mode = 'default'
@@ -12928,7 +13433,7 @@ local operators
local _function_factories = {}
-local utils = { _VERSION = "1.11.0" }
+local utils = { _VERSION = "1.12.0" }
for k, v in pairs(compat) do utils[k] = v end
--- Some standard patterns
@@ -12970,7 +13475,7 @@ utils.pack = table.pack -- added here to be symmetrical with unpack
-- that this one DOES honor the `n` field in the table `t`, such that it is 'nil-safe'.
-- @param t table to unpack
-- @param[opt] i index from which to start unpacking, defaults to 1
--- @param[opt] t index of the last element to unpack, defaults to `t.n` or `#t`
+-- @param[opt] j index of the last element to unpack, defaults to `t.n` or else `#t`
-- @return multiple return values from the table
-- @function utils.unpack
-- @see compat.unpack
@@ -13156,6 +13661,68 @@ function utils.assert_arg (n,val,tp,verify,msg,lev)
return val
end
+--- creates an Enum table.
+-- This helps prevent magic strings in code by throwing errors for accessing
+-- non-existing values.
+--
+-- Calling on the object does the same, but returns a soft error; `nil + err`.
+--
+-- The values are equal to the keys. The enum object is
+-- read-only.
+-- @param ... strings that make up the enumeration.
+-- @return Enum object
+-- @usage -- accessing at runtime
+-- local obj = {}
+-- obj.MOVEMENT = utils.enum("FORWARD", "REVERSE", "LEFT", "RIGHT")
+--
+-- if current_movement == obj.MOVEMENT.FORWARD then
+-- -- do something
+--
+-- elseif current_movement == obj.MOVEMENT.REVERES then
+-- -- throws error due to typo 'REVERES', so a silent mistake becomes a hard error
+-- -- "'REVERES' is not a valid value (expected one of: 'FORWARD', 'REVERSE', 'LEFT', 'RIGHT')"
+--
+-- end
+-- @usage -- validating user-input
+-- local parameter = "...some user provided option..."
+-- local ok, err = obj.MOVEMENT(parameter) -- calling on the object
+-- if not ok then
+-- print("bad 'parameter', " .. err)
+-- os.exit(1)
+-- end
+function utils.enum(...)
+ local lst = utils.pack(...)
+ utils.assert_arg(1, lst[1], "string") -- at least 1 string
+
+ local enum = {}
+ for i, value in ipairs(lst) do
+ utils.assert_arg(i, value, "string")
+ enum[value] = value
+ end
+
+ local valid = "(expected one of: '" .. concat(lst, "', '") .. "')"
+ setmetatable(enum, {
+ __index = function(self, key)
+ error(("'%s' is not a valid value %s"):format(tostring(key), valid), 2)
+ end,
+ __newindex = function(self, key, value)
+ error("the Enum object is read-only", 2)
+ end,
+ __call = function(self, key)
+ if type(key) == "string" then
+ local v = rawget(self, key)
+ if v then
+ return v
+ end
+ end
+ return nil, ("'%s' is not a valid value %s"):format(tostring(key), valid)
+ end
+ })
+
+ return enum
+end
+
+
--- process a function argument.
-- This is used throughout Penlight and defines what is meant by a function:
-- Something that is callable, or an operator string as defined by <code>pl.operator</code>,
@@ -13367,7 +13934,7 @@ function utils.quote_arg(argument)
r[i] = utils.quote_arg(arg)
end
- return table.concat(r, " ")
+ return concat(r, " ")
end
-- only a single argument
if is_windows then
@@ -13436,7 +14003,6 @@ end
-- @see splitv
function utils.split(s,re,plain,n)
utils.assert_string(1,s)
- local find,sub,append = string.find, string.sub, table.insert
local i1,ls = 1,{}
if not re then re = '%s+' end
if re == '' then return {s} end
diff --git a/macros/luatex/generic/penlight/penlight.pdf b/macros/luatex/generic/penlight/penlight.pdf
index 1edb8bf2b5..a8e166a1ae 100644
--- a/macros/luatex/generic/penlight/penlight.pdf
+++ b/macros/luatex/generic/penlight/penlight.pdf
Binary files differ
diff --git a/macros/luatex/generic/penlight/penlight.sty b/macros/luatex/generic/penlight/penlight.sty
index 9f0ead874f..b9f5a9490a 100644
--- a/macros/luatex/generic/penlight/penlight.sty
+++ b/macros/luatex/generic/penlight/penlight.sty
@@ -1,5 +1,5 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-12-15
+% 2022-02-27
% Copyright (C) 2021 Kale Ewasiuk
%
% Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -22,7 +22,7 @@
% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
% OR OTHER DEALINGS IN THE SOFTWARE.
-\ProvidesPackage{penlight}[2021-12-15]
+\ProvidesPackage{penlight}[2022-02-27]
\RequirePackage{luacode}
@@ -34,9 +34,15 @@
penlight = require('penlight')
__PENLIGHT__ = 'penlight'
}}
+
+
\DeclareOption{stringx}{\luadirect{_G[__PENLIGHT__].stringx.import()}}
-\DeclareOption{format}{\luadirect{_G[__PENLIGHT__].text.format_operator()}}
+\DeclareOption{format}{\luadirect{_G[__PENLIGHT__].stringx.format_operator()}}
\DeclareOption{func}{\luadirect{_G[__PENLIGHT__].utils.import(__PENLIGHT__..'.func')}}
\DeclareOption{extras}{\luadirect{require('penlightextras')}}
+\DeclareOption{extrasnoglobals}{
+ __PL_NO_GLOBALS__ = true
+ \luadirect{require('penlightextras')
+ }}
\ProcessOptions*\relax
diff --git a/macros/luatex/generic/penlight/penlight.tex b/macros/luatex/generic/penlight/penlight.tex
index 79a5d08589..176dc4e557 100644
--- a/macros/luatex/generic/penlight/penlight.tex
+++ b/macros/luatex/generic/penlight/penlight.tex
@@ -1,5 +1,5 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-12-15
+% 2022-02-27
% Copyright (C) 2021 Kale Ewasiuk
%
% Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -25,7 +25,7 @@
\documentclass[11pt,parskip=half]{scrartcl}
\setlength{\parindent}{0ex}
-\newcommand{\llcmd}[1]{\leavevmode\llap{\texttt{\detokenize{#1}\ }}}
+\newcommand{\llcmd}[1]{\leavevmode\llap{\texttt{\detokenize{#1}}}}
\newcommand{\cmd}[1]{\texttt{\detokenize{#1}}}
\newcommand{\qcmd}[1]{``\cmd{#1}''}
\usepackage{url}
@@ -52,7 +52,7 @@
\date{\today}
-\RequirePackage{penlight}
+\RequirePackage[pl,extras]{penlight}
\title{penlight}
\subtitle{Lua libraries for use in LuaLaTeX}
@@ -60,7 +60,7 @@
\maketitle
The official documentation for the Lua library can be found here:\\
- \mbox{\url{https://stevedonovan.github.io/Penlight/api/manual/01-introduction.md.html#}}
+ \mbox{\url{https://lunarmodules.github.io/Penlight}}
\\
\subsection*{Required Package Option}
@@ -70,48 +70,91 @@
\texttt{penlight.XYZ} or \texttt{pl.XYZ}
+
+ \subsection*{texlua usage}
+If you want to use Penlight (and extras) with the \texttt{texlua} intrepreter (no document made, only for Lua files, useful for testing),
+you can access it by setting \cmd{__SKIP_TEX__ = true} and adding the package to path. For example:
+ \begin{verbatim}
+package.path = package.path .. ';'..'path/to/texmf/tex/latex/penlight/?.lua'
+penlight = require('penlight')
+-- below is optional
+__SKIP_TEX__ = true --only required if you want to use
+ --penlightextras without a LaTeX run
+-- __PL_NO_GLOBALS__ = true -- optional, skip global definitions
+require('penlightextras')
+\end{verbatim}
- \subsection*{Additional Package Options}
+\pagebreak
+
+
+\subsection*{Additional Package Options}
\noindent
- \begin{tabular}{lp{4.5in}}
+ \hspace*{-6ex}\begin{tabular}{lp{4.8in}}
\texttt{stringx} & will import additional string functions into the string meta table.\\
& this will be ran in pre-amble: \texttt{require('pl.stringx').import()}\\
- & \hspace*{-4em}\url{https://stevedonovan.github.io/Penlight/api/libraries/pl.stringx.html}\\\\
\texttt{format} & allows \% operator for Python-style string formating\\
- & this will be ran in pre-amble: \texttt{require('pl.text').format\_operator()}\\
- & \hspace*{-4em}\url{https://stevedonovan.github.io/Penlight/api/libraries/pl.text.html}\\\\
+ & this will be ran in pre-amble: \texttt{require('pl.stringx').format\_operator()}\\
+ & \mbox{\url{https://lunarmodules.github.io/Penlight/libraries/pl.stringx.html#format_operator}}
+ \\
\texttt{func} & allows placehold expressions eg. \texttt{\_1+1} to be used \\
& this will be ran in pre-amble: \texttt{penlight.utils.import('pl.func')}\\
- & \hspace*{-4em}\url{https://stevedonovan.github.io/Penlight/api/libraries/pl.func.html}\\\\
- \texttt{extras} & adds some additional functions. Adds functions to some Penlight sub-modules. The following Lua globals will be defined:
- \texttt{close\_bkt\_cnt,
- add\_bkt\_cnt,
- reset\_bkt\_cnt,
- \_NumBkts,
- \_xTrue,
- \_xFalse,
- \_xNoValue,
- \_\_SKIP\_TEX\_\_,
- hasval,
- mod, mod2,
- }\\
+ & \url{https://lunarmodules.github.io/Penlight/libraries/pl.func}\\
+ \texttt{\llap{extras}noglobals} & does the above three (\cmd{func,stringx,format}); adds some additional functions to \cmd{penlight} module; and adds the \cmd{pl.tex} sub-module.\\
+ \texttt{extras} & does the above \texttt{extrasnoglobals} but makes many of the functions global variables.
\end{tabular}
- \\\\
-
- If you want to use Penlight (and extras) with the \texttt{texlua} intrepreter (no document made, only for Lua files, useful for testing),
- you can access it by the following:
- \begin{verbatim}
- package.path = package.path .. ';'..'path/to/texmf/tex/latex/penlight/?.lua'
- penlight = require('penlight')
- __SKIP_TEX__ = true --only required if you want to use
- --penlightextras without a LaTeX run
- require('penlightextras')
- \end{verbatim}
+\subsection*{Extras}
+
+If \cmd{extras} is used, the following Lua globals will be defined:\\
+
+\subsubsection*{Misc stuff}
+\llcmd{__SKIP_TEX__} If using package with \cmd{texlua}, set this global before loading \cmd{penlight}\\
+\llcmd{__PL_NO_}\cmd{GLOBALS__} If using package with \cmd{texlua} and you don't want to set some globals (described in next sections), set this global before to \cmd{true} loading \cmd{penlight}\\
+\llcmd{hasval(x)} Python-like boolean testing\\
+\llcmd{COMP'xyz'()} Python-like comprehensions:\\\url{https://lunarmodules.github.io/Penlight/libraries/pl.comprehension.html}\\
+\llcmd{math.mod(n,d)}, \cmd{math.mod2(n)} math modulous\\
+\llcmd{string.}\cmd{totable(s)} string a table of characters\\
+\llcmd{kpairs(t), }\cmd{npairs(t)} iterate over keys only, or include nil value from table ipairs\\
+
+\llcmd{pl.utils.}\cmd{filterfiles}\cmd{(dir,filt,rec)} Get files from dir and apply glob-like filters. Set rec to \cmd{true} to include sub directories\\
+
+\pagebreak
+
+\subsubsection*{\cmd{pl.tex.} module is added}
+\llcmd{add_bkt}\cmd{_cnt(n), }\cmd{close_bkt_cnt(n), reset_bkt_cnt} functions to keep track of adding curly brackets as strings. \cmd{add} will return \cmd{n} (default 1) \{'s and increment a counter. \cmd{close} will return \cmd{n} \}'s (default will close all brackets) and decrement.\\
+\llcmd{_NumBkts} internal integer for tracking the number of brackets\\
+\llcmd{opencmd(cs)} prints \cmd{\cs}\{ and adds to the bracket counters.\\
+\\
+\llcmd{_xNoValue,}\cmd{_xTrue,_xFalse}: \cmd{xparse} equivalents for commands\\
+\\
+\llcmd{prt(x),prtn(x)} print without or with a newline at end. Tries to help with special characters or numbers printing.\\
+\llcmd{prtl(l),prtt(t)} print a literal string, or table\\
+\llcmd{wrt(x), wrtn(x)} write to log\\
+\llcmd{help_wrt}\cmd{(s1, s2)} pretty-print something to console. S2 is a flag to help you find.\\
+\llcmd{prt_array2d(tt)} pretty print a 2d array\\
+\\
+\llcmd{pkgwarn}\cmd{(pkg, msg1, msg2)} throw a package warning\\
+\llcmd{pkgerror}\cmd{(pkg, msg1, msg2, stop)} throw a package error. If stop is true, immediately ceases compile.\\
+\\
+\llcmd{defcmd}\cmd{(cs, val)} like \cmd{\gdef}\\
+\llcmd{newcmd}\cmd{(cs, val)} like \cmd{\newcommand}\\
+\llcmd{renewcmd}\cmd{(cs, val)} like \cmd{\renewcommand}\\
+\llcmd{prvcmd}\cmd{(cs, val)} like \cmd{\providecommand}\\
+\llcmd{deccmd}\cmd{(cs, dft, overwrite)} declare a command. If \cmd{dft} (default) is \cmd{nil}, \cmd{cs} is set
+to a package warning saying \cmd{'cs' was declared and used in document, but never set}. If \cmd{overwrite}
+is true, it will overwrite an existing command (using \cmd{defcmd}), otherwise, it will throw error like \cmd{newcmd}.\\
+
+
+
+\subsubsection*{global extras}
+If \cmd{extras} is used and NOT \cmd{extrasnoglobals}, then some globals are set.\\
+All \cmd{pl.tex} modules are added.\\
+\cmd{hasval}, \cmd{COMP}, \cmd{kpairs}, \cmd{npairs} are globals.\\
+\cmd{pl.tablex} functions are added to the \cmd{table} table.\\
\section*{}
Disclaimer: I am not the author of the Lua Penlight library.
diff --git a/macros/luatex/generic/penlight/penlightextras.lua b/macros/luatex/generic/penlight/penlightextras.lua
index 3479eb3fa0..0cdec03763 100644
--- a/macros/luatex/generic/penlight/penlightextras.lua
+++ b/macros/luatex/generic/penlight/penlightextras.lua
@@ -1,172 +1,264 @@
+
+__SKIP_TEX__ = __SKIP_TEX__ or false --if declared true before here, it will use regular print functions
+-- (for troubleshooting with texlua instead of actual use in lua latex)
+
+__PL_NO_GLOBALS__ = __PL_NO_GLOBALS__ or false
+
-- requires penlight
local pl = _G['penlight'] or _G['pl'] -- penlight for this namespace is pl
-local bind = bind or pl.func.bind
--- some bonus string operations, % text operator, and functional programmng
+
+-- some bonus string operations, % text operator, and functional programming
pl.stringx.import()
pl.text.format_operator()
-pl.utils.import('pl.func')
+pl.utils.import('pl.func') -- allow placeholder expressions _1 +1 etc.
+
+pl.COMP = require'pl.comprehension'.new() -- for comprehensions
+
+-- http://lua-users.org/wiki/SplitJoin -- todo read me!!
+
+pl.tex = {} -- adding a sub-module for tex related stuff
+
+local bind = bind or pl.func.bind
+
+
+function pl.hasval(x) -- if something has value
+ if (type(x) == 'function') or (type(x) == 'CFunction') or (type(x) == 'userdata') then
+ return true
+ elseif (x == nil) or (x == false) or (x == 0) or (x == '') or (x == {}) then
+ return false
+ elseif (type(x) ~= 'boolean') and (type(x) ~= 'number') and (type(x) ~= 'string') then -- something else? maybe ths not needed
+ if #x == 0 then -- one more check, probably no needed though, I was trying to cover other classes but they all tables
+ return false
+ else
+ return true
+ end
+ end
+ return true
+end
+
+
+-- Some simple and helpful LaTeX functions -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+-- xparse defaults
+pl.tex._xTrue = '\\BooleanTrue '
+pl.tex._xFalse = '\\BooleanFalse '
+pl.tex._xNoValue = '-NoValue-'
+
+--Generic LuaLaTeX utilities for print commands or environments
+
+if not __SKIP_TEX__ then
+ local function check_special_chars(s) -- todo extend to toher special chars?
+ if type(s) == 'string' then
+ if string.find(s, '[\n\r\t\0]') then
+ pl.tex.pkgwarn('penlight', 'printing string with special (eg. newline) char, possible unexpected behaviour on string: '..s)
+ end
+ end
+ end
-function help_wrt(s1, s2) -- helpful printing, makes it easy to debug, s1 is object, s2 is note
- local wrt = wrt or texio.write_nl
- local wrt = wrt or print
+ -- NOTE: usage is a bit different than default. If number is first arg, you CANT change catcode.
+ -- We don't need that under normal use, use tex.print or tex.sprint if you need
+ function pl.tex.prt(s, ...) -- print something, no new line after
+ check_special_chars(s)
+ if type(s) == 'number' then s = tostring(s) end
+ tex.sprint(s, ...) --can print lists as well, but will NOT put new line between them or anything printed
+ end
+
+ function pl.tex.prtn(s, ...) -- print with new line after, can print lists or nums. C-function not in Lua, apparantly
+ s = s or ''
+ check_special_chars(s)
+ if type(s) == 'number' then s = tostring(s) end
+ tex.print(s, ...)
+ end
+
+ pl.tex.wrt = texio.write
+ pl.tex.wrtn = texio.write_nl
+else
+ pl.tex.prt = io.write
+ pl.tex.prtn = print --print with new line
+ pl.tex.wrt = io.write
+ pl.tex.wrtn = io.write_nl
+end
+
+function pl.tex.prtl(str) -- prints a literal/lines string in latex, adds new line between them
+ for line in str:gmatch"[^\n]*" do -- gets all characters up to a new line
+ pl.tex.prtn(line)
+ end
+end
+
+-- todo option to specify between character? one for first table, on for recursives?
+function pl.tex.prtt(tab, d1, d2) -- prints a table with new line between each item
+ d1 = d1 or ''
+ d2 = d2 or '\\leavevmode\\\\'
+ for _, t in pairs(tab) do --
+ if type(t) ~= 'table' then
+ if d1 == '' then
+ pl.tex.prtn(t)
+ else
+ pl.tex.prt(t, d1)
+ end
+ else
+ pl.tex.prtn(d2)
+ pl.tex.prtt(t,d1,d2)
+ end
+ end
+end
+
+function pl.tex.help_wrt(s1, s2) -- helpful printing, makes it easy to debug, s1 is object, s2 is note
+ local wrt2 = wrt or texio.write_nl or print
s2 = s2 or ''
- wrt('\nvvvvv '..s2..'\n')
+ wrt2('\nvvvvv '..s2..'\n')
if type(s1) == 'table' then
- wrt(pl.pretty.write(s1))
+ wrt2(pl.pretty.write(s1))
else
- wrt(tostring(s1))
+ wrt2(tostring(s1))
end
- wrt('\n^^^^^\n')
+ wrt2('\n^^^^^\n')
end
-function prt_array2d(t)
+function pl.tex.prt_array2d(t)
for _, r in ipairs(t) do
local s = ''
for _, v in ipairs(r) do
s = s.. tostring(v)..', '
end
- print(s)
+ pl.tex.prt(s)
+ pl.tex.prt('\n')
end
end
-- -- -- -- --
-
--- -- -- -- functions below are helpers for arrays and 2d
-
-local function compare_elements(a, b, op, ele)
- op = op or pl.oper.gt
- ele = ele or 1
- return op(a[ele], b[ele])
+function pl.tex.pkgwarn(pkg, msg1, msg2)
+ pkg = pkg or ''
+ msg1 = msg1 or ''
+ msg2 = msg2 or ''
+ tex.sprint('\\PackageWarning{'..pkg..'}{'..msg1..'}{'..msg2..'}')
end
-local function comp_2ele_func(op, ele) -- make a 2 element comparison function,
- --sort with function on element nnum
- return bind(compare_elements, _1, _2, op, ele)
+function pl.tex.pkgerror(pkg, msg1, msg2, stop)
+ pkg = pkg or ''
+ msg1 = msg1 or ''
+ msg2 = msg2 or ''
+ stop = pl.hasval(stop)
+ tex.sprint('\\PackageError{'..pkg..'}{'..msg1..'}{'..msg2..'}')
+ if stop then tex.sprint('\\stop') end -- stop on the spot (say that 10 times)
end
--- -- -- --
+--definition helpers -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+function pl.tex.defcmd(cs, val) -- simple definitions
+ val = val or ''
+ token.set_macro(cs, val, 'global')
+end
-local function check_index(ij, rc) -- converts array index to positive value if negative
- if type(ij) ~= 'number' then
- return 1
+function pl.tex.prvcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ -- do nothing if token is defined already --pkgwarn('penlight', 'Definition '..cs..' is being overwritten')
else
- if ij < 0 then
- ij = rc + ij + 1
- elseif ij > rc then
- ij = rc
- end
- return ij
+ pl.tex.defcmd(cs, val)
end
end
-local function check_slice(M, i1, j1, i2, j2) -- ensure a slice is valid; i.e. all positive numbers
- r, c = pl.array2d.size(M)
- i1 = check_index(i1, r)
- i2 = check_index(i2, r)
- j1 = check_index(j1, c)
- j2 = check_index(j2, c)
- return i1, j1, i2, j2
-end
-local function check_func(func) -- check if a function is a PE, if so, make it a function
- if type(func) ~= 'function' then
- __func = I(func)
+function pl.tex.newcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ pl.tex.pkgerror('penlight: newcmd',cs..' already defined')
+ else
+ pl.tex.defcmd(cs, val)
end
- return __func
end
-
--- -- -- -- -- -- --
--- -- -- -- functions below extend the array2d module
-
-
-function pl.array2d.map_slice1(func, L, i1, i2) -- map a function to a slice of an array, can use PlcExpr
- i2 = i2 or i1
- local len = #L
- i1 = check_index(i1, len)
- i2 = check_index(i2, len)
- func = check_func(func)
- for i in pl.seq.range(i1,i2) do
- L[i] = func(L[i])
- end
- return L
+function pl.tex.renewcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ pl.tex.defcmd(cs, val)
+ else
+ pl.tex.pkgerror('penlight: renewcmd',cs..' not defined')
+ end
end
-function pl.array2d.map_slice2(func, M, i1, j1, i2, j2) -- map a function to a slice of a Matrix
- i1, j1, i2, j2 = check_slice(M, i1, j1, i2, j2)
- --for i,j in array2d.iter(M, true, i1, j1, i2, j2) do --todo this did not work, penlight may have fixed this
- func = check_func(func)
- for i in pl.seq.range(i1,i2) do
- for j in pl.seq.range(j1,j2) do
- M[i][j] = func(M[i][j])
- end
+function pl.tex.deccmd(cs, def, overwrite) -- declare a definition, placeholder throws an error if it used but not set!
+ overwrite = pl.hasval(overwrite)
+ local decfun
+ if overwrite then decfun = pl.tex.defcmd else decfun = pl.tex.newcmd end
+ if def == nil then
+ decfun(cs, pkgerror('penlight', cs..' was declared and used in document, but never set'))
+ else
+ decfun(cs, def)
end
- return M
end
-function pl.array2d.map_columns(func, M, j1, j2) -- map function to columns of matrix
- j2 = j2 or j1
- return map_slice2(func, M, 1, j1, -1, j2)
-end
-function pl.array2d.map_rows(func, M, i1, i2) -- map function to rows of matrix
- i2 = i2 or i1
- return map_slice2(func, M, i1, 1, i2, -1)
-end
+--
+-- -- todo add and improve this, options for args?
+--local function defcmd_nest(cs) -- for option if you'd like your commands under a parent ex. \csparent{var}
+-- tex.print('\\gdef\\'..cs..'#1{\\csname '..var..'--#1--\\endcsname}')
+--end
+--
+--
+--local function defcmd(cs, val, nargs)
+-- if (nargs == nil) or (args == 0) then
+-- token.set_macro(cs, tostring(val), 'global')
+-- else
+-- local args = '#1'
+-- tex.print('\\gdef\\'..cs..args..'{'..val..'}')
+-- -- todo https://tex.stackexchange.com/questions/57551/create-a-capitalized-macro-token-using-csname
+-- -- \expandafter\gdef\csname Two\endcsname#1#2{1:#1, two:#2} --todo do it like this
+-- end
+--end
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- -- -- -- -- -- -- --
-function pl.array2d.sortOP(M, op, ele) -- sort a 2d array based on operator criteria, ele is column, ie sort on which element
- M_new = {}
- for row in pl.seq.sort(M, comp_2ele_func(op, ele)) do
- M_new[#M_new+1] = row
- end
- return M_new
+
+-- when nesting commands, this makes it helpful to not worry about brackets
+pl.tex._NumBkts = 0
+--prt(opencmd('textbf')..opencmd('texttt')..'bold typwriter'..close_bkt_cnt())
+
+function pl.tex.opencmd(cmd)
+ return '\\'..cmd..add_bkt_cnt()
end
-function pl.array2d.like(M1, v)
- v = v or 0
- r, c = pl.array2d.size(M1)
- return pl.array2d.new(r,c,v)
+function pl.tex.reset_bkt_cnt(n)
+ n = n or 0
+ _NumBkts = n
end
-function pl.array2d.from_table(t) -- turns a labelled table to a 2d, label-free array
- t_new = {}
- for k, v in pairs(t) do
- if type(v) == 'table' then
- t_new_row = {k}
- for _, v_ in ipairs(v) do
- t_new_row[#t_new_row+1] = v_
- end
- t_new[#t_new+1] = t_new_row
- else
- t_new[#t_new+1] = {k, v}
- end
- end
- return t_new
+function pl.tex.add_bkt_cnt(n)
+ -- add open bracket n times, returns brackets
+ n = n or 1
+ _NumBkts = _NumBkts + n
+ return ('{'):rep(n)
end
-function pl.array2d.toTeX(M, EL) --puts & between columns, can choose to end line with \\ if EL is true (end-line)
- EL = EL or false
- if EL then EL = '\\\\' else EL = '' end
- return pl.array2d.reduce2(_1..EL.._2, _1..'&'.._2, M)..EL
+function pl.tex.close_bkt_cnt(n)
+ n = n or _NumBkts
+ local s = ('}'):rep(n)
+ _NumBkts = _NumBkts - n
+ return s
end
--- -- -- -- -- -- --
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+
--- -- -- -- -- -- -- -- -- -- -- -- functions below extend the operator module
-function pl.operator.strgt(a,b) return tostring(a) > tostring(b) end
-function pl.operator.strlt(a,b) return tostring(a) < tostring(b) end
+-- -- -- -- math stuff
+function math.mod(a, b) -- math modulo, return remainder only
+ return a - (math.floor(a/b)*b)
+end
+
+function math.mod2(a) -- math modulo 2
+ return math.mod(a,2)
+end
+
-- -- -- -- string stuff
@@ -184,14 +276,13 @@ local number = P{"number",
}
-local mt = getmetatable("") -- register functions with str
-
+local str_mt = getmetatable("") -- register functions with str
-function mt.__index.gnum(s)
+function str_mt.__index.gnum(s)
return number:match(s)
end
-function mt.__index.gextract(s, pat) --extract a pattern from string, returns both
+function str_mt.__index.gextract(s, pat) --extract a pattern from string, returns both
local s_extr = ''
local s_rem = s
for e in s:gmatch(pat) do
@@ -201,7 +292,7 @@ function mt.__index.gextract(s, pat) --extract a pattern from string, returns bo
return s_extr, s_rem
end
-function mt.__index.gfirst(s, t) -- get the first pattern found from a table of pattern
+function str_mt.__index.gfirst(s, t) -- get the first pattern found from a table of pattern
for _, pat in pairs(t) do
if string.find(s, pat) then
return pat
@@ -209,7 +300,7 @@ function mt.__index.gfirst(s, t) -- get the first pattern found from a table of
end
end
-function mt.__index.appif(S, W, B, O) --append W ord to S tring if B oolean true, otherwise O ther
+function str_mt.__index.appif(S, W, B, O) --append W ord to S tring if B oolean true, otherwise O ther
--append Word to String
if B then --if b is true
S = S .. W
@@ -221,155 +312,394 @@ function mt.__index.appif(S, W, B, O) --append W ord to S tring if B oolean true
end
-
--- -- -- -- -- math stuffs
-
-function mod(a, b) -- math modulo, return remainder only
- return a - (math.floor(a/b)*b)
+ function str_mt.__index.containsany(s, exp)
+ if type(exp) ~= 'table' then exp = {exp} end
+ for _, e in ipairs(exp) do
+ if s:find(e) then return true end
+ end
+ return false
end
-function mod2(a) -- math modulo 2
- return mod(a,2)
+function str_mt.__index.containsanycase(s, exp)
+ if type(exp) ~= 'table' then exp = {exp} end
+ for _, e in ipairs(exp) do
+ if s:lower():find(e:lower()) then return true end
+ end
+ return false
end
-function hasval(x) -- if something has value
- if (type(x) == 'function') then
- return true
- elseif (x == nil) or (x == false) or (x == 0) or (x == '') then
- return false
- elseif (type(x) ~= 'boolean') and (type(x) ~= 'number') and (type(x) ~= 'string') then
- if #x == 0 then
- return false
- else
- return true
- end
+function str_mt.__index.totable(str)
+ local t = {}
+ for i = 1, #str do
+ t[i] = str:sub(i, i)
end
- return true
+ return t
end
+-- -- -- -- function stuff
+
+function pl.clone_function(fn)
+ local dumped = string.dump(fn)
+ local cloned = loadstring(dumped)
+ local i = 1
+ while true do
+ local name = debug.getupvalue(fn, i)
+ if not name then
+ break
+ end
+ debug.upvaluejoin(cloned, i, fn, i)
+ i = i + 1
+ end
+ return cloned
+end
+-- -- -- -- -- -- -- -- -- -- -- -- functions below extend the operator module
--- Some simple and helpful LaTeX functions
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+function pl.operator.strgt(a,b) return tostring(a) > tostring(b) end
+function pl.operator.strlt(a,b) return tostring(a) < tostring(b) end
---Generic LuaLaTeX utilities for print commands or environments
--- http://lua-users.org/wiki/SplitJoin -- todo read me!!
-__SKIP_TEX__ = __SKIP_TEX__ or false --if declared true before here, it will use regular print functions
--- (for troubleshooting with texlua)
--- xparse defaults
-_xTrue = '\\BooleanTrue '
-_xFalse = '\\BooleanFalse '
-_xNoValue = '-NoValue-'
+-- -- -- -- functions below are helpers for arrays and 2d
-if not __SKIP_TEX__ then
- prt = tex.sprint --can print lists, but will NOT put new line between them
- prtn = tex.print --can print lists and will put new line. C-function not in Lua. think P rint R return
- wrt = texio.write
- wrtn = texio.write_nl
-else
- prt = io.write
- prtn = print --print with new line
- wrt = io.write
- wrtn = io.write_nl
+local function compare_elements(a, b, op, ele)
+ op = op or pl.oper.gt
+ ele = ele or 1
+ return op(a[ele], b[ele])
end
-function prtl(str) -- prints a literal string to latex, adds new line between them
- for line in str:gmatch"[^\n]*" do -- gets all characters up to a new line
- prtn(line)
- end
+local function comp_2ele_func(op, ele) -- make a 2 element comparison function,
+ --sort with function on element nnum
+ return bind(compare_elements, _1, _2, op, ele)
end
-_NumBkts = 0
-function reset_bkt_cnt(n)
- n = n or 0
- _NumBkts = n
-end
-function add_bkt_cnt(n)
- -- add open bracket n times, returns brackets
- n = n or 1
- _NumBkts = _NumBkts + n
- return ('{'):rep(n)
+
+-- table stuff below
+
+
+function pl.tablex.map_slice(func, T, j1, j2)
+ if type(j1) == 'string' then
+ return pl.array2d.map_slice(func, {T}, ','..j1)[1]
+ else
+ return pl.array2d.map_slice(func, {T}, 1, j1, 1, j2)[1]
+ end
end
-function close_bkt_cnt()
- local s = ('}'):rep(_NumBkts)
- reset_bkt_cnt()
- return s
+pl.array2d.map_slice1 = pl.tablex.map_slice
+
+
+-- todo option for multiple filters with AND logic, like the filter files??
+function pl.tablex.filterstr(t, exp, case)
+ -- case = case sensitive
+ case = hasval(case)
+ -- apply lua patterns to a table to filter iter
+ -- str or table of str's can be passed, OR logic is used if table is passed
+ if case then
+ return pl.tablex.filter(t, bind(string.containsany,_1,exp))
+ else
+ return pl.tablex.filter(t, bind(string.containsanycase,_1,exp))
+ end
end
+function pl.utils.filterfiles(...)
+ -- f1 is a series of filtering patterns, or condition
+ -- f2 is a series of filtering patters, or condition
+ -- (f1_a or f2_...) and (f2 .. ) must match
+ local args = table.pack(...)
+ -- todo -- check where boolean is for recursive or not, set starting argument
+ -- this could allow one to omit dir
+ -- todo if no boolean at all, assume dir = '.' and r = false
+ -- if boolean given, assume dir = '.'
+ local nstart = 3
+ local r = args[2]
+ local dir = args[1]
+ if type(args[1]) == 'boolean' then
+ dir = '.'
+ r = args[1]
+ nstart = 2
+ elseif type(args[2]) ~= 'boolean' then
+ dir = '.'
+ r = false
+ nstart = 1
+ end
+ local files
+ if r then files = pl.dir.getallfiles(dir)
+ else files = pl.dir.getfiles(dir)
+ end
+ for i=nstart,args.n do
+ files = pl.tablex.filter(files, pl.func.compose(bind(string.containsanycase,_1, args[i]), pl.path.basename))
+ end
+ return files
+end
+-- -- -- -- -- -- -- -- functions below extend the array2d module
---definition helpers -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--- todo add and improve this
-local function defcmd_nest(cs) -- for option if you'd like your commands under a parent ex. \csparent{var}
- tex.print('\\gdef\\'..cs..'#1{\\csname '..var..'--#1--\\endcsname}')
+function pl.array2d.map_slice(func, M, i1, j1, i2, j2) -- map a function to a slice of a Matrix
+ func = pl.utils.function_arg(1, func)
+ for i,j in pl.array2d.iter(M, true, i1, j1, i2, j2) do
+ M[i][j] = func(M[i][j])
+ end
+ return M
end
+pl.array2d.map_slice2 = pl.array2d.map_slice
-local function defcmd(cs, val, nargs)
- if (nargs == nil) or (args == 0) then
- token.set_macro(cs, tostring(val), 'global')
+function pl.array2d.map_cols(func, M, j1, j2) -- map function to columns of matrix
+ if type(j1) == 'string' then
+ return pl.array2d.map_slice(func, M, ','..j1)
else
- local args = '#1'
- tex.print('\\gdef\\'..cs..args..'{'..val..'}')
- -- todo https://tex.stackexchange.com/questions/57551/create-a-capitalized-macro-token-using-csname
- -- \expandafter\gdef\csname Two\endcsname#1#2{1:#1, two:#2} --todo do it like this
+ j2 = j2 or -1
+ return pl.array2d.map_slice(func, M, 1, j1, -1, j2)
end
end
+pl.array2d.map_columns = pl.array2d.map_cols
-local function prvcmd(cs, val) -- provide command via lua
- if token.is_defined(cs) then
- tex.print('\\PackageWarning{YAMLvars}{Variable '..cs..' already defined, could not declare}{}')
+function pl.array2d.map_rows(func, M, i1, i2) -- map function to rows of matrix
+ if type(i1) == 'string' then
+ return pl.array2d.map_slice(func, M, i1)
else
- defcmd(cs, val)
+ i2 = i2 or -1
+ return pl.array2d.map_slice(func, M, i1, 1, i2, -1)
end
end
-local function newcmd(cs, val) -- provide command via lua
- if token.is_defined(cs) then
- tex.print('\\PackageError{luadefs}{Command '..cs..' already defined}{}')
- else
- defcmd(cs, val)
+-- -- -- -- -- -- -- --
+
+function pl.array2d.sortOP(M, op, ele) -- sort a 2d array based on operator criteria, ele is column, ie sort on which element
+ M_new = {}
+ for row in pl.seq.sort(M, comp_2ele_func(op, ele)) do
+ M_new[#M_new+1] = row
+ end
+ return M_new
+end
+
+function pl.array2d.like(M1, v)
+ v = v or 0
+ r, c = pl.array2d.size(M1)
+ return pl.array2d.new(r,c,v)
+end
+
+function pl.array2d.from_table(t) -- turns a labelled table to a 2d, label-free array
+ t_new = {}
+ for k, v in pairs(t) do
+ if type(v) == 'table' then
+ t_new_row = {k}
+ for _, v_ in ipairs(v) do
+ t_new_row[#t_new_row+1] = v_
+ end
+ t_new[#t_new+1] = t_new_row
+ else
+ t_new[#t_new+1] = {k, v}
+ end
end
+ return t_new
end
-local function renewcmd(cs, val) -- provide command via lua
- if token.is_defined(cs) then
- defcmd(cs, val)
+function pl.array2d.toTeX(M, EL) --puts & between columns, can choose to end line with \\ if EL is true (end-line)
+ EL = EL or false
+ if EL then EL = '\\\\' else EL = '' end
+ return pl.array2d.reduce2(_1..EL.._2, _1..'&'.._2, M)..EL
+end
+
+
+local function parse_numpy1d(i1, i2, iS)
+ i1 = tonumber(i1)
+ i2 = tonumber(i2)
+ if iS == ':' then
+ if i1 == nil then i1 = 1 end
+ if i2 == nil then i2 = -1 end
else
- tex.print('\\PackageError{luadefs}{Command '..cs..' already defined}{}')
+ if i1 == nil then
+ i1 = 1
+ i2 = -1
+ else
+ i2 = i1
+ end
end
+ return i1, i2
end
-local function deccmd(cs, def)
- if def == nil then
- prvcmd(cs, '\\PackageError{luadefs}{Command "'..cs..'" was declared and used but, not set}{}')
- else
- prvcmd(cs, def)
+function pl.array2d.parse_numpy2d_str(s)
+ s = s:gsub('%s+', '')
+ _, _, i1, iS, i2, j1, jS, j2 = string.find(s, "(%-?%d*)(:?)(%-?%d*),?(%-?%d*)(:?)(%-?%d*)")
+ i1, i2 = parse_numpy1d(i1, i2, iS)
+ j1, j2 = parse_numpy1d(j1, j2, jS)
+ return i1, j1, i2, j2
+end
+
+
+local _parse_range = pl.clone_function(pl.array2d.parse_range)
+
+function pl.array2d.parse_range(s) -- edit parse range to do numpy string if no letter passed
+ pl.utils.assert_arg(1,s,'string')
+ if not s:find'%a' then
+ return pl.array2d.parse_numpy2d_str(s)
end
+ return _parse_range(s)
end
--- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+
+
+
+if not __PL_NO_GLOBALS__ then
+ -- iterators
+ kpairs = pl.utils.kpairs
+ npairs = pl.utils.npairs
+ --enum = utils.enum
+
+ for k,v in pairs(pl.tablex) do -- extend the table table to contain tablex functions
+ _G['table'][k] = v
+ end
+
+ hasval = pl.hasval
+ COMP = pl.COMP
+
+ -- shortcuts
+-- http://stevedonovan.github.io/Penlight/api/libraries/pl.utils.html
+ pl.writefile = pl.utils.writefile
+ pl.readfile = pl.utils.readfile
+ pl.readlines = pl.utils.readfile
+ pl.filterfiles = pl.utils.filterfiles
+
+ pl.a2 = pl.array2d
+ pl.tbl = pl.tablex
+
+
+ for k,v in pairs(pl.tex) do -- make tex functions global
+ _G[k] = v
+ end
+
+ --_xTrue = pl.tex._xTrue
+ --_xFalse = pl.tex._xFalse
+ --_xNoValue = pl.tex._xNoValue
+ --
+ --prt = pl.tex.prt
+ --prtn = pl.tex.prtn
+ --wrt = pl.tex.wrt
+ --wrtn = pl.tex.wrtn
+ --
+ --prtl = pl.tex.prtl
+ --prtt = pl.tex.prtt
+ --
+ --help_wrt = pl.tex.help_wrt
+ --prt_array2d = pl.tex.prt_array2d
+ --
+ --pkgwarn = pl.tex.pkgwarn
+ --pkgerror = pl.tex.pkgerror
+ --
+ --defcmd = pl.tex.defcmd
+ --prvcmd = pl.tex.prvcmd
+ --newcmd = pl.tex.newcmd
+ --renewcmd = pl.tex.renewcmd
+ --deccmd = pl.tex.deccmd
+ --
+ --_NumBkts = pl.tex._NumBkts
+ --opencmd = pl.tex.opencmd
+ --reset_bkt_cnt = pl.tex.reset_bkt_cnt
+ --add_bkt_cnt = pl.tex.add_bkt_cnt
+ --close_bkt_cnt = pl.tex.close_bkt_cnt
+
+end
+
+
+
+
+
+-- graveyard
+
+
+-- luakeys parses individual keys as ipairs, this changes the list to a pure map
+--function pl.luakeystomap(t)
+-- local t_new = {}
+-- for k, v in pairs(t) do
+-- if type(k) == 'number' then
+-- t_new[v] = true
+-- else
+-- t_new[k] = v
+-- end
+-- end
+-- return t_new
+--end
+--if luakeys then -- if luakeys is already loaded
+-- function luakeys.parseN(s, ...)
+-- local t = luakeys.parse(s,...)
+-- t = pl.luakeystomap(t)
+-- return t
+-- end
+--end
+-- might not be needed
+
+
+ --local func = check_func(func)
+--local function check_func(func) -- check if a function is a PE, if so, make it a function
+-- if type(func) ~= 'function' then
+-- return I(func)
+-- end
+-- return func
+--end
+
+-- -- -- -- -- -- --
+-- -- -- -- functions below extend the array2d module
+
+
+--function pl.array2d.map_slice1(func, L, i1, i2) -- map a function to a slice of an array, can use PlcExpr
+-- i2 = i2 or i1
+-- local len = #L
+-- i1 = check_index(i1, len)
+-- i2 = check_index(i2, len)
+-- func = check_func(func)
+-- for i in pl.seq.range(i1,i2) do
+-- L[i] = func(L[i])
+-- end
+-- return L
+--end
+
+ -- used this below when iter was not working..
+ --i1, j1, i2, j2 = check_slice(M, i1, j1, i2, j2)
+ --for i in pl.seq.range(i1,i2) do
+ -- for j in pl.seq.range(j1,j2) do
+ --end
+ -- penlight may have fixed this
+--local function check_index(ij, rc) -- converts array index to positive value if negative
+-- if type(ij) ~= 'number' then
+-- return 1
+-- else
+-- if ij < 0 then
+-- ij = rc + ij + 1
+-- elseif ij > rc then
+-- ij = rc
+-- elseif ij == 0 then
+-- ij = 1
+-- end
+-- return ij
+-- end
+--end
+--local function check_slice(M, i1, j1, i2, j2) -- ensure a slice is valid; i.e. all positive numbers
+-- r, c = pl.array2d.size(M)
+-- i1 = check_index(i1 or 1, r)
+-- i2 = check_index(i2 or r, r)
+-- j1 = check_index(j1 or 1, c)
+-- j2 = check_index(j2 or c, c)
+-- return i1, j1, i2, j2
+--end
+