summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2021-09-20 03:02:23 +0000
committerNorbert Preining <norbert@preining.info>2021-09-20 03:02:23 +0000
commitc9fcf97bc045a9d020980c33b6c7c1ee5aedef99 (patch)
treebf74a43623e081cad4b72a752730aeff86b7c5cf
parent996c9b239bde82dc3ef3e5b69cd525e87fdb61a8 (diff)
CTAN sync 202109200302
-rw-r--r--FILES.byname577
-rw-r--r--FILES.byname.gzbin2387287 -> 2388058 bytes
-rw-r--r--FILES.last07days513
-rw-r--r--indexing/makeindexk/ChangeLog11
-rw-r--r--indexing/makeindexk/configure.ac2
-rwxr-xr-xindexing/makeindexk/configure.texk22
-rw-r--r--indexing/makeindexk/mkind.h5
-rw-r--r--indexing/makeindexk/scanid.c14
-rw-r--r--indexing/makeindexk/scanid.h6
-rw-r--r--indexing/makeindexk/scanst.c5
-rwxr-xr-xindexing/makeindexk/tests/makeindex.test37
-rw-r--r--indexing/makeindexk/tests/pprec0.ist11
-rw-r--r--indexing/makeindexk/tests/pprec1.ist11
-rw-r--r--indexing/makeindexk/tests/pprec2.ist11
-rw-r--r--indexing/makeindexk/tests/pprec3.ist11
-rw-r--r--indexing/makeindexk/tests/pprec4.ist11
-rw-r--r--indexing/makeindexk/tests/pprecA-0.ind5
-rw-r--r--indexing/makeindexk/tests/pprecA-1.ind5
-rw-r--r--indexing/makeindexk/tests/pprecA-2.ind5
-rw-r--r--indexing/makeindexk/tests/pprecA.idx9
-rw-r--r--indexing/makeindexk/tests/pprecB-3.ind5
-rw-r--r--indexing/makeindexk/tests/pprecB-4.ind5
-rw-r--r--indexing/makeindexk/tests/pprecB.idx9
-rw-r--r--indexing/makeindexk/tests/range.idx126
-rw-r--r--indexing/makeindexk/tests/range1.ind25
-rw-r--r--indexing/makeindexk/tests/range1.ist3
-rw-r--r--indexing/makeindexk/tests/range2.ind26
-rw-r--r--indexing/makeindexk/tests/range2.ist4
-rw-r--r--indexing/makeindexk/tests/range3.ind25
-rw-r--r--indexing/makeindexk/tests/range3.ist4
-rw-r--r--info/dtk-bibliography/README.md2
-rw-r--r--info/dtk-bibliography/dtk-bibliography.bib116
-rw-r--r--info/dtk-bibliography/dtk-bibliography.pdfbin366387 -> 381060 bytes
-rw-r--r--info/dtk-bibliography/dtk-bibliography.tex4
-rw-r--r--info/dtk-bibliography/dtk-logos.sty169
-rw-r--r--language/korean/pmhanguljamo/README.md8
-rw-r--r--language/korean/pmhanguljamo/doc/pmhanguljamo-doc.pdfbin135349 -> 128727 bytes
-rw-r--r--language/korean/pmhanguljamo/doc/pmhanguljamo-doc.tex82
-rw-r--r--language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.pdfbin531979 -> 533270 bytes
-rw-r--r--language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.tex15
-rw-r--r--language/korean/pmhanguljamo/tex/pmhanguljamo.sty13
-rw-r--r--macros/latex/contrib/schooldocs/schooldocs.dtx121
-rw-r--r--macros/latex/contrib/schooldocs/schooldocs.ins4
-rw-r--r--macros/latex/contrib/schooldocs/schooldocs.pdfbin567470 -> 568734 bytes
-rw-r--r--macros/latex/contrib/tiscreen/README5
-rw-r--r--macros/latex/contrib/tiscreen/doc/tiscreen-doc.pdfbin133505 -> 150094 bytes
-rw-r--r--macros/latex/contrib/tiscreen/doc/tiscreen-doc.tex58
-rw-r--r--macros/latex/contrib/tiscreen/tex/tiscreen.sty46
-rw-r--r--macros/luatex/generic/luakeys/luakeys-debug.sty21
-rw-r--r--macros/luatex/generic/luakeys/luakeys-debug.tex35
-rw-r--r--macros/luatex/generic/luakeys/luakeys.lua114
-rw-r--r--macros/luatex/generic/luakeys/luakeys.pdfbin140586 -> 153582 bytes
-rw-r--r--macros/luatex/generic/luakeys/luakeys.tex121
-rw-r--r--macros/luatex/generic/penlight/README.md29
-rw-r--r--macros/luatex/generic/penlight/penlight.pdfbin0 -> 20067 bytes
-rw-r--r--macros/luatex/generic/penlight/penlight.sty36
-rw-r--r--macros/luatex/generic/penlight/penlight.tex84
-rw-r--r--macros/luatex/generic/penlight/pl/Date.lua675
-rw-r--r--macros/luatex/generic/penlight/pl/LICENSE-pl.md25
-rw-r--r--macros/luatex/generic/penlight/pl/List.lua566
-rw-r--r--macros/luatex/generic/penlight/pl/Map.lua120
-rw-r--r--macros/luatex/generic/penlight/pl/MultiMap.lua54
-rw-r--r--macros/luatex/generic/penlight/pl/OrderedMap.lua167
-rw-r--r--macros/luatex/generic/penlight/pl/README-pl.md127
-rw-r--r--macros/luatex/generic/penlight/pl/Set.lua222
-rw-r--r--macros/luatex/generic/penlight/pl/app.lua297
-rw-r--r--macros/luatex/generic/penlight/pl/array2d.lua579
-rw-r--r--macros/luatex/generic/penlight/pl/class.lua265
-rw-r--r--macros/luatex/generic/penlight/pl/compat.lua250
-rw-r--r--macros/luatex/generic/penlight/pl/comprehension.lua285
-rw-r--r--macros/luatex/generic/penlight/pl/config.lua207
-rw-r--r--macros/luatex/generic/penlight/pl/data.lua654
-rw-r--r--macros/luatex/generic/penlight/pl/dir.lua524
-rw-r--r--macros/luatex/generic/penlight/pl/file.lua55
-rw-r--r--macros/luatex/generic/penlight/pl/func.lua393
-rw-r--r--macros/luatex/generic/penlight/pl/import_into.lua91
-rw-r--r--macros/luatex/generic/penlight/pl/init.lua11
-rw-r--r--macros/luatex/generic/penlight/pl/input.lua171
-rw-r--r--macros/luatex/generic/penlight/pl/lapp.lua451
-rw-r--r--macros/luatex/generic/penlight/pl/lexer.lua515
-rw-r--r--macros/luatex/generic/penlight/pl/luabalanced.lua264
-rw-r--r--macros/luatex/generic/penlight/pl/operator.lua209
-rw-r--r--macros/luatex/generic/penlight/pl/path.lua575
-rw-r--r--macros/luatex/generic/penlight/pl/permute.lua196
-rw-r--r--macros/luatex/generic/penlight/pl/pretty.lua437
-rw-r--r--macros/luatex/generic/penlight/pl/seq.lua544
-rw-r--r--macros/luatex/generic/penlight/pl/sip.lua337
-rw-r--r--macros/luatex/generic/penlight/pl/strict.lua138
-rw-r--r--macros/luatex/generic/penlight/pl/stringio.lua158
-rw-r--r--macros/luatex/generic/penlight/pl/stringx.lua594
-rw-r--r--macros/luatex/generic/penlight/pl/tablex.lua999
-rw-r--r--macros/luatex/generic/penlight/pl/template.lua202
-rw-r--r--macros/luatex/generic/penlight/pl/test.lua164
-rw-r--r--macros/luatex/generic/penlight/pl/text.lua246
-rw-r--r--macros/luatex/generic/penlight/pl/types.lua183
-rw-r--r--macros/luatex/generic/penlight/pl/url.lua51
-rw-r--r--macros/luatex/generic/penlight/pl/utils.lua777
-rw-r--r--macros/luatex/generic/penlight/pl/xml.lua776
-rw-r--r--support/light-latex-make/README.md22
-rw-r--r--support/light-latex-make/llmk.12
-rwxr-xr-xsupport/light-latex-make/llmk.lua7
-rw-r--r--support/light-latex-make/llmk.pdfbin99340 -> 99421 bytes
-rw-r--r--support/light-latex-make/llmk.tex11
-rw-r--r--systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl3
-rw-r--r--systems/texlive/tlnet/tlpkg/texlive.tlpdb77
-rw-r--r--systems/texlive/tlnet/tlpkg/texlive.tlpdb.md52
-rw-r--r--systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha5122
-rw-r--r--systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc14
-rw-r--r--systems/texlive/tlnet/tlpkg/texlive.tlpdb.xzbin2312048 -> 2311436 bytes
-rw-r--r--systems/win32/miktex/tm/packages/next/pr.ini8
-rw-r--r--systems/win32/miktex/tm/packages/pr.ini8
-rw-r--r--timestamp2
112 files changed, 15486 insertions, 812 deletions
diff --git a/FILES.byname b/FILES.byname
index 3efa759bbd..baccaddfac 100644
--- a/FILES.byname
+++ b/FILES.byname
@@ -5105,7 +5105,7 @@
1990/10/02 | 5440 | dviware/vutex/vaxvms.hlp
1990/10/02 | 812 | dviware/vutex/vaxvms.ins
1990/10/02 | 127576 | dviware/vutex/vutex.web
-2021/09/18 | 65639 | FILES.last07days
+2021/09/19 | 70048 | FILES.last07days
2021/07/24 | 1139410 | fonts/aboensis/Aboensis.glyphs
2021/07/24 | 18907382 | fonts/aboensis/aboensis.pdf
2021/07/24 | 137860 | fonts/aboensis/Aboensis-Regular.otf
@@ -161641,13 +161641,14 @@
2021/01/09 | 86520 | info/docsurvey/docsurvey.tex
2021/01/09 | 804 | info/docsurvey/README.txt
2021/01/09 | 359183 | info/docsurvey.zip
-2021/06/03 | 1869 | info/dtk-bibliography/dtk-authoryear.bbx
-2021/06/03 | 520 | info/dtk-bibliography/dtk-authoryear.dbx
-2021/06/03 | 519245 | info/dtk-bibliography/dtk-bibliography.bib
-2021/06/03 | 366387 | info/dtk-bibliography/dtk-bibliography.pdf
-2021/06/03 | 862 | info/dtk-bibliography/dtk-bibliography.tex
-2021/06/03 | 654 | info/dtk-bibliography/README.md
-2021/06/03 | 423573 | info/dtk-bibliography.zip
+2021/09/19 | 1869 | info/dtk-bibliography/dtk-authoryear.bbx
+2021/09/19 | 520 | info/dtk-bibliography/dtk-authoryear.dbx
+2021/09/19 | 522146 | info/dtk-bibliography/dtk-bibliography.bib
+2021/09/19 | 381060 | info/dtk-bibliography/dtk-bibliography.pdf
+2021/09/19 | 863 | info/dtk-bibliography/dtk-bibliography.tex
+2021/09/19 | 4834 | info/dtk-bibliography/dtk-logos.sty
+2021/09/19 | 657 | info/dtk-bibliography/README.md
+2021/09/19 | 436311 | info/dtk-bibliography.zip
2007/08/06 | 1426 | info/dtxgallery/conditional-code.dtx
2007/08/06 | 47941 | info/dtxgallery/conditional-code.pdf
2007/08/06 | 6401 | info/dtxgallery/dtxgallery.dtx
@@ -193514,15 +193515,15 @@
2014/10/26 | 675 | language/korean/kotex-utils/utf8-lang.xdy
2015/02/04 | 611539 | language/korean/kotex-utils/utf8.xdy
2015/10/27 | 148484 | language/korean/kotex-utils.zip
-2020/01/29 | 143726 | language/korean/pmhanguljamo/doc/munjangganghwa.jpg
-2020/03/15 | 135349 | language/korean/pmhanguljamo/doc/pmhanguljamo-doc.pdf
-2020/03/15 | 23786 | language/korean/pmhanguljamo/doc/pmhanguljamo-doc.tex
-2020/03/16 | 531979 | language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.pdf
-2020/03/16 | 51098 | language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.tex
-2020/02/04 | 1280 | language/korean/pmhanguljamo/README.md
-2020/03/15 | 12851 | language/korean/pmhanguljamo/tex/pmhanguljamo-rrk.sty
-2020/03/15 | 13640 | language/korean/pmhanguljamo/tex/pmhanguljamo.sty
-2020/03/17 | 833472 | language/korean/pmhanguljamo.zip
+2021/09/19 | 143726 | language/korean/pmhanguljamo/doc/munjangganghwa.jpg
+2021/09/19 | 128727 | language/korean/pmhanguljamo/doc/pmhanguljamo-doc.pdf
+2021/09/19 | 24362 | language/korean/pmhanguljamo/doc/pmhanguljamo-doc.tex
+2021/09/19 | 533270 | language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.pdf
+2021/09/19 | 51597 | language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.tex
+2021/09/19 | 1319 | language/korean/pmhanguljamo/README.md
+2021/09/19 | 12851 | language/korean/pmhanguljamo/tex/pmhanguljamo-rrk.sty
+2021/09/19 | 13750 | language/korean/pmhanguljamo/tex/pmhanguljamo.sty
+2021/09/19 | 828771 | language/korean/pmhanguljamo.zip
1995/12/03 | 220 | language/korean/README.CJK
2007/03/20 | 23023 | language/latin/latin.dtx
2000/09/26 | 2978 | language/latin/latin.ins
@@ -221270,11 +221271,11 @@
2019/08/01 | 243693 | macros/latex/contrib/schedule/schedule.pdf
2019/08/01 | 250009 | macros/latex/contrib/schedule.zip
2020/07/13 | 902 | macros/latex/contrib/schooldocs/README.md
-2020/07/13 | 37252 | macros/latex/contrib/schooldocs/schooldocs.dtx
+2021/09/18 | 38020 | macros/latex/contrib/schooldocs/schooldocs.dtx
2020/07/13 | 348408 | macros/latex/contrib/schooldocs/schooldocs-examples.pdf
-2020/01/01 | 1987 | macros/latex/contrib/schooldocs/schooldocs.ins
-2020/07/13 | 567470 | macros/latex/contrib/schooldocs/schooldocs.pdf
-2020/07/14 | 913918 | macros/latex/contrib/schooldocs.zip
+2021/09/18 | 1997 | macros/latex/contrib/schooldocs/schooldocs.ins
+2021/09/18 | 568734 | macros/latex/contrib/schooldocs/schooldocs.pdf
+2021/09/19 | 915258 | macros/latex/contrib/schooldocs.zip
2021/08/07 | 866 | macros/latex/contrib/schule/data/xsim-style/xsim.style.schule-binnen.code.tex
2021/08/07 | 1786 | macros/latex/contrib/schule/data/xsim-style/xsim.style.schule-default.code.tex
2021/08/07 | 1200 | macros/latex/contrib/schule/data/xsim-style/xsim.style.schule-keinenummer.code.tex
@@ -224859,11 +224860,11 @@
1994/05/15 | 7262 | macros/latex/contrib/timing/timing.sty
1994/05/17 | 9458 | macros/latex/contrib/timing/timing.tex
2005/06/28 | 106439 | macros/latex/contrib/timing.zip
-2021/07/22 | 133505 | macros/latex/contrib/tiscreen/doc/tiscreen-doc.pdf
-2021/07/22 | 3334 | macros/latex/contrib/tiscreen/doc/tiscreen-doc.tex
-2021/07/22 | 724 | macros/latex/contrib/tiscreen/README
-2021/07/22 | 2058 | macros/latex/contrib/tiscreen/tex/tiscreen.sty
-2021/07/22 | 135483 | macros/latex/contrib/tiscreen.zip
+2021/09/18 | 150094 | macros/latex/contrib/tiscreen/doc/tiscreen-doc.pdf
+2021/09/18 | 4414 | macros/latex/contrib/tiscreen/doc/tiscreen-doc.tex
+2021/09/18 | 763 | macros/latex/contrib/tiscreen/README
+2021/09/18 | 3110 | macros/latex/contrib/tiscreen/tex/tiscreen.sty
+2021/09/19 | 152528 | macros/latex/contrib/tiscreen.zip
2013/04/11 | 447 | macros/latex/contrib/titlecaps/README
2015/01/30 | 272789 | macros/latex/contrib/titlecaps/titlecaps.pdf
2015/01/30 | 15251 | macros/latex/contrib/titlecaps/titlecaps.sty
@@ -233296,13 +233297,13 @@
2018/06/19 | 922 | macros/luatex/generic/kanaparser/kanaparser.tex
2018/06/19 | 2005 | macros/luatex/generic/kanaparser/README.md
2018/06/19 | 378065 | macros/luatex/generic/kanaparser.zip
-2021/01/18 | 1075 | macros/luatex/generic/luakeys/luakeys-debug.sty
-2021/01/18 | 880 | macros/luatex/generic/luakeys/luakeys-debug.tex
-2021/01/18 | 13604 | macros/luatex/generic/luakeys/luakeys.lua
-2021/01/18 | 140586 | macros/luatex/generic/luakeys/luakeys.pdf
-2021/01/18 | 11321 | macros/luatex/generic/luakeys/luakeys.tex
-2021/01/18 | 1271 | macros/luatex/generic/luakeys/README.md
-2021/01/18 | 145359 | macros/luatex/generic/luakeys.zip
+2021/09/19 | 758 | macros/luatex/generic/luakeys/luakeys-debug.sty
+2021/09/19 | 1618 | macros/luatex/generic/luakeys/luakeys-debug.tex
+2021/09/19 | 14658 | macros/luatex/generic/luakeys/luakeys.lua
+2021/09/19 | 153582 | macros/luatex/generic/luakeys/luakeys.pdf
+2021/09/19 | 13465 | macros/luatex/generic/luakeys/luakeys.tex
+2021/09/19 | 1271 | macros/luatex/generic/luakeys/README.md
+2021/09/19 | 159022 | macros/luatex/generic/luakeys.zip
2020/01/09 | 18092 | macros/luatex/generic/lualibs/LICENSE
2021/05/21 | 131865 | macros/luatex/generic/lualibs/lualibs-basic-merged.lua
2020/01/09 | 1850 | macros/luatex/generic/lualibs/lualibs-boolean.lua
@@ -233697,6 +233698,52 @@
2020/10/23 | 281887 | macros/luatex/generic/nodetree/nodetree.pdf
2020/10/23 | 4029 | macros/luatex/generic/nodetree/README.md
2020/10/23 | 289509 | macros/luatex/generic/nodetree.zip
+2021/09/19 | 20067 | macros/luatex/generic/penlight/penlight.pdf
+2021/09/19 | 1550 | macros/luatex/generic/penlight/penlight.sty
+2021/09/19 | 3376 | macros/luatex/generic/penlight/penlight.tex
+2021/09/12 | 10587 | macros/luatex/generic/penlight/pl/app.lua
+2021/09/12 | 16021 | macros/luatex/generic/penlight/pl/array2d.lua
+2021/09/12 | 8363 | macros/luatex/generic/penlight/pl/class.lua
+2021/09/12 | 8812 | macros/luatex/generic/penlight/pl/compat.lua
+2021/09/12 | 9062 | macros/luatex/generic/penlight/pl/comprehension.lua
+2021/09/12 | 6734 | macros/luatex/generic/penlight/pl/config.lua
+2021/09/12 | 20235 | macros/luatex/generic/penlight/pl/data.lua
+2021/09/12 | 18511 | macros/luatex/generic/penlight/pl/Date.lua
+2021/09/12 | 17610 | macros/luatex/generic/penlight/pl/dir.lua
+2021/09/12 | 1503 | macros/luatex/generic/penlight/pl/file.lua
+2021/09/12 | 11115 | macros/luatex/generic/penlight/pl/func.lua
+2021/09/12 | 2960 | macros/luatex/generic/penlight/pl/import_into.lua
+2021/09/12 | 432 | macros/luatex/generic/penlight/pl/init.lua
+2021/09/12 | 5156 | macros/luatex/generic/penlight/pl/input.lua
+2021/09/12 | 14827 | macros/luatex/generic/penlight/pl/lapp.lua
+2021/09/12 | 15421 | macros/luatex/generic/penlight/pl/lexer.lua
+2021/09/18 | 1114 | macros/luatex/generic/penlight/pl/LICENSE-pl.md
+2021/09/12 | 15792 | macros/luatex/generic/penlight/pl/List.lua
+2021/09/12 | 7985 | macros/luatex/generic/penlight/pl/luabalanced.lua
+2021/09/12 | 2786 | macros/luatex/generic/penlight/pl/Map.lua
+2021/09/12 | 1222 | macros/luatex/generic/penlight/pl/MultiMap.lua
+2021/09/12 | 4249 | macros/luatex/generic/penlight/pl/operator.lua
+2021/09/12 | 4506 | macros/luatex/generic/penlight/pl/OrderedMap.lua
+2021/09/12 | 16749 | macros/luatex/generic/penlight/pl/path.lua
+2021/09/12 | 5146 | macros/luatex/generic/penlight/pl/permute.lua
+2021/09/12 | 13121 | macros/luatex/generic/penlight/pl/pretty.lua
+2021/09/18 | 5642 | macros/luatex/generic/penlight/pl/README-pl.md
+2021/09/12 | 14502 | macros/luatex/generic/penlight/pl/seq.lua
+2021/09/12 | 5359 | macros/luatex/generic/penlight/pl/Set.lua
+2021/09/12 | 10296 | macros/luatex/generic/penlight/pl/sip.lua
+2021/09/12 | 4429 | macros/luatex/generic/penlight/pl/strict.lua
+2021/09/12 | 3774 | macros/luatex/generic/penlight/pl/stringio.lua
+2021/09/12 | 17161 | macros/luatex/generic/penlight/pl/stringx.lua
+2021/09/12 | 30263 | macros/luatex/generic/penlight/pl/tablex.lua
+2021/09/12 | 7652 | macros/luatex/generic/penlight/pl/template.lua
+2021/09/12 | 5146 | macros/luatex/generic/penlight/pl/test.lua
+2021/09/12 | 7366 | macros/luatex/generic/penlight/pl/text.lua
+2021/09/12 | 5828 | macros/luatex/generic/penlight/pl/types.lua
+2021/09/12 | 1196 | macros/luatex/generic/penlight/pl/url.lua
+2021/09/12 | 25196 | macros/luatex/generic/penlight/pl/utils.lua
+2021/09/12 | 25249 | macros/luatex/generic/penlight/pl/xml.lua
+2021/09/19 | 1300 | macros/luatex/generic/penlight/README.md
+2021/09/19 | 152964 | macros/luatex/generic/penlight.zip
2013/05/25 | 2280 | macros/luatex/generic/spelling/CHANGES
2012/11/26 | 19106 | macros/luatex/generic/spelling/LICENSE
2013/05/25 | 1606 | macros/luatex/generic/spelling/README
@@ -270058,16 +270105,16 @@
2000/07/20 | 649 | support/ligatex/mkinstalldirs
2000/11/23 | 1120 | support/ligatex/README
2012/09/29 | 53919 | support/ligatex.zip
-2021/08/15 | 1091 | support/light-latex-make/LICENSE
-2021/08/15 | 2341 | support/light-latex-make/llmk.1
-2021/08/15 | 8161 | support/light-latex-make/llmk-doc.cls
-2021/08/15 | 1433 | support/light-latex-make/llmk-logo-code.tex
-2021/08/15 | 77263 | support/light-latex-make/llmk-logo.png
-2021/08/15 | 39416 | support/light-latex-make/llmk.lua
-2021/08/15 | 99340 | support/light-latex-make/llmk.pdf
-2021/08/15 | 36569 | support/light-latex-make/llmk.tex
-2021/08/15 | 9170 | support/light-latex-make/README.md
-2021/08/15 | 179891 | support/light-latex-make.zip
+2021/09/19 | 1091 | support/light-latex-make/LICENSE
+2021/09/19 | 2341 | support/light-latex-make/llmk.1
+2021/09/19 | 8161 | support/light-latex-make/llmk-doc.cls
+2021/09/19 | 1433 | support/light-latex-make/llmk-logo-code.tex
+2021/09/19 | 77263 | support/light-latex-make/llmk-logo.png
+2021/09/19 | 39513 | support/light-latex-make/llmk.lua
+2021/09/19 | 99421 | support/light-latex-make/llmk.pdf
+2021/09/19 | 36697 | support/light-latex-make/llmk.tex
+2021/09/19 | 9234 | support/light-latex-make/README.md
+2021/09/19 | 179998 | support/light-latex-make.zip
2001/12/02 | 18002 | support/lintex/COPYING
2002/09/25 | 6149 | support/lintex/cxx/cleandir.cxx
2001/01/22 | 652 | support/lintex/cxx/cleandir.hh
@@ -278626,8 +278673,8 @@
2019/02/28 | 386944 | systems/texlive/tlnet/archive/cleveref.doc.r47525.tar.xz
2019/02/28 | 28484 | systems/texlive/tlnet/archive/cleveref.r47525.tar.xz
2019/02/28 | 77476 | systems/texlive/tlnet/archive/cleveref.source.r47525.tar.xz
-2021/09/14 | 258104 | systems/texlive/tlnet/archive/clicks.doc.r60498.tar.xz
-2021/09/14 | 1552 | systems/texlive/tlnet/archive/clicks.r60498.tar.xz
+2021/09/19 | 260244 | systems/texlive/tlnet/archive/clicks.doc.r60539.tar.xz
+2021/09/19 | 1548 | systems/texlive/tlnet/archive/clicks.r60539.tar.xz
2019/02/28 | 161528 | systems/texlive/tlnet/archive/clipboard.doc.r47747.tar.xz
2019/02/28 | 1028 | systems/texlive/tlnet/archive/clipboard.r47747.tar.xz
2019/02/28 | 68320 | systems/texlive/tlnet/archive/clock.doc.r15878.tar.xz
@@ -280660,9 +280707,9 @@
2019/02/28 | 15012 | systems/texlive/tlnet/archive/exercise.source.r35417.tar.xz
2020/05/18 | 2216 | systems/texlive/tlnet/archive/exercises.r55188.tar.xz
2020/05/18 | 7728 | systems/texlive/tlnet/archive/exercises.source.r55188.tar.xz
-2021/08/31 | 179344 | systems/texlive/tlnet/archive/exesheet.doc.r60373.tar.xz
-2021/08/31 | 3672 | systems/texlive/tlnet/archive/exesheet.r60373.tar.xz
-2021/08/31 | 16360 | systems/texlive/tlnet/archive/exesheet.source.r60373.tar.xz
+2021/09/19 | 178108 | systems/texlive/tlnet/archive/exesheet.doc.r60537.tar.xz
+2021/09/19 | 3848 | systems/texlive/tlnet/archive/exesheet.r60537.tar.xz
+2021/09/19 | 16324 | systems/texlive/tlnet/archive/exesheet.source.r60537.tar.xz
2020/02/26 | 401556 | systems/texlive/tlnet/archive/exframe.doc.r53911.tar.xz
2020/02/26 | 10848 | systems/texlive/tlnet/archive/exframe.r53911.tar.xz
2020/02/26 | 43532 | systems/texlive/tlnet/archive/exframe.source.r53911.tar.xz
@@ -280674,9 +280721,9 @@
2021/07/08 | 443316 | systems/texlive/tlnet/archive/expkv-cs.doc.r59864.tar.xz
2021/07/08 | 5528 | systems/texlive/tlnet/archive/expkv-cs.r59864.tar.xz
2021/07/08 | 25540 | systems/texlive/tlnet/archive/expkv-cs.source.r59864.tar.xz
-2021/05/26 | 395756 | systems/texlive/tlnet/archive/expkv-def.doc.r59341.tar.xz
-2021/05/26 | 5024 | systems/texlive/tlnet/archive/expkv-def.r59341.tar.xz
-2021/05/26 | 17396 | systems/texlive/tlnet/archive/expkv-def.source.r59341.tar.xz
+2021/09/19 | 396880 | systems/texlive/tlnet/archive/expkv-def.doc.r60538.tar.xz
+2021/09/19 | 5060 | systems/texlive/tlnet/archive/expkv-def.r60538.tar.xz
+2021/09/19 | 17488 | systems/texlive/tlnet/archive/expkv-def.source.r60538.tar.xz
2021/07/17 | 476672 | systems/texlive/tlnet/archive/expkv.doc.r59949.tar.xz
2021/04/07 | 264172 | systems/texlive/tlnet/archive/expkv-opt.doc.r58772.tar.xz
2021/04/07 | 2908 | systems/texlive/tlnet/archive/expkv-opt.r58772.tar.xz
@@ -283506,9 +283553,9 @@
2021/04/13 | 1857504 | systems/texlive/tlnet/archive/luatex.i386-linux.r58850.tar.xz
2021/04/14 | 1449908 | systems/texlive/tlnet/archive/luatex.i386-netbsd.r58866.tar.xz
2021/04/13 | 1615968 | systems/texlive/tlnet/archive/luatex.i386-solaris.r58850.tar.xz
-2021/06/08 | 3203076 | systems/texlive/tlnet/archive/luatexja.doc.r59518.tar.xz
-2021/06/08 | 190152 | systems/texlive/tlnet/archive/luatexja.r59518.tar.xz
-2021/06/08 | 83288 | systems/texlive/tlnet/archive/luatexja.source.r59518.tar.xz
+2021/09/19 | 3204664 | systems/texlive/tlnet/archive/luatexja.doc.r60541.tar.xz
+2021/09/19 | 190612 | systems/texlive/tlnet/archive/luatexja.r60541.tar.xz
+2021/09/19 | 83276 | systems/texlive/tlnet/archive/luatexja.source.r60541.tar.xz
2021/07/12 | 349016 | systems/texlive/tlnet/archive/luatexko.doc.r59892.tar.xz
2021/07/12 | 23432 | systems/texlive/tlnet/archive/luatexko.r59892.tar.xz
2021/04/12 | 12912 | systems/texlive/tlnet/archive/luatex.r58822.tar.xz
@@ -285147,8 +285194,8 @@
2019/12/11 | 342644 | systems/texlive/tlnet/archive/pdfescape.doc.r53082.tar.xz
2019/12/11 | 4264 | systems/texlive/tlnet/archive/pdfescape.r53082.tar.xz
2019/12/11 | 8308 | systems/texlive/tlnet/archive/pdfescape.source.r53082.tar.xz
-2021/05/15 | 625096 | systems/texlive/tlnet/archive/pdfextra.doc.r59204.tar.xz
-2021/05/15 | 22320 | systems/texlive/tlnet/archive/pdfextra.r59204.tar.xz
+2021/09/19 | 644568 | systems/texlive/tlnet/archive/pdfextra.doc.r60542.tar.xz
+2021/09/19 | 22584 | systems/texlive/tlnet/archive/pdfextra.r60542.tar.xz
2019/11/19 | 340 | systems/texlive/tlnet/archive/pdfjam.aarch64-linux.r52858.tar.xz
2019/11/19 | 340 | systems/texlive/tlnet/archive/pdfjam.amd64-freebsd.r52858.tar.xz
2019/11/19 | 340 | systems/texlive/tlnet/archive/pdfjam.amd64-netbsd.r52858.tar.xz
@@ -286408,8 +286455,8 @@
2019/02/28 | 23544 | systems/texlive/tlnet/archive/rcs.source.r15878.tar.xz
2021/01/16 | 207536 | systems/texlive/tlnet/archive/readablecv.doc.r57433.tar.xz
2021/01/16 | 3896 | systems/texlive/tlnet/archive/readablecv.r57433.tar.xz
-2021/08/09 | 504052 | systems/texlive/tlnet/archive/readarray.doc.r60193.tar.xz
-2021/08/09 | 5276 | systems/texlive/tlnet/archive/readarray.r60193.tar.xz
+2021/09/19 | 506352 | systems/texlive/tlnet/archive/readarray.doc.r60540.tar.xz
+2021/09/19 | 5492 | systems/texlive/tlnet/archive/readarray.r60540.tar.xz
2020/09/08 | 175812 | systems/texlive/tlnet/archive/realboxes.doc.r56291.tar.xz
2020/09/08 | 3028 | systems/texlive/tlnet/archive/realboxes.r56291.tar.xz
2020/09/08 | 7416 | systems/texlive/tlnet/archive/realboxes.source.r56291.tar.xz
@@ -287778,8 +287825,8 @@
2020/03/24 | 280 | systems/texlive/tlnet/archive/texlive-cz.r54496.tar.xz
2021/03/23 | 1554012 | systems/texlive/tlnet/archive/texlive-de.doc.r58648.tar.xz
2021/03/23 | 272 | systems/texlive/tlnet/archive/texlive-de.r58648.tar.xz
-2021/07/26 | 1939716 | systems/texlive/tlnet/archive/texlive-en.doc.r60070.tar.xz
-2021/07/26 | 272 | systems/texlive/tlnet/archive/texlive-en.r60070.tar.xz
+2021/09/19 | 1939872 | systems/texlive/tlnet/archive/texlive-en.doc.r60536.tar.xz
+2021/09/19 | 276 | systems/texlive/tlnet/archive/texlive-en.r60536.tar.xz
2021/03/09 | 3667820 | systems/texlive/tlnet/archive/texlive-es.doc.r58221.tar.xz
2021/03/09 | 276 | systems/texlive/tlnet/archive/texlive-es.r58221.tar.xz
2021/03/22 | 1379000 | systems/texlive/tlnet/archive/texlive-fr.doc.r58579.tar.xz
@@ -287833,7 +287880,7 @@
2020/05/17 | 592 | systems/texlive/tlnet/archive/texlive-scripts.amd64-freebsd.r55172.tar.xz
2020/05/17 | 588 | systems/texlive/tlnet/archive/texlive-scripts.amd64-netbsd.r55172.tar.xz
2020/05/17 | 592 | systems/texlive/tlnet/archive/texlive-scripts.armhf-linux.r55172.tar.xz
-2021/09/18 | 400152 | systems/texlive/tlnet/archive/texlive-scripts.doc.r60525.tar.xz
+2021/09/19 | 400072 | systems/texlive/tlnet/archive/texlive-scripts.doc.r60532.tar.xz
2020/01/27 | 916 | systems/texlive/tlnet/archive/texlive-scripts-extra.aarch64-linux.r53577.tar.xz
2020/01/27 | 924 | systems/texlive/tlnet/archive/texlive-scripts-extra.amd64-freebsd.r53577.tar.xz
2020/01/27 | 916 | systems/texlive/tlnet/archive/texlive-scripts-extra.amd64-netbsd.r53577.tar.xz
@@ -287857,7 +287904,7 @@
2020/05/17 | 588 | systems/texlive/tlnet/archive/texlive-scripts.i386-linux.r55172.tar.xz
2020/05/17 | 592 | systems/texlive/tlnet/archive/texlive-scripts.i386-netbsd.r55172.tar.xz
2020/05/17 | 588 | systems/texlive/tlnet/archive/texlive-scripts.i386-solaris.r55172.tar.xz
-2021/09/18 | 108296 | systems/texlive/tlnet/archive/texlive-scripts.r60525.tar.xz
+2021/09/19 | 108292 | systems/texlive/tlnet/archive/texlive-scripts.r60532.tar.xz
2021/02/25 | 572 | systems/texlive/tlnet/archive/texlive-scripts.universal-darwin.r57908.tar.xz
2021/04/12 | 36308 | systems/texlive/tlnet/archive/texlive-scripts.win32.r58829.tar.xz
2020/05/17 | 588 | systems/texlive/tlnet/archive/texlive-scripts.x86_64-cygwin.r55172.tar.xz
@@ -289587,16 +289634,16 @@
2020/07/18 | 47816 | systems/texlive/tlnet/archive/zztex.doc.r55862.tar.xz
2020/07/18 | 101724 | systems/texlive/tlnet/archive/zztex.r55862.tar.xz
2021/05/02 | 105152 | systems/texlive/tlnet/install-tl
-2021/09/18 | 6811635 | systems/texlive/tlnet/install-tl-unx.tar.gz
-2021/09/18 | 152 | systems/texlive/tlnet/install-tl-unx.tar.gz.sha512
-2021/09/18 | 455 | systems/texlive/tlnet/install-tl-unx.tar.gz.sha512.asc
+2021/09/19 | 6811631 | systems/texlive/tlnet/install-tl-unx.tar.gz
+2021/09/19 | 152 | systems/texlive/tlnet/install-tl-unx.tar.gz.sha512
+2021/09/19 | 455 | systems/texlive/tlnet/install-tl-unx.tar.gz.sha512.asc
2021/04/11 | 4945 | systems/texlive/tlnet/install-tl-windows.bat
-2021/09/18 | 20877598 | systems/texlive/tlnet/install-tl-windows.exe
-2021/09/18 | 153 | systems/texlive/tlnet/install-tl-windows.exe.sha512
-2021/09/18 | 455 | systems/texlive/tlnet/install-tl-windows.exe.sha512.asc
-2021/09/18 | 25629533 | systems/texlive/tlnet/install-tl.zip
-2021/09/18 | 145 | systems/texlive/tlnet/install-tl.zip.sha512
-2021/09/18 | 455 | systems/texlive/tlnet/install-tl.zip.sha512.asc
+2021/09/19 | 20875499 | systems/texlive/tlnet/install-tl-windows.exe
+2021/09/19 | 153 | systems/texlive/tlnet/install-tl-windows.exe.sha512
+2021/09/19 | 455 | systems/texlive/tlnet/install-tl-windows.exe.sha512.asc
+2021/09/19 | 25629531 | systems/texlive/tlnet/install-tl.zip
+2021/09/19 | 145 | systems/texlive/tlnet/install-tl.zip.sha512
+2021/09/19 | 455 | systems/texlive/tlnet/install-tl.zip.sha512.asc
2021/04/01 | 1229 | systems/texlive/tlnet/README.md
2021/04/01 | 0 | systems/texlive/tlnet/TEXLIVE_2021
2021/07/03 | 7181 | systems/texlive/tlnet/tlpkg/gpg/pubring.gpg
@@ -289606,7 +289653,7 @@
2021/07/03 | 1280 | systems/texlive/tlnet/tlpkg/gpg/trustdb.gpg
2021/07/02 | 49348 | systems/texlive/tlnet/tlpkg/installer/config.guess
2010/11/11 | 11651 | systems/texlive/tlnet/tlpkg/installer/COPYING.MinGW-runtime.txt
-2021/09/17 | 6834 | systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl
+2021/09/18 | 6845 | systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl
2020/04/15 | 223687 | systems/texlive/tlnet/tlpkg/installer/curl/curl-ca-bundle.crt
2020/04/15 | 2606592 | systems/texlive/tlnet/tlpkg/installer/curl/curl.exe
2017/04/16 | 2629 | systems/texlive/tlnet/tlpkg/installer/installer-options.txt
@@ -289648,12 +289695,12 @@
2021/05/16 | 20371 | systems/texlive/tlnet/tlpkg/TeXLive/TLCrypto.pm
2021/05/16 | 5402 | systems/texlive/tlnet/tlpkg/TeXLive/TLDownload.pm
2021/05/16 | 27555 | systems/texlive/tlnet/tlpkg/TeXLive/TLPaper.pm
-2021/09/18 | 16884922 | systems/texlive/tlnet/tlpkg/texlive.tlpdb
-2021/09/18 | 48 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.md5
+2021/09/19 | 16885163 | systems/texlive/tlnet/tlpkg/texlive.tlpdb
+2021/09/19 | 48 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.md5
2021/05/16 | 86511 | systems/texlive/tlnet/tlpkg/TeXLive/TLPDB.pm
-2021/09/18 | 144 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512
-2021/09/18 | 455 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc
-2021/09/18 | 2311652 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.xz
+2021/09/19 | 144 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512
+2021/09/19 | 455 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc
+2021/09/19 | 2312048 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.xz
2021/05/16 | 58801 | systems/texlive/tlnet/tlpkg/TeXLive/TLPOBJ.pm
2021/05/16 | 42084 | systems/texlive/tlnet/tlpkg/TeXLive/TLPSRC.pm
2021/05/16 | 16103 | systems/texlive/tlnet/tlpkg/TeXLive/TLTREE.pm
@@ -294571,7 +294618,7 @@
2020/10/03 | 318630 | systems/win32/miktex/tm/packages/filehook.tar.lzma
2012/12/04 | 924573 | systems/win32/miktex/tm/packages/fileinfo.tar.lzma
2011/09/21 | 281409 | systems/win32/miktex/tm/packages/filemod.tar.lzma
-2021/09/18 | 614598 | systems/win32/miktex/tm/packages/files.csv.lzma
+2021/09/19 | 614598 | systems/win32/miktex/tm/packages/files.csv.lzma
2001/05/14 | 5617 | systems/win32/miktex/tm/packages/finbib.tar.lzma
2015/10/20 | 44379 | systems/win32/miktex/tm/packages/findhyph.tar.lzma
2011/10/21 | 89323 | systems/win32/miktex/tm/packages/fink.tar.lzma
@@ -295959,9 +296006,9 @@
2018/01/12 | 157226 | systems/win32/miktex/tm/packages/miktex-zip-bin-x64.tar.lzma
2021/08/07 | 39000 | systems/win32/miktex/tm/packages/miktex-zlib-bin-2.9.tar.lzma
2021/08/07 | 43393 | systems/win32/miktex/tm/packages/miktex-zlib-bin-x64-2.9.tar.lzma
-2021/09/18 | 240935 | systems/win32/miktex/tm/packages/miktex-zzdb1-2.9.tar.lzma
-2021/09/18 | 1357611 | systems/win32/miktex/tm/packages/miktex-zzdb2-2.9.tar.lzma
-2021/09/18 | 1261208 | systems/win32/miktex/tm/packages/miktex-zzdb3-2.9.tar.lzma
+2021/09/19 | 240950 | systems/win32/miktex/tm/packages/miktex-zzdb1-2.9.tar.lzma
+2021/09/19 | 1356796 | systems/win32/miktex/tm/packages/miktex-zzdb2-2.9.tar.lzma
+2021/09/19 | 1261692 | systems/win32/miktex/tm/packages/miktex-zzdb3-2.9.tar.lzma
2021/08/07 | 13446 | systems/win32/miktex/tm/packages/miktex-zzip-bin-2.9.tar.lzma
2021/08/07 | 14327 | systems/win32/miktex/tm/packages/miktex-zzip-bin-x64-2.9.tar.lzma
2016/07/15 | 35914 | systems/win32/miktex/tm/packages/mil3.tar.lzma
@@ -296171,189 +296218,189 @@
2018/04/09 | 400132 | systems/win32/miktex/tm/packages/newunicodechar.tar.lzma
2011/09/19 | 101312 | systems/win32/miktex/tm/packages/newvbtm.tar.lzma
2021/03/17 | 232249 | systems/win32/miktex/tm/packages/newverbs.tar.lzma
-2021/09/18 | 614469 | systems/win32/miktex/tm/packages/next/files.csv.lzma
-2021/09/18 | 51113 | systems/win32/miktex/tm/packages/next/miktex-arctrl-bin-2.9.tar.lzma
-2021/09/18 | 56476 | systems/win32/miktex/tm/packages/next/miktex-arctrl-bin-x64-2.9.tar.lzma
-2021/09/18 | 975018 | systems/win32/miktex/tm/packages/next/miktex-asymptote-bin-2.9.tar.lzma
-2021/09/18 | 1077991 | systems/win32/miktex/tm/packages/next/miktex-asymptote-bin-x64-2.9.tar.lzma
-2021/09/18 | 29037 | systems/win32/miktex/tm/packages/next/miktex-autosp-bin-2.9.tar.lzma
-2021/09/18 | 31351 | systems/win32/miktex/tm/packages/next/miktex-autosp-bin-x64-2.9.tar.lzma
-2021/09/18 | 29841 | systems/win32/miktex/tm/packages/next/miktex-axohelp-bin-2.9.tar.lzma
-2021/09/18 | 34666 | systems/win32/miktex/tm/packages/next/miktex-axohelp-bin-x64-2.9.tar.lzma
-2021/09/18 | 50152 | systems/win32/miktex/tm/packages/next/miktex-bibarts-bin-2.9.tar.lzma
-2021/09/18 | 57548 | systems/win32/miktex/tm/packages/next/miktex-bibarts-bin-x64-2.9.tar.lzma
-2021/09/18 | 83540 | systems/win32/miktex/tm/packages/next/miktex-bibtex8bit-bin-2.9.tar.lzma
-2021/09/18 | 95403 | systems/win32/miktex/tm/packages/next/miktex-bibtex8bit-bin-x64-2.9.tar.lzma
-2021/09/18 | 67819 | systems/win32/miktex/tm/packages/next/miktex-bibtex-bin-2.9.tar.lzma
-2021/09/18 | 75792 | systems/win32/miktex/tm/packages/next/miktex-bibtex-bin-x64-2.9.tar.lzma
-2021/09/18 | 27197 | systems/win32/miktex/tm/packages/next/miktex-bzip2-bin-2.9.tar.lzma
-2021/09/18 | 30823 | systems/win32/miktex/tm/packages/next/miktex-bzip2-bin-x64-2.9.tar.lzma
-2021/09/18 | 330788 | systems/win32/miktex/tm/packages/next/miktex-cairo-bin-2.9.tar.lzma
-2021/09/18 | 392792 | systems/win32/miktex/tm/packages/next/miktex-cairo-bin-x64-2.9.tar.lzma
-2021/09/18 | 32673 | systems/win32/miktex/tm/packages/next/miktex-chktex-bin-2.9.tar.lzma
-2021/09/18 | 36630 | systems/win32/miktex/tm/packages/next/miktex-chktex-bin-x64-2.9.tar.lzma
-2021/09/18 | 51309 | systems/win32/miktex/tm/packages/next/miktex-cjkutils-bin-2.9.tar.lzma
-2021/09/18 | 56685 | systems/win32/miktex/tm/packages/next/miktex-cjkutils-bin-x64-2.9.tar.lzma
-2021/09/18 | 241812 | systems/win32/miktex/tm/packages/next/miktex-console-bin-2.9.tar.lzma
-2021/09/18 | 263957 | systems/win32/miktex/tm/packages/next/miktex-console-bin-x64-2.9.tar.lzma
-2021/09/18 | 180011 | systems/win32/miktex/tm/packages/next/miktex-curl-bin-2.9.tar.lzma
-2021/09/18 | 195635 | systems/win32/miktex/tm/packages/next/miktex-curl-bin-x64-2.9.tar.lzma
-2021/09/18 | 54631 | systems/win32/miktex/tm/packages/next/miktex-cweb-bin-2.9.tar.lzma
-2021/09/18 | 61814 | systems/win32/miktex/tm/packages/next/miktex-cweb-bin-x64-2.9.tar.lzma
-2021/09/18 | 21225 | systems/win32/miktex/tm/packages/next/miktex-devnag-bin-2.9.tar.lzma
-2021/09/18 | 23365 | systems/win32/miktex/tm/packages/next/miktex-devnag-bin-x64-2.9.tar.lzma
-2021/09/18 | 461808 | systems/win32/miktex/tm/packages/next/miktex-doc.tar.lzma
-2021/09/18 | 45904 | systems/win32/miktex/tm/packages/next/miktex-dvicopy-bin-2.9.tar.lzma
-2021/09/18 | 51203 | systems/win32/miktex/tm/packages/next/miktex-dvicopy-bin-x64-2.9.tar.lzma
-2021/09/18 | 284580 | systems/win32/miktex/tm/packages/next/miktex-dvipdfmx-bin-2.9.tar.lzma
-2021/09/18 | 325042 | systems/win32/miktex/tm/packages/next/miktex-dvipdfmx-bin-x64-2.9.tar.lzma
-2021/09/18 | 41194 | systems/win32/miktex/tm/packages/next/miktex-dvipng-bin-2.9.tar.lzma
-2021/09/18 | 45313 | systems/win32/miktex/tm/packages/next/miktex-dvipng-bin-x64-2.9.tar.lzma
-2021/09/18 | 119821 | systems/win32/miktex/tm/packages/next/miktex-dvips-bin-2.9.tar.lzma
-2021/09/18 | 133865 | systems/win32/miktex/tm/packages/next/miktex-dvips-bin-x64-2.9.tar.lzma
-2021/09/18 | 1291299 | systems/win32/miktex/tm/packages/next/miktex-dvisvgm-bin-2.9.tar.lzma
-2021/09/18 | 1421868 | systems/win32/miktex/tm/packages/next/miktex-dvisvgm-bin-x64-2.9.tar.lzma
-2021/09/18 | 100624 | systems/win32/miktex/tm/packages/next/miktex-epstopdf-bin-2.9.tar.lzma
-2021/09/18 | 112983 | systems/win32/miktex/tm/packages/next/miktex-epstopdf-bin-x64-2.9.tar.lzma
-2021/09/18 | 43026 | systems/win32/miktex/tm/packages/next/miktex-expat-bin-2.9.tar.lzma
-2021/09/18 | 48251 | systems/win32/miktex/tm/packages/next/miktex-expat-bin-x64-2.9.tar.lzma
-2021/09/18 | 25019 | systems/win32/miktex/tm/packages/next/miktex-findtexmf-bin-2.9.tar.lzma
-2021/09/18 | 28181 | systems/win32/miktex/tm/packages/next/miktex-findtexmf-bin-x64-2.9.tar.lzma
-2021/09/18 | 56318 | systems/win32/miktex/tm/packages/next/miktex-fmt-bin-2.9.tar.lzma
-2021/09/18 | 62784 | systems/win32/miktex/tm/packages/next/miktex-fmt-bin-x64-2.9.tar.lzma
-2021/09/18 | 136818 | systems/win32/miktex/tm/packages/next/miktex-fontconfig-bin-2.9.tar.lzma
-2021/09/18 | 154425 | systems/win32/miktex/tm/packages/next/miktex-fontconfig-bin-x64-2.9.tar.lzma
-2021/09/18 | 73195 | systems/win32/miktex/tm/packages/next/miktex-fonts-bin-2.9.tar.lzma
-2021/09/18 | 77952 | systems/win32/miktex/tm/packages/next/miktex-fonts-bin-x64-2.9.tar.lzma
-2021/09/18 | 71668 | systems/win32/miktex/tm/packages/next/miktex-freeglut-bin-2.9.tar.lzma
-2021/09/18 | 80449 | systems/win32/miktex/tm/packages/next/miktex-freeglut-bin-x64-2.9.tar.lzma
-2021/09/18 | 258880 | systems/win32/miktex/tm/packages/next/miktex-freetype2-bin-2.9.tar.lzma
-2021/09/18 | 295202 | systems/win32/miktex/tm/packages/next/miktex-freetype2-bin-x64-2.9.tar.lzma
-2021/09/18 | 22049 | systems/win32/miktex/tm/packages/next/miktex-fribidixetex-bin-2.9.tar.lzma
-2021/09/18 | 24995 | systems/win32/miktex/tm/packages/next/miktex-fribidixetex-bin-x64-2.9.tar.lzma
-2021/09/18 | 97338 | systems/win32/miktex/tm/packages/next/miktex-gd-bin-2.9.tar.lzma
-2021/09/18 | 112087 | systems/win32/miktex/tm/packages/next/miktex-gd-bin-x64-2.9.tar.lzma
-2021/09/18 | 64427 | systems/win32/miktex/tm/packages/next/miktex-graphite2-bin-2.9.tar.lzma
-2021/09/18 | 73148 | systems/win32/miktex/tm/packages/next/miktex-graphite2-bin-x64-2.9.tar.lzma
-2021/09/18 | 96632 | systems/win32/miktex/tm/packages/next/miktex-gregorio-bin-2.9.tar.lzma
-2021/09/18 | 115351 | systems/win32/miktex/tm/packages/next/miktex-gregorio-bin-x64-2.9.tar.lzma
-2021/09/18 | 38141 | systems/win32/miktex/tm/packages/next/miktex-gsf2pk-bin-2.9.tar.lzma
-2021/09/18 | 43003 | systems/win32/miktex/tm/packages/next/miktex-gsf2pk-bin-x64-2.9.tar.lzma
-2021/09/18 | 483654 | systems/win32/miktex/tm/packages/next/miktex-harfbuzz-bin-2.9.tar.lzma
-2021/09/18 | 566529 | systems/win32/miktex/tm/packages/next/miktex-harfbuzz-bin-x64-2.9.tar.lzma
-2021/09/18 | 147372 | systems/win32/miktex/tm/packages/next/miktex-hunspell-bin-2.9.tar.lzma
-2021/09/18 | 159579 | systems/win32/miktex/tm/packages/next/miktex-hunspell-bin-x64-2.9.tar.lzma
-2021/09/18 | 94799 | systems/win32/miktex/tm/packages/next/miktex-jpeg-bin-2.9.tar.lzma
-2021/09/18 | 109031 | systems/win32/miktex/tm/packages/next/miktex-jpeg-bin-x64-2.9.tar.lzma
-2021/09/18 | 16355 | systems/win32/miktex/tm/packages/next/miktex-kpathsea-bin-2.9.tar.lzma
-2021/09/18 | 17980 | systems/win32/miktex/tm/packages/next/miktex-kpathsea-bin-x64-2.9.tar.lzma
-2021/09/18 | 28389 | systems/win32/miktex/tm/packages/next/miktex-lacheck-bin-2.9.tar.lzma
-2021/09/18 | 30059 | systems/win32/miktex/tm/packages/next/miktex-lacheck-bin-x64-2.9.tar.lzma
-2021/09/18 | 629398 | systems/win32/miktex/tm/packages/next/miktex-lcdf-typetools-bin-2.9.tar.lzma
-2021/09/18 | 681250 | systems/win32/miktex/tm/packages/next/miktex-lcdf-typetools-bin-x64-2.9.tar.lzma
-2021/09/18 | 531850 | systems/win32/miktex/tm/packages/next/miktex-libressl-bin-2.9.tar.lzma
-2021/09/18 | 596930 | systems/win32/miktex/tm/packages/next/miktex-libressl-bin-x64-2.9.tar.lzma
-2021/09/18 | 476923 | systems/win32/miktex/tm/packages/next/miktex-log4cxx-bin-2.9.tar.lzma
-2021/09/18 | 510587 | systems/win32/miktex/tm/packages/next/miktex-log4cxx-bin-x64-2.9.tar.lzma
-2021/09/18 | 94310 | systems/win32/miktex/tm/packages/next/miktex-lua53-bin-2.9.tar.lzma
-2021/09/18 | 103400 | systems/win32/miktex/tm/packages/next/miktex-lua53-bin-x64-2.9.tar.lzma
-2021/09/18 | 1713895 | systems/win32/miktex/tm/packages/next/miktex-luatex-bin-2.9.tar.lzma
-2021/09/18 | 2009903 | systems/win32/miktex/tm/packages/next/miktex-luatex-bin-x64-2.9.tar.lzma
-2021/09/18 | 67619 | systems/win32/miktex/tm/packages/next/miktex-lzma-bin-2.9.tar.lzma
-2021/09/18 | 74205 | systems/win32/miktex/tm/packages/next/miktex-lzma-bin-x64-2.9.tar.lzma
-2021/09/18 | 27358 | systems/win32/miktex/tm/packages/next/miktex-makeindex-bin-2.9.tar.lzma
-2021/09/18 | 31621 | systems/win32/miktex/tm/packages/next/miktex-makeindex-bin-x64-2.9.tar.lzma
-2021/09/18 | 7720 | systems/win32/miktex/tm/packages/next/miktex-md5-bin-2.9.tar.lzma
-2021/09/18 | 8767 | systems/win32/miktex/tm/packages/next/miktex-md5-bin-x64-2.9.tar.lzma
-2021/09/18 | 196872 | systems/win32/miktex/tm/packages/next/miktex-metafont-bin-2.9.tar.lzma
-2021/09/18 | 215248 | systems/win32/miktex/tm/packages/next/miktex-metafont-bin-x64-2.9.tar.lzma
-2021/09/18 | 250264 | systems/win32/miktex/tm/packages/next/miktex-metapost-bin-2.9.tar.lzma
-2021/09/18 | 284025 | systems/win32/miktex/tm/packages/next/miktex-metapost-bin-x64-2.9.tar.lzma
-2021/09/18 | 98380 | systems/win32/miktex/tm/packages/next/miktex-mfware-bin-2.9.tar.lzma
-2021/09/18 | 103204 | systems/win32/miktex/tm/packages/next/miktex-mfware-bin-x64-2.9.tar.lzma
-2021/09/18 | 60636 | systems/win32/miktex/tm/packages/next/miktex-mkfntmap-bin-2.9.tar.lzma
-2021/09/18 | 68023 | systems/win32/miktex/tm/packages/next/miktex-mkfntmap-bin-x64-2.9.tar.lzma
-2021/09/18 | 124282 | systems/win32/miktex/tm/packages/next/miktex-mktex-bin-2.9.tar.lzma
-2021/09/18 | 136822 | systems/win32/miktex/tm/packages/next/miktex-mktex-bin-x64-2.9.tar.lzma
-2021/09/18 | 178352 | systems/win32/miktex/tm/packages/next/miktex-mo-bin-2.9.tar.lzma
-2021/09/18 | 196742 | systems/win32/miktex/tm/packages/next/miktex-mo-bin-x64-2.9.tar.lzma
-2021/09/18 | 199731 | systems/win32/miktex/tm/packages/next/miktex-mpfr-bin-2.9.tar.lzma
-2021/09/18 | 180120 | systems/win32/miktex/tm/packages/next/miktex-mpfr-bin-x64-2.9.tar.lzma
-2021/09/18 | 67483 | systems/win32/miktex/tm/packages/next/miktex-mpmcli-bin-2.9.tar.lzma
-2021/09/18 | 76826 | systems/win32/miktex/tm/packages/next/miktex-mpmcli-bin-x64-2.9.tar.lzma
-2021/09/18 | 31231 | systems/win32/miktex/tm/packages/next/miktex-mspack-bin-2.9.tar.lzma
-2021/09/18 | 35088 | systems/win32/miktex/tm/packages/next/miktex-mspack-bin-x64-2.9.tar.lzma
-2021/09/18 | 37805 | systems/win32/miktex/tm/packages/next/miktex-mthelp-bin-2.9.tar.lzma
-2021/09/18 | 42712 | systems/win32/miktex/tm/packages/next/miktex-mthelp-bin-x64-2.9.tar.lzma
-2021/09/18 | 42890 | systems/win32/miktex/tm/packages/next/miktex-mtprint-bin-2.9.tar.lzma
-2021/09/18 | 46907 | systems/win32/miktex/tm/packages/next/miktex-mtprint-bin-x64-2.9.tar.lzma
-2021/09/18 | 45971 | systems/win32/miktex/tm/packages/next/miktex-m-tx-bin-2.9.tar.lzma
-2021/09/18 | 52523 | systems/win32/miktex/tm/packages/next/miktex-m-tx-bin-x64-2.9.tar.lzma
-2021/09/18 | 194383 | systems/win32/miktex/tm/packages/next/miktex-omegaware-bin-2.9.tar.lzma
-2021/09/18 | 206605 | systems/win32/miktex/tm/packages/next/miktex-omegaware-bin-x64-2.9.tar.lzma
-2021/09/18 | 35691 | systems/win32/miktex/tm/packages/next/miktex-patgen-bin.tar.lzma
-2021/09/18 | 40230 | systems/win32/miktex/tm/packages/next/miktex-patgen-bin-x64.tar.lzma
-2021/09/18 | 744117 | systems/win32/miktex/tm/packages/next/miktex-pdftex-bin-2.9.tar.lzma
-2021/09/18 | 815242 | systems/win32/miktex/tm/packages/next/miktex-pdftex-bin-x64-2.9.tar.lzma
-2021/09/18 | 198170 | systems/win32/miktex/tm/packages/next/miktex-pixman-bin-2.9.tar.lzma
-2021/09/18 | 202797 | systems/win32/miktex/tm/packages/next/miktex-pixman-bin-x64-2.9.tar.lzma
-2021/09/18 | 208049 | systems/win32/miktex/tm/packages/next/miktex-pmx-bin-2.9.tar.lzma
-2021/09/18 | 243427 | systems/win32/miktex/tm/packages/next/miktex-pmx-bin-x64-2.9.tar.lzma
-2021/09/18 | 78773 | systems/win32/miktex/tm/packages/next/miktex-png-bin-2.9.tar.lzma
-2021/09/18 | 87777 | systems/win32/miktex/tm/packages/next/miktex-png-bin-x64-2.9.tar.lzma
-2021/09/18 | 1169717 | systems/win32/miktex/tm/packages/next/miktex-poppler-bin-2.9.tar.lzma
-2021/09/18 | 1301194 | systems/win32/miktex/tm/packages/next/miktex-poppler-bin-x64-2.9.tar.lzma
-2021/09/18 | 20135 | systems/win32/miktex/tm/packages/next/miktex-popt-bin-2.9.tar.lzma
-2021/09/18 | 23901 | systems/win32/miktex/tm/packages/next/miktex-popt-bin-x64-2.9.tar.lzma
-2021/09/18 | 31539 | systems/win32/miktex/tm/packages/next/miktex-posix-bin-2.9.tar.lzma
-2021/09/18 | 37240 | systems/win32/miktex/tm/packages/next/miktex-posix-bin-x64-2.9.tar.lzma
-2021/09/18 | 64185 | systems/win32/miktex/tm/packages/next/miktex-ps2pk-bin-2.9.tar.lzma
-2021/09/18 | 72208 | systems/win32/miktex/tm/packages/next/miktex-ps2pk-bin-x64-2.9.tar.lzma
-2021/09/18 | 50394 | systems/win32/miktex/tm/packages/next/miktex-psutils-bin-2.9.tar.lzma
-2021/09/18 | 57728 | systems/win32/miktex/tm/packages/next/miktex-psutils-bin-x64-2.9.tar.lzma
-2021/09/18 | 1340928 | systems/win32/miktex/tm/packages/next/miktex-runtime-bin-2.9.tar.lzma
-2021/09/18 | 1464202 | systems/win32/miktex/tm/packages/next/miktex-runtime-bin-x64-2.9.tar.lzma
-2021/09/18 | 42385 | systems/win32/miktex/tm/packages/next/miktex-synctex-bin-2.9.tar.lzma
-2021/09/18 | 51751 | systems/win32/miktex/tm/packages/next/miktex-synctex-bin-x64-2.9.tar.lzma
-2021/09/18 | 56513 | systems/win32/miktex/tm/packages/next/miktex-tdsutil-bin.tar.lzma
-2021/09/18 | 60039 | systems/win32/miktex/tm/packages/next/miktex-tdsutil-bin-x64.tar.lzma
-2021/09/18 | 25290 | systems/win32/miktex/tm/packages/next/miktex-teckit-bin-2.9.tar.lzma
-2021/09/18 | 27391 | systems/win32/miktex/tm/packages/next/miktex-teckit-bin-x64-2.9.tar.lzma
-2021/09/18 | 16414 | systems/win32/miktex/tm/packages/next/miktex-tex2xindy-bin-2.9.tar.lzma
-2021/09/18 | 18174 | systems/win32/miktex/tm/packages/next/miktex-tex2xindy-bin-x64-2.9.tar.lzma
-2021/09/18 | 63776 | systems/win32/miktex/tm/packages/next/miktex-tex4ht-bin-2.9.tar.lzma
-2021/09/18 | 69913 | systems/win32/miktex/tm/packages/next/miktex-tex4ht-bin-x64-2.9.tar.lzma
-2021/09/18 | 154805 | systems/win32/miktex/tm/packages/next/miktex-tex-bin-2.9.tar.lzma
-2021/09/18 | 169242 | systems/win32/miktex/tm/packages/next/miktex-tex-bin-x64-2.9.tar.lzma
-2021/09/18 | 57013 | systems/win32/miktex/tm/packages/next/miktex-texify-bin-2.9.tar.lzma
-2021/09/18 | 65786 | systems/win32/miktex/tm/packages/next/miktex-texify-bin-x64-2.9.tar.lzma
-2021/09/18 | 98260 | systems/win32/miktex/tm/packages/next/miktex-texware-bin-2.9.tar.lzma
-2021/09/18 | 105222 | systems/win32/miktex/tm/packages/next/miktex-texware-bin-x64-2.9.tar.lzma
-2021/09/18 | 2672610 | systems/win32/miktex/tm/packages/next/miktex-texworks-bin-2.9.tar.lzma
-2021/09/18 | 2796725 | systems/win32/miktex/tm/packages/next/miktex-texworks-bin-x64-2.9.tar.lzma
-2021/09/18 | 56355 | systems/win32/miktex/tm/packages/next/miktex-ttf2pk2-bin-2.9.tar.lzma
-2021/09/18 | 62228 | systems/win32/miktex/tm/packages/next/miktex-ttf2pk2-bin-x64-2.9.tar.lzma
-2021/09/18 | 33543 | systems/win32/miktex/tm/packages/next/miktex-upmendex-bin.tar.lzma
-2021/09/18 | 37359 | systems/win32/miktex/tm/packages/next/miktex-upmendex-bin-x64.tar.lzma
-2021/09/18 | 27727 | systems/win32/miktex/tm/packages/next/miktex-uriparser-bin-2.9.tar.lzma
-2021/09/18 | 33632 | systems/win32/miktex/tm/packages/next/miktex-uriparser-bin-x64-2.9.tar.lzma
-2021/09/18 | 65618 | systems/win32/miktex/tm/packages/next/miktex-web-bin-2.9.tar.lzma
-2021/09/18 | 70648 | systems/win32/miktex/tm/packages/next/miktex-web-bin-x64-2.9.tar.lzma
-2021/09/18 | 312107 | systems/win32/miktex/tm/packages/next/miktex-xetex-bin-2.9.tar.lzma
-2021/09/18 | 345739 | systems/win32/miktex/tm/packages/next/miktex-xetex-bin-x64-2.9.tar.lzma
-2021/09/18 | 55498 | systems/win32/miktex/tm/packages/next/miktex-xml2pmx-bin.tar.lzma
-2021/09/18 | 57139 | systems/win32/miktex/tm/packages/next/miktex-xml2pmx-bin-x64.tar.lzma
-2021/09/18 | 589843 | systems/win32/miktex/tm/packages/next/miktex-yap-bin-2.9.tar.lzma
-2021/09/18 | 624652 | systems/win32/miktex/tm/packages/next/miktex-yap-bin-x64-2.9.tar.lzma
-2021/09/18 | 39128 | systems/win32/miktex/tm/packages/next/miktex-zlib-bin-2.9.tar.lzma
-2021/09/18 | 43425 | systems/win32/miktex/tm/packages/next/miktex-zlib-bin-x64-2.9.tar.lzma
-2021/09/18 | 240961 | systems/win32/miktex/tm/packages/next/miktex-zzdb1-2.9.tar.lzma
-2021/09/18 | 1357420 | systems/win32/miktex/tm/packages/next/miktex-zzdb2-2.9.tar.lzma
-2021/09/18 | 1260183 | systems/win32/miktex/tm/packages/next/miktex-zzdb3-2.9.tar.lzma
-2021/09/18 | 13467 | systems/win32/miktex/tm/packages/next/miktex-zzip-bin-2.9.tar.lzma
-2021/09/18 | 14348 | systems/win32/miktex/tm/packages/next/miktex-zzip-bin-x64-2.9.tar.lzma
+2021/09/19 | 614469 | systems/win32/miktex/tm/packages/next/files.csv.lzma
+2021/09/19 | 51113 | systems/win32/miktex/tm/packages/next/miktex-arctrl-bin-2.9.tar.lzma
+2021/09/19 | 56476 | systems/win32/miktex/tm/packages/next/miktex-arctrl-bin-x64-2.9.tar.lzma
+2021/09/19 | 975020 | systems/win32/miktex/tm/packages/next/miktex-asymptote-bin-2.9.tar.lzma
+2021/09/19 | 1077990 | systems/win32/miktex/tm/packages/next/miktex-asymptote-bin-x64-2.9.tar.lzma
+2021/09/19 | 29037 | systems/win32/miktex/tm/packages/next/miktex-autosp-bin-2.9.tar.lzma
+2021/09/19 | 31349 | systems/win32/miktex/tm/packages/next/miktex-autosp-bin-x64-2.9.tar.lzma
+2021/09/19 | 29841 | systems/win32/miktex/tm/packages/next/miktex-axohelp-bin-2.9.tar.lzma
+2021/09/19 | 34666 | systems/win32/miktex/tm/packages/next/miktex-axohelp-bin-x64-2.9.tar.lzma
+2021/09/19 | 50152 | systems/win32/miktex/tm/packages/next/miktex-bibarts-bin-2.9.tar.lzma
+2021/09/19 | 57547 | systems/win32/miktex/tm/packages/next/miktex-bibarts-bin-x64-2.9.tar.lzma
+2021/09/19 | 83539 | systems/win32/miktex/tm/packages/next/miktex-bibtex8bit-bin-2.9.tar.lzma
+2021/09/19 | 95400 | systems/win32/miktex/tm/packages/next/miktex-bibtex8bit-bin-x64-2.9.tar.lzma
+2021/09/19 | 67819 | systems/win32/miktex/tm/packages/next/miktex-bibtex-bin-2.9.tar.lzma
+2021/09/19 | 75791 | systems/win32/miktex/tm/packages/next/miktex-bibtex-bin-x64-2.9.tar.lzma
+2021/09/19 | 27198 | systems/win32/miktex/tm/packages/next/miktex-bzip2-bin-2.9.tar.lzma
+2021/09/19 | 30824 | systems/win32/miktex/tm/packages/next/miktex-bzip2-bin-x64-2.9.tar.lzma
+2021/09/19 | 330787 | systems/win32/miktex/tm/packages/next/miktex-cairo-bin-2.9.tar.lzma
+2021/09/19 | 392790 | systems/win32/miktex/tm/packages/next/miktex-cairo-bin-x64-2.9.tar.lzma
+2021/09/19 | 32674 | systems/win32/miktex/tm/packages/next/miktex-chktex-bin-2.9.tar.lzma
+2021/09/19 | 36628 | systems/win32/miktex/tm/packages/next/miktex-chktex-bin-x64-2.9.tar.lzma
+2021/09/19 | 51309 | systems/win32/miktex/tm/packages/next/miktex-cjkutils-bin-2.9.tar.lzma
+2021/09/19 | 56684 | systems/win32/miktex/tm/packages/next/miktex-cjkutils-bin-x64-2.9.tar.lzma
+2021/09/19 | 241811 | systems/win32/miktex/tm/packages/next/miktex-console-bin-2.9.tar.lzma
+2021/09/19 | 263958 | systems/win32/miktex/tm/packages/next/miktex-console-bin-x64-2.9.tar.lzma
+2021/09/19 | 180009 | systems/win32/miktex/tm/packages/next/miktex-curl-bin-2.9.tar.lzma
+2021/09/19 | 195636 | systems/win32/miktex/tm/packages/next/miktex-curl-bin-x64-2.9.tar.lzma
+2021/09/19 | 54632 | systems/win32/miktex/tm/packages/next/miktex-cweb-bin-2.9.tar.lzma
+2021/09/19 | 61813 | systems/win32/miktex/tm/packages/next/miktex-cweb-bin-x64-2.9.tar.lzma
+2021/09/19 | 21225 | systems/win32/miktex/tm/packages/next/miktex-devnag-bin-2.9.tar.lzma
+2021/09/19 | 23364 | systems/win32/miktex/tm/packages/next/miktex-devnag-bin-x64-2.9.tar.lzma
+2021/09/19 | 461811 | systems/win32/miktex/tm/packages/next/miktex-doc.tar.lzma
+2021/09/19 | 45905 | systems/win32/miktex/tm/packages/next/miktex-dvicopy-bin-2.9.tar.lzma
+2021/09/19 | 51202 | systems/win32/miktex/tm/packages/next/miktex-dvicopy-bin-x64-2.9.tar.lzma
+2021/09/19 | 284580 | systems/win32/miktex/tm/packages/next/miktex-dvipdfmx-bin-2.9.tar.lzma
+2021/09/19 | 325044 | systems/win32/miktex/tm/packages/next/miktex-dvipdfmx-bin-x64-2.9.tar.lzma
+2021/09/19 | 41193 | systems/win32/miktex/tm/packages/next/miktex-dvipng-bin-2.9.tar.lzma
+2021/09/19 | 45310 | systems/win32/miktex/tm/packages/next/miktex-dvipng-bin-x64-2.9.tar.lzma
+2021/09/19 | 119820 | systems/win32/miktex/tm/packages/next/miktex-dvips-bin-2.9.tar.lzma
+2021/09/19 | 133864 | systems/win32/miktex/tm/packages/next/miktex-dvips-bin-x64-2.9.tar.lzma
+2021/09/19 | 1291300 | systems/win32/miktex/tm/packages/next/miktex-dvisvgm-bin-2.9.tar.lzma
+2021/09/19 | 1421867 | systems/win32/miktex/tm/packages/next/miktex-dvisvgm-bin-x64-2.9.tar.lzma
+2021/09/19 | 100624 | systems/win32/miktex/tm/packages/next/miktex-epstopdf-bin-2.9.tar.lzma
+2021/09/19 | 112982 | systems/win32/miktex/tm/packages/next/miktex-epstopdf-bin-x64-2.9.tar.lzma
+2021/09/19 | 43028 | systems/win32/miktex/tm/packages/next/miktex-expat-bin-2.9.tar.lzma
+2021/09/19 | 48253 | systems/win32/miktex/tm/packages/next/miktex-expat-bin-x64-2.9.tar.lzma
+2021/09/19 | 25019 | systems/win32/miktex/tm/packages/next/miktex-findtexmf-bin-2.9.tar.lzma
+2021/09/19 | 28177 | systems/win32/miktex/tm/packages/next/miktex-findtexmf-bin-x64-2.9.tar.lzma
+2021/09/19 | 56317 | systems/win32/miktex/tm/packages/next/miktex-fmt-bin-2.9.tar.lzma
+2021/09/19 | 62787 | systems/win32/miktex/tm/packages/next/miktex-fmt-bin-x64-2.9.tar.lzma
+2021/09/19 | 136816 | systems/win32/miktex/tm/packages/next/miktex-fontconfig-bin-2.9.tar.lzma
+2021/09/19 | 154425 | systems/win32/miktex/tm/packages/next/miktex-fontconfig-bin-x64-2.9.tar.lzma
+2021/09/19 | 73196 | systems/win32/miktex/tm/packages/next/miktex-fonts-bin-2.9.tar.lzma
+2021/09/19 | 77951 | systems/win32/miktex/tm/packages/next/miktex-fonts-bin-x64-2.9.tar.lzma
+2021/09/19 | 71668 | systems/win32/miktex/tm/packages/next/miktex-freeglut-bin-2.9.tar.lzma
+2021/09/19 | 80449 | systems/win32/miktex/tm/packages/next/miktex-freeglut-bin-x64-2.9.tar.lzma
+2021/09/19 | 258882 | systems/win32/miktex/tm/packages/next/miktex-freetype2-bin-2.9.tar.lzma
+2021/09/19 | 295204 | systems/win32/miktex/tm/packages/next/miktex-freetype2-bin-x64-2.9.tar.lzma
+2021/09/19 | 22048 | systems/win32/miktex/tm/packages/next/miktex-fribidixetex-bin-2.9.tar.lzma
+2021/09/19 | 24992 | systems/win32/miktex/tm/packages/next/miktex-fribidixetex-bin-x64-2.9.tar.lzma
+2021/09/19 | 97338 | systems/win32/miktex/tm/packages/next/miktex-gd-bin-2.9.tar.lzma
+2021/09/19 | 112086 | systems/win32/miktex/tm/packages/next/miktex-gd-bin-x64-2.9.tar.lzma
+2021/09/19 | 64427 | systems/win32/miktex/tm/packages/next/miktex-graphite2-bin-2.9.tar.lzma
+2021/09/19 | 73147 | systems/win32/miktex/tm/packages/next/miktex-graphite2-bin-x64-2.9.tar.lzma
+2021/09/19 | 96633 | systems/win32/miktex/tm/packages/next/miktex-gregorio-bin-2.9.tar.lzma
+2021/09/19 | 115350 | systems/win32/miktex/tm/packages/next/miktex-gregorio-bin-x64-2.9.tar.lzma
+2021/09/19 | 38141 | systems/win32/miktex/tm/packages/next/miktex-gsf2pk-bin-2.9.tar.lzma
+2021/09/19 | 43002 | systems/win32/miktex/tm/packages/next/miktex-gsf2pk-bin-x64-2.9.tar.lzma
+2021/09/19 | 483653 | systems/win32/miktex/tm/packages/next/miktex-harfbuzz-bin-2.9.tar.lzma
+2021/09/19 | 566528 | systems/win32/miktex/tm/packages/next/miktex-harfbuzz-bin-x64-2.9.tar.lzma
+2021/09/19 | 147372 | systems/win32/miktex/tm/packages/next/miktex-hunspell-bin-2.9.tar.lzma
+2021/09/19 | 159579 | systems/win32/miktex/tm/packages/next/miktex-hunspell-bin-x64-2.9.tar.lzma
+2021/09/19 | 94800 | systems/win32/miktex/tm/packages/next/miktex-jpeg-bin-2.9.tar.lzma
+2021/09/19 | 109032 | systems/win32/miktex/tm/packages/next/miktex-jpeg-bin-x64-2.9.tar.lzma
+2021/09/19 | 16357 | systems/win32/miktex/tm/packages/next/miktex-kpathsea-bin-2.9.tar.lzma
+2021/09/19 | 17980 | systems/win32/miktex/tm/packages/next/miktex-kpathsea-bin-x64-2.9.tar.lzma
+2021/09/19 | 28391 | systems/win32/miktex/tm/packages/next/miktex-lacheck-bin-2.9.tar.lzma
+2021/09/19 | 30058 | systems/win32/miktex/tm/packages/next/miktex-lacheck-bin-x64-2.9.tar.lzma
+2021/09/19 | 629398 | systems/win32/miktex/tm/packages/next/miktex-lcdf-typetools-bin-2.9.tar.lzma
+2021/09/19 | 681250 | systems/win32/miktex/tm/packages/next/miktex-lcdf-typetools-bin-x64-2.9.tar.lzma
+2021/09/19 | 531850 | systems/win32/miktex/tm/packages/next/miktex-libressl-bin-2.9.tar.lzma
+2021/09/19 | 596929 | systems/win32/miktex/tm/packages/next/miktex-libressl-bin-x64-2.9.tar.lzma
+2021/09/19 | 476920 | systems/win32/miktex/tm/packages/next/miktex-log4cxx-bin-2.9.tar.lzma
+2021/09/19 | 510588 | systems/win32/miktex/tm/packages/next/miktex-log4cxx-bin-x64-2.9.tar.lzma
+2021/09/19 | 94311 | systems/win32/miktex/tm/packages/next/miktex-lua53-bin-2.9.tar.lzma
+2021/09/19 | 103400 | systems/win32/miktex/tm/packages/next/miktex-lua53-bin-x64-2.9.tar.lzma
+2021/09/19 | 1713894 | systems/win32/miktex/tm/packages/next/miktex-luatex-bin-2.9.tar.lzma
+2021/09/19 | 2009906 | systems/win32/miktex/tm/packages/next/miktex-luatex-bin-x64-2.9.tar.lzma
+2021/09/19 | 67619 | systems/win32/miktex/tm/packages/next/miktex-lzma-bin-2.9.tar.lzma
+2021/09/19 | 74211 | systems/win32/miktex/tm/packages/next/miktex-lzma-bin-x64-2.9.tar.lzma
+2021/09/19 | 27357 | systems/win32/miktex/tm/packages/next/miktex-makeindex-bin-2.9.tar.lzma
+2021/09/19 | 31620 | systems/win32/miktex/tm/packages/next/miktex-makeindex-bin-x64-2.9.tar.lzma
+2021/09/19 | 7721 | systems/win32/miktex/tm/packages/next/miktex-md5-bin-2.9.tar.lzma
+2021/09/19 | 8768 | systems/win32/miktex/tm/packages/next/miktex-md5-bin-x64-2.9.tar.lzma
+2021/09/19 | 196871 | systems/win32/miktex/tm/packages/next/miktex-metafont-bin-2.9.tar.lzma
+2021/09/19 | 215248 | systems/win32/miktex/tm/packages/next/miktex-metafont-bin-x64-2.9.tar.lzma
+2021/09/19 | 250262 | systems/win32/miktex/tm/packages/next/miktex-metapost-bin-2.9.tar.lzma
+2021/09/19 | 284023 | systems/win32/miktex/tm/packages/next/miktex-metapost-bin-x64-2.9.tar.lzma
+2021/09/19 | 98382 | systems/win32/miktex/tm/packages/next/miktex-mfware-bin-2.9.tar.lzma
+2021/09/19 | 103205 | systems/win32/miktex/tm/packages/next/miktex-mfware-bin-x64-2.9.tar.lzma
+2021/09/19 | 60638 | systems/win32/miktex/tm/packages/next/miktex-mkfntmap-bin-2.9.tar.lzma
+2021/09/19 | 68024 | systems/win32/miktex/tm/packages/next/miktex-mkfntmap-bin-x64-2.9.tar.lzma
+2021/09/19 | 124282 | systems/win32/miktex/tm/packages/next/miktex-mktex-bin-2.9.tar.lzma
+2021/09/19 | 136819 | systems/win32/miktex/tm/packages/next/miktex-mktex-bin-x64-2.9.tar.lzma
+2021/09/19 | 178353 | systems/win32/miktex/tm/packages/next/miktex-mo-bin-2.9.tar.lzma
+2021/09/19 | 196740 | systems/win32/miktex/tm/packages/next/miktex-mo-bin-x64-2.9.tar.lzma
+2021/09/19 | 199732 | systems/win32/miktex/tm/packages/next/miktex-mpfr-bin-2.9.tar.lzma
+2021/09/19 | 180119 | systems/win32/miktex/tm/packages/next/miktex-mpfr-bin-x64-2.9.tar.lzma
+2021/09/19 | 67484 | systems/win32/miktex/tm/packages/next/miktex-mpmcli-bin-2.9.tar.lzma
+2021/09/19 | 76826 | systems/win32/miktex/tm/packages/next/miktex-mpmcli-bin-x64-2.9.tar.lzma
+2021/09/19 | 31232 | systems/win32/miktex/tm/packages/next/miktex-mspack-bin-2.9.tar.lzma
+2021/09/19 | 35086 | systems/win32/miktex/tm/packages/next/miktex-mspack-bin-x64-2.9.tar.lzma
+2021/09/19 | 37805 | systems/win32/miktex/tm/packages/next/miktex-mthelp-bin-2.9.tar.lzma
+2021/09/19 | 42710 | systems/win32/miktex/tm/packages/next/miktex-mthelp-bin-x64-2.9.tar.lzma
+2021/09/19 | 42891 | systems/win32/miktex/tm/packages/next/miktex-mtprint-bin-2.9.tar.lzma
+2021/09/19 | 46906 | systems/win32/miktex/tm/packages/next/miktex-mtprint-bin-x64-2.9.tar.lzma
+2021/09/19 | 45970 | systems/win32/miktex/tm/packages/next/miktex-m-tx-bin-2.9.tar.lzma
+2021/09/19 | 52522 | systems/win32/miktex/tm/packages/next/miktex-m-tx-bin-x64-2.9.tar.lzma
+2021/09/19 | 194383 | systems/win32/miktex/tm/packages/next/miktex-omegaware-bin-2.9.tar.lzma
+2021/09/19 | 206603 | systems/win32/miktex/tm/packages/next/miktex-omegaware-bin-x64-2.9.tar.lzma
+2021/09/19 | 35691 | systems/win32/miktex/tm/packages/next/miktex-patgen-bin.tar.lzma
+2021/09/19 | 40228 | systems/win32/miktex/tm/packages/next/miktex-patgen-bin-x64.tar.lzma
+2021/09/19 | 744117 | systems/win32/miktex/tm/packages/next/miktex-pdftex-bin-2.9.tar.lzma
+2021/09/19 | 815241 | systems/win32/miktex/tm/packages/next/miktex-pdftex-bin-x64-2.9.tar.lzma
+2021/09/19 | 198171 | systems/win32/miktex/tm/packages/next/miktex-pixman-bin-2.9.tar.lzma
+2021/09/19 | 202797 | systems/win32/miktex/tm/packages/next/miktex-pixman-bin-x64-2.9.tar.lzma
+2021/09/19 | 208047 | systems/win32/miktex/tm/packages/next/miktex-pmx-bin-2.9.tar.lzma
+2021/09/19 | 243426 | systems/win32/miktex/tm/packages/next/miktex-pmx-bin-x64-2.9.tar.lzma
+2021/09/19 | 78774 | systems/win32/miktex/tm/packages/next/miktex-png-bin-2.9.tar.lzma
+2021/09/19 | 87776 | systems/win32/miktex/tm/packages/next/miktex-png-bin-x64-2.9.tar.lzma
+2021/09/19 | 1169718 | systems/win32/miktex/tm/packages/next/miktex-poppler-bin-2.9.tar.lzma
+2021/09/19 | 1301194 | systems/win32/miktex/tm/packages/next/miktex-poppler-bin-x64-2.9.tar.lzma
+2021/09/19 | 20138 | systems/win32/miktex/tm/packages/next/miktex-popt-bin-2.9.tar.lzma
+2021/09/19 | 23903 | systems/win32/miktex/tm/packages/next/miktex-popt-bin-x64-2.9.tar.lzma
+2021/09/19 | 31537 | systems/win32/miktex/tm/packages/next/miktex-posix-bin-2.9.tar.lzma
+2021/09/19 | 37242 | systems/win32/miktex/tm/packages/next/miktex-posix-bin-x64-2.9.tar.lzma
+2021/09/19 | 64185 | systems/win32/miktex/tm/packages/next/miktex-ps2pk-bin-2.9.tar.lzma
+2021/09/19 | 72206 | systems/win32/miktex/tm/packages/next/miktex-ps2pk-bin-x64-2.9.tar.lzma
+2021/09/19 | 50396 | systems/win32/miktex/tm/packages/next/miktex-psutils-bin-2.9.tar.lzma
+2021/09/19 | 57727 | systems/win32/miktex/tm/packages/next/miktex-psutils-bin-x64-2.9.tar.lzma
+2021/09/19 | 1340929 | systems/win32/miktex/tm/packages/next/miktex-runtime-bin-2.9.tar.lzma
+2021/09/19 | 1464203 | systems/win32/miktex/tm/packages/next/miktex-runtime-bin-x64-2.9.tar.lzma
+2021/09/19 | 42386 | systems/win32/miktex/tm/packages/next/miktex-synctex-bin-2.9.tar.lzma
+2021/09/19 | 51748 | systems/win32/miktex/tm/packages/next/miktex-synctex-bin-x64-2.9.tar.lzma
+2021/09/19 | 56515 | systems/win32/miktex/tm/packages/next/miktex-tdsutil-bin.tar.lzma
+2021/09/19 | 60039 | systems/win32/miktex/tm/packages/next/miktex-tdsutil-bin-x64.tar.lzma
+2021/09/19 | 25292 | systems/win32/miktex/tm/packages/next/miktex-teckit-bin-2.9.tar.lzma
+2021/09/19 | 27390 | systems/win32/miktex/tm/packages/next/miktex-teckit-bin-x64-2.9.tar.lzma
+2021/09/19 | 16413 | systems/win32/miktex/tm/packages/next/miktex-tex2xindy-bin-2.9.tar.lzma
+2021/09/19 | 18176 | systems/win32/miktex/tm/packages/next/miktex-tex2xindy-bin-x64-2.9.tar.lzma
+2021/09/19 | 63777 | systems/win32/miktex/tm/packages/next/miktex-tex4ht-bin-2.9.tar.lzma
+2021/09/19 | 69914 | systems/win32/miktex/tm/packages/next/miktex-tex4ht-bin-x64-2.9.tar.lzma
+2021/09/19 | 154806 | systems/win32/miktex/tm/packages/next/miktex-tex-bin-2.9.tar.lzma
+2021/09/19 | 169241 | systems/win32/miktex/tm/packages/next/miktex-tex-bin-x64-2.9.tar.lzma
+2021/09/19 | 57014 | systems/win32/miktex/tm/packages/next/miktex-texify-bin-2.9.tar.lzma
+2021/09/19 | 65786 | systems/win32/miktex/tm/packages/next/miktex-texify-bin-x64-2.9.tar.lzma
+2021/09/19 | 98262 | systems/win32/miktex/tm/packages/next/miktex-texware-bin-2.9.tar.lzma
+2021/09/19 | 105222 | systems/win32/miktex/tm/packages/next/miktex-texware-bin-x64-2.9.tar.lzma
+2021/09/19 | 2672612 | systems/win32/miktex/tm/packages/next/miktex-texworks-bin-2.9.tar.lzma
+2021/09/19 | 2796726 | systems/win32/miktex/tm/packages/next/miktex-texworks-bin-x64-2.9.tar.lzma
+2021/09/19 | 56356 | systems/win32/miktex/tm/packages/next/miktex-ttf2pk2-bin-2.9.tar.lzma
+2021/09/19 | 62227 | systems/win32/miktex/tm/packages/next/miktex-ttf2pk2-bin-x64-2.9.tar.lzma
+2021/09/19 | 33545 | systems/win32/miktex/tm/packages/next/miktex-upmendex-bin.tar.lzma
+2021/09/19 | 37360 | systems/win32/miktex/tm/packages/next/miktex-upmendex-bin-x64.tar.lzma
+2021/09/19 | 27729 | systems/win32/miktex/tm/packages/next/miktex-uriparser-bin-2.9.tar.lzma
+2021/09/19 | 33633 | systems/win32/miktex/tm/packages/next/miktex-uriparser-bin-x64-2.9.tar.lzma
+2021/09/19 | 65619 | systems/win32/miktex/tm/packages/next/miktex-web-bin-2.9.tar.lzma
+2021/09/19 | 70648 | systems/win32/miktex/tm/packages/next/miktex-web-bin-x64-2.9.tar.lzma
+2021/09/19 | 312108 | systems/win32/miktex/tm/packages/next/miktex-xetex-bin-2.9.tar.lzma
+2021/09/19 | 345738 | systems/win32/miktex/tm/packages/next/miktex-xetex-bin-x64-2.9.tar.lzma
+2021/09/19 | 55498 | systems/win32/miktex/tm/packages/next/miktex-xml2pmx-bin.tar.lzma
+2021/09/19 | 57138 | systems/win32/miktex/tm/packages/next/miktex-xml2pmx-bin-x64.tar.lzma
+2021/09/19 | 589841 | systems/win32/miktex/tm/packages/next/miktex-yap-bin-2.9.tar.lzma
+2021/09/19 | 624651 | systems/win32/miktex/tm/packages/next/miktex-yap-bin-x64-2.9.tar.lzma
+2021/09/19 | 39129 | systems/win32/miktex/tm/packages/next/miktex-zlib-bin-2.9.tar.lzma
+2021/09/19 | 43426 | systems/win32/miktex/tm/packages/next/miktex-zlib-bin-x64-2.9.tar.lzma
+2021/09/19 | 240974 | systems/win32/miktex/tm/packages/next/miktex-zzdb1-2.9.tar.lzma
+2021/09/19 | 1357977 | systems/win32/miktex/tm/packages/next/miktex-zzdb2-2.9.tar.lzma
+2021/09/19 | 1260116 | systems/win32/miktex/tm/packages/next/miktex-zzdb3-2.9.tar.lzma
+2021/09/19 | 13464 | systems/win32/miktex/tm/packages/next/miktex-zzip-bin-2.9.tar.lzma
+2021/09/19 | 14349 | systems/win32/miktex/tm/packages/next/miktex-zzip-bin-x64-2.9.tar.lzma
2019/03/12 | 1412 | systems/win32/miktex/tm/packages/nextpage.tar.lzma
-2021/09/18 | 1153 | systems/win32/miktex/tm/packages/next/pr.ini
+2021/09/19 | 1153 | systems/win32/miktex/tm/packages/next/pr.ini
2018/07/06 | 145915 | systems/win32/miktex/tm/packages/nexus-otf.tar.lzma
2017/03/30 | 191149 | systems/win32/miktex/tm/packages/nfssext-cfr.tar.lzma
2012/12/20 | 612251 | systems/win32/miktex/tm/packages/nicefilelist.tar.lzma
@@ -296743,7 +296790,7 @@
2020/10/20 | 342254 | systems/win32/miktex/tm/packages/preview.tar.lzma
2019/06/20 | 375963 | systems/win32/miktex/tm/packages/prftree.tar.lzma
2021/04/23 | 274028 | systems/win32/miktex/tm/packages/principia.tar.lzma
-2021/09/18 | 927 | systems/win32/miktex/tm/packages/pr.ini
+2021/09/19 | 927 | systems/win32/miktex/tm/packages/pr.ini
2010/09/27 | 248469 | systems/win32/miktex/tm/packages/printlen.tar.lzma
2008/06/17 | 77313 | systems/win32/miktex/tm/packages/proba.tar.lzma
2017/07/12 | 988486 | systems/win32/miktex/tm/packages/probsoln.tar.lzma
@@ -298347,7 +298394,7 @@
2004/06/04 | 404 | tds/tds.sed
2004/06/23 | 65418 | tds/tds.tex
2019/05/29 | 304264 | tds.zip
-2021/09/19 | 186 | timestamp
+2021/09/20 | 186 | timestamp
2002/08/26 | 61898 | usergrps/dante/antrag.pdf
2002/08/26 | 118217 | usergrps/dante/antrag.ps
2021/08/04 | 917 | usergrps/dante/dtk/doc/beispiel.bib
diff --git a/FILES.byname.gz b/FILES.byname.gz
index 7b5f079e49..bfaa9d6ff0 100644
--- a/FILES.byname.gz
+++ b/FILES.byname.gz
Binary files differ
diff --git a/FILES.last07days b/FILES.last07days
index d39beea1c3..d5b8e490da 100644
--- a/FILES.last07days
+++ b/FILES.last07days
@@ -1,309 +1,421 @@
-2021/09/18 | 98380 | systems/win32/miktex/tm/packages/next/miktex-mfware-bin-2.9.tar.lzma
-2021/09/18 | 98260 | systems/win32/miktex/tm/packages/next/miktex-texware-bin-2.9.tar.lzma
+2021/09/19 | 99421 | support/light-latex-make/llmk.pdf
+2021/09/19 | 98382 | systems/win32/miktex/tm/packages/next/miktex-mfware-bin-2.9.tar.lzma
+2021/09/19 | 98262 | systems/win32/miktex/tm/packages/next/miktex-texware-bin-2.9.tar.lzma
+2021/09/19 | 975020 | systems/win32/miktex/tm/packages/next/miktex-asymptote-bin-2.9.tar.lzma
+2021/09/19 | 97338 | systems/win32/miktex/tm/packages/next/miktex-gd-bin-2.9.tar.lzma
+2021/09/19 | 96633 | systems/win32/miktex/tm/packages/next/miktex-gregorio-bin-2.9.tar.lzma
+2021/09/19 | 95400 | systems/win32/miktex/tm/packages/next/miktex-bibtex8bit-bin-x64-2.9.tar.lzma
+2021/09/19 | 94800 | systems/win32/miktex/tm/packages/next/miktex-jpeg-bin-2.9.tar.lzma
+2021/09/19 | 94311 | systems/win32/miktex/tm/packages/next/miktex-lua53-bin-2.9.tar.lzma
+2021/09/19 | 927 | systems/win32/miktex/tm/packages/pr.ini
+2021/09/19 | 9234 | support/light-latex-make/README.md
+2021/09/19 | 915258 | macros/latex/contrib/schooldocs.zip
+2021/09/19 | 87776 | systems/win32/miktex/tm/packages/next/miktex-png-bin-x64-2.9.tar.lzma
+2021/09/19 | 8768 | systems/win32/miktex/tm/packages/next/miktex-md5-bin-x64-2.9.tar.lzma
+2021/09/19 | 863 | info/dtk-bibliography/dtk-bibliography.tex
+2021/09/19 | 83539 | systems/win32/miktex/tm/packages/next/miktex-bibtex8bit-bin-2.9.tar.lzma
+2021/09/19 | 83276 | systems/texlive/tlnet/archive/luatexja.source.r60541.tar.xz
+2021/09/19 | 828771 | language/korean/pmhanguljamo.zip
+2021/09/19 | 8161 | support/light-latex-make/llmk-doc.cls
+2021/09/19 | 815241 | systems/win32/miktex/tm/packages/next/miktex-pdftex-bin-x64-2.9.tar.lzma
+2021/09/19 | 80449 | systems/win32/miktex/tm/packages/next/miktex-freeglut-bin-x64-2.9.tar.lzma
+2021/09/19 | 78774 | systems/win32/miktex/tm/packages/next/miktex-png-bin-2.9.tar.lzma
+2021/09/19 | 77951 | systems/win32/miktex/tm/packages/next/miktex-fonts-bin-x64-2.9.tar.lzma
+2021/09/19 | 77263 | support/light-latex-make/llmk-logo.png
+2021/09/19 | 7721 | systems/win32/miktex/tm/packages/next/miktex-md5-bin-2.9.tar.lzma
+2021/09/19 | 76826 | systems/win32/miktex/tm/packages/next/miktex-mpmcli-bin-x64-2.9.tar.lzma
+2021/09/19 | 758 | macros/luatex/generic/luakeys/luakeys-debug.sty
+2021/09/19 | 75791 | systems/win32/miktex/tm/packages/next/miktex-bibtex-bin-x64-2.9.tar.lzma
+2021/09/19 | 744117 | systems/win32/miktex/tm/packages/next/miktex-pdftex-bin-2.9.tar.lzma
+2021/09/19 | 74211 | systems/win32/miktex/tm/packages/next/miktex-lzma-bin-x64-2.9.tar.lzma
+2021/09/19 | 73196 | systems/win32/miktex/tm/packages/next/miktex-fonts-bin-2.9.tar.lzma
+2021/09/19 | 73147 | systems/win32/miktex/tm/packages/next/miktex-graphite2-bin-x64-2.9.tar.lzma
+2021/09/19 | 72206 | systems/win32/miktex/tm/packages/next/miktex-ps2pk-bin-x64-2.9.tar.lzma
+2021/09/19 | 71668 | systems/win32/miktex/tm/packages/next/miktex-freeglut-bin-2.9.tar.lzma
+2021/09/19 | 70648 | systems/win32/miktex/tm/packages/next/miktex-web-bin-x64-2.9.tar.lzma
+2021/09/19 | 69914 | systems/win32/miktex/tm/packages/next/miktex-tex4ht-bin-x64-2.9.tar.lzma
+2021/09/19 | 681250 | systems/win32/miktex/tm/packages/next/miktex-lcdf-typetools-bin-x64-2.9.tar.lzma
+2021/09/19 | 6811631 | systems/texlive/tlnet/install-tl-unx.tar.gz
+2021/09/19 | 68024 | systems/win32/miktex/tm/packages/next/miktex-mkfntmap-bin-x64-2.9.tar.lzma
+2021/09/19 | 67819 | systems/win32/miktex/tm/packages/next/miktex-bibtex-bin-2.9.tar.lzma
+2021/09/19 | 67619 | systems/win32/miktex/tm/packages/next/miktex-lzma-bin-2.9.tar.lzma
+2021/09/19 | 67484 | systems/win32/miktex/tm/packages/next/miktex-mpmcli-bin-2.9.tar.lzma
+2021/09/19 | 657 | info/dtk-bibliography/README.md
+2021/09/19 | 65786 | systems/win32/miktex/tm/packages/next/miktex-texify-bin-x64-2.9.tar.lzma
+2021/09/19 | 65619 | systems/win32/miktex/tm/packages/next/miktex-web-bin-2.9.tar.lzma
+2021/09/19 | 644568 | systems/texlive/tlnet/archive/pdfextra.doc.r60542.tar.xz
+2021/09/19 | 64427 | systems/win32/miktex/tm/packages/next/miktex-graphite2-bin-2.9.tar.lzma
+2021/09/19 | 64185 | systems/win32/miktex/tm/packages/next/miktex-ps2pk-bin-2.9.tar.lzma
+2021/09/19 | 63777 | systems/win32/miktex/tm/packages/next/miktex-tex4ht-bin-2.9.tar.lzma
+2021/09/19 | 629398 | systems/win32/miktex/tm/packages/next/miktex-lcdf-typetools-bin-2.9.tar.lzma
+2021/09/19 | 62787 | systems/win32/miktex/tm/packages/next/miktex-fmt-bin-x64-2.9.tar.lzma
+2021/09/19 | 624651 | systems/win32/miktex/tm/packages/next/miktex-yap-bin-x64-2.9.tar.lzma
+2021/09/19 | 62227 | systems/win32/miktex/tm/packages/next/miktex-ttf2pk2-bin-x64-2.9.tar.lzma
+2021/09/19 | 61813 | systems/win32/miktex/tm/packages/next/miktex-cweb-bin-x64-2.9.tar.lzma
+2021/09/19 | 614598 | systems/win32/miktex/tm/packages/files.csv.lzma
+2021/09/19 | 614469 | systems/win32/miktex/tm/packages/next/files.csv.lzma
+2021/09/19 | 60638 | systems/win32/miktex/tm/packages/next/miktex-mkfntmap-bin-2.9.tar.lzma
+2021/09/19 | 60039 | systems/win32/miktex/tm/packages/next/miktex-tdsutil-bin-x64.tar.lzma
+2021/09/19 | 596929 | systems/win32/miktex/tm/packages/next/miktex-libressl-bin-x64-2.9.tar.lzma
+2021/09/19 | 589841 | systems/win32/miktex/tm/packages/next/miktex-yap-bin-2.9.tar.lzma
+2021/09/19 | 57727 | systems/win32/miktex/tm/packages/next/miktex-psutils-bin-x64-2.9.tar.lzma
+2021/09/19 | 57547 | systems/win32/miktex/tm/packages/next/miktex-bibarts-bin-x64-2.9.tar.lzma
+2021/09/19 | 57138 | systems/win32/miktex/tm/packages/next/miktex-xml2pmx-bin-x64.tar.lzma
+2021/09/19 | 57014 | systems/win32/miktex/tm/packages/next/miktex-texify-bin-2.9.tar.lzma
+2021/09/19 | 56684 | systems/win32/miktex/tm/packages/next/miktex-cjkutils-bin-x64-2.9.tar.lzma
+2021/09/19 | 566528 | systems/win32/miktex/tm/packages/next/miktex-harfbuzz-bin-x64-2.9.tar.lzma
+2021/09/19 | 56515 | systems/win32/miktex/tm/packages/next/miktex-tdsutil-bin.tar.lzma
+2021/09/19 | 56476 | systems/win32/miktex/tm/packages/next/miktex-arctrl-bin-x64-2.9.tar.lzma
+2021/09/19 | 56356 | systems/win32/miktex/tm/packages/next/miktex-ttf2pk2-bin-2.9.tar.lzma
+2021/09/19 | 56317 | systems/win32/miktex/tm/packages/next/miktex-fmt-bin-2.9.tar.lzma
+2021/09/19 | 55498 | systems/win32/miktex/tm/packages/next/miktex-xml2pmx-bin.tar.lzma
+2021/09/19 | 5492 | systems/texlive/tlnet/archive/readarray.r60540.tar.xz
+2021/09/19 | 54632 | systems/win32/miktex/tm/packages/next/miktex-cweb-bin-2.9.tar.lzma
+2021/09/19 | 533270 | language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.pdf
+2021/09/19 | 531850 | systems/win32/miktex/tm/packages/next/miktex-libressl-bin-2.9.tar.lzma
+2021/09/19 | 52522 | systems/win32/miktex/tm/packages/next/miktex-m-tx-bin-x64-2.9.tar.lzma
+2021/09/19 | 522146 | info/dtk-bibliography/dtk-bibliography.bib
+2021/09/19 | 520 | info/dtk-bibliography/dtk-authoryear.dbx
+2021/09/19 | 51748 | systems/win32/miktex/tm/packages/next/miktex-synctex-bin-x64-2.9.tar.lzma
+2021/09/19 | 51597 | language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.tex
+2021/09/19 | 51309 | systems/win32/miktex/tm/packages/next/miktex-cjkutils-bin-2.9.tar.lzma
+2021/09/19 | 51202 | systems/win32/miktex/tm/packages/next/miktex-dvicopy-bin-x64-2.9.tar.lzma
+2021/09/19 | 51113 | systems/win32/miktex/tm/packages/next/miktex-arctrl-bin-2.9.tar.lzma
+2021/09/19 | 510588 | systems/win32/miktex/tm/packages/next/miktex-log4cxx-bin-x64-2.9.tar.lzma
+2021/09/19 | 506352 | systems/texlive/tlnet/archive/readarray.doc.r60540.tar.xz
+2021/09/19 | 5060 | systems/texlive/tlnet/archive/expkv-def.r60538.tar.xz
+2021/09/19 | 50396 | systems/win32/miktex/tm/packages/next/miktex-psutils-bin-2.9.tar.lzma
+2021/09/19 | 50152 | systems/win32/miktex/tm/packages/next/miktex-bibarts-bin-2.9.tar.lzma
+2021/09/19 | 48 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.md5
+2021/09/19 | 483653 | systems/win32/miktex/tm/packages/next/miktex-harfbuzz-bin-2.9.tar.lzma
+2021/09/19 | 4834 | info/dtk-bibliography/dtk-logos.sty
+2021/09/19 | 48253 | systems/win32/miktex/tm/packages/next/miktex-expat-bin-x64-2.9.tar.lzma
+2021/09/19 | 476920 | systems/win32/miktex/tm/packages/next/miktex-log4cxx-bin-2.9.tar.lzma
+2021/09/19 | 46906 | systems/win32/miktex/tm/packages/next/miktex-mtprint-bin-x64-2.9.tar.lzma
+2021/09/19 | 461811 | systems/win32/miktex/tm/packages/next/miktex-doc.tar.lzma
+2021/09/19 | 45970 | systems/win32/miktex/tm/packages/next/miktex-m-tx-bin-2.9.tar.lzma
+2021/09/19 | 45905 | systems/win32/miktex/tm/packages/next/miktex-dvicopy-bin-2.9.tar.lzma
+2021/09/19 | 455 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc
+2021/09/19 | 455 | systems/texlive/tlnet/install-tl.zip.sha512.asc
+2021/09/19 | 455 | systems/texlive/tlnet/install-tl-windows.exe.sha512.asc
+2021/09/19 | 455 | systems/texlive/tlnet/install-tl-unx.tar.gz.sha512.asc
+2021/09/19 | 45310 | systems/win32/miktex/tm/packages/next/miktex-dvipng-bin-x64-2.9.tar.lzma
+2021/09/19 | 436311 | info/dtk-bibliography.zip
+2021/09/19 | 43426 | systems/win32/miktex/tm/packages/next/miktex-zlib-bin-x64-2.9.tar.lzma
+2021/09/19 | 43028 | systems/win32/miktex/tm/packages/next/miktex-expat-bin-2.9.tar.lzma
+2021/09/19 | 43002 | systems/win32/miktex/tm/packages/next/miktex-gsf2pk-bin-x64-2.9.tar.lzma
+2021/09/19 | 42891 | systems/win32/miktex/tm/packages/next/miktex-mtprint-bin-2.9.tar.lzma
+2021/09/19 | 42710 | systems/win32/miktex/tm/packages/next/miktex-mthelp-bin-x64-2.9.tar.lzma
+2021/09/19 | 42386 | systems/win32/miktex/tm/packages/next/miktex-synctex-bin-2.9.tar.lzma
+2021/09/19 | 41193 | systems/win32/miktex/tm/packages/next/miktex-dvipng-bin-2.9.tar.lzma
+2021/09/19 | 40228 | systems/win32/miktex/tm/packages/next/miktex-patgen-bin-x64.tar.lzma
+2021/09/19 | 400072 | systems/texlive/tlnet/archive/texlive-scripts.doc.r60532.tar.xz
+2021/09/19 | 396880 | systems/texlive/tlnet/archive/expkv-def.doc.r60538.tar.xz
+2021/09/19 | 39513 | support/light-latex-make/llmk.lua
+2021/09/19 | 392790 | systems/win32/miktex/tm/packages/next/miktex-cairo-bin-x64-2.9.tar.lzma
+2021/09/19 | 39129 | systems/win32/miktex/tm/packages/next/miktex-zlib-bin-2.9.tar.lzma
+2021/09/19 | 3848 | systems/texlive/tlnet/archive/exesheet.r60537.tar.xz
+2021/09/19 | 38141 | systems/win32/miktex/tm/packages/next/miktex-gsf2pk-bin-2.9.tar.lzma
+2021/09/19 | 381060 | info/dtk-bibliography/dtk-bibliography.pdf
+2021/09/19 | 37805 | systems/win32/miktex/tm/packages/next/miktex-mthelp-bin-2.9.tar.lzma
+2021/09/19 | 37360 | systems/win32/miktex/tm/packages/next/miktex-upmendex-bin-x64.tar.lzma
+2021/09/19 | 37242 | systems/win32/miktex/tm/packages/next/miktex-posix-bin-x64-2.9.tar.lzma
+2021/09/19 | 36697 | support/light-latex-make/llmk.tex
+2021/09/19 | 36628 | systems/win32/miktex/tm/packages/next/miktex-chktex-bin-x64-2.9.tar.lzma
+2021/09/19 | 35691 | systems/win32/miktex/tm/packages/next/miktex-patgen-bin.tar.lzma
+2021/09/19 | 35086 | systems/win32/miktex/tm/packages/next/miktex-mspack-bin-x64-2.9.tar.lzma
+2021/09/19 | 34666 | systems/win32/miktex/tm/packages/next/miktex-axohelp-bin-x64-2.9.tar.lzma
+2021/09/19 | 345738 | systems/win32/miktex/tm/packages/next/miktex-xetex-bin-x64-2.9.tar.lzma
+2021/09/19 | 3376 | macros/luatex/generic/penlight/penlight.tex
+2021/09/19 | 33633 | systems/win32/miktex/tm/packages/next/miktex-uriparser-bin-x64-2.9.tar.lzma
+2021/09/19 | 33545 | systems/win32/miktex/tm/packages/next/miktex-upmendex-bin.tar.lzma
+2021/09/19 | 330787 | systems/win32/miktex/tm/packages/next/miktex-cairo-bin-2.9.tar.lzma
+2021/09/19 | 32674 | systems/win32/miktex/tm/packages/next/miktex-chktex-bin-2.9.tar.lzma
+2021/09/19 | 325044 | systems/win32/miktex/tm/packages/next/miktex-dvipdfmx-bin-x64-2.9.tar.lzma
+2021/09/19 | 3204664 | systems/texlive/tlnet/archive/luatexja.doc.r60541.tar.xz
+2021/09/19 | 31620 | systems/win32/miktex/tm/packages/next/miktex-makeindex-bin-x64-2.9.tar.lzma
+2021/09/19 | 31537 | systems/win32/miktex/tm/packages/next/miktex-posix-bin-2.9.tar.lzma
+2021/09/19 | 31349 | systems/win32/miktex/tm/packages/next/miktex-autosp-bin-x64-2.9.tar.lzma
+2021/09/19 | 31232 | systems/win32/miktex/tm/packages/next/miktex-mspack-bin-2.9.tar.lzma
+2021/09/19 | 312108 | systems/win32/miktex/tm/packages/next/miktex-xetex-bin-2.9.tar.lzma
+2021/09/19 | 30824 | systems/win32/miktex/tm/packages/next/miktex-bzip2-bin-x64-2.9.tar.lzma
+2021/09/19 | 30058 | systems/win32/miktex/tm/packages/next/miktex-lacheck-bin-x64-2.9.tar.lzma
+2021/09/19 | 29841 | systems/win32/miktex/tm/packages/next/miktex-axohelp-bin-2.9.tar.lzma
+2021/09/19 | 295204 | systems/win32/miktex/tm/packages/next/miktex-freetype2-bin-x64-2.9.tar.lzma
+2021/09/19 | 29037 | systems/win32/miktex/tm/packages/next/miktex-autosp-bin-2.9.tar.lzma
+2021/09/19 | 284580 | systems/win32/miktex/tm/packages/next/miktex-dvipdfmx-bin-2.9.tar.lzma
+2021/09/19 | 284023 | systems/win32/miktex/tm/packages/next/miktex-metapost-bin-x64-2.9.tar.lzma
+2021/09/19 | 28391 | systems/win32/miktex/tm/packages/next/miktex-lacheck-bin-2.9.tar.lzma
+2021/09/19 | 28177 | systems/win32/miktex/tm/packages/next/miktex-findtexmf-bin-x64-2.9.tar.lzma
+2021/09/19 | 2796726 | systems/win32/miktex/tm/packages/next/miktex-texworks-bin-x64-2.9.tar.lzma
+2021/09/19 | 27729 | systems/win32/miktex/tm/packages/next/miktex-uriparser-bin-2.9.tar.lzma
+2021/09/19 | 276 | systems/texlive/tlnet/archive/texlive-en.r60536.tar.xz
+2021/09/19 | 27390 | systems/win32/miktex/tm/packages/next/miktex-teckit-bin-x64-2.9.tar.lzma
+2021/09/19 | 27357 | systems/win32/miktex/tm/packages/next/miktex-makeindex-bin-2.9.tar.lzma
+2021/09/19 | 27198 | systems/win32/miktex/tm/packages/next/miktex-bzip2-bin-2.9.tar.lzma
+2021/09/19 | 2672612 | systems/win32/miktex/tm/packages/next/miktex-texworks-bin-2.9.tar.lzma
+2021/09/19 | 263958 | systems/win32/miktex/tm/packages/next/miktex-console-bin-x64-2.9.tar.lzma
+2021/09/19 | 260244 | systems/texlive/tlnet/archive/clicks.doc.r60539.tar.xz
+2021/09/19 | 258882 | systems/win32/miktex/tm/packages/next/miktex-freetype2-bin-2.9.tar.lzma
+2021/09/19 | 25629531 | systems/texlive/tlnet/install-tl.zip
+2021/09/19 | 25292 | systems/win32/miktex/tm/packages/next/miktex-teckit-bin-2.9.tar.lzma
+2021/09/19 | 250262 | systems/win32/miktex/tm/packages/next/miktex-metapost-bin-2.9.tar.lzma
+2021/09/19 | 25019 | systems/win32/miktex/tm/packages/next/miktex-findtexmf-bin-2.9.tar.lzma
+2021/09/19 | 24992 | systems/win32/miktex/tm/packages/next/miktex-fribidixetex-bin-x64-2.9.tar.lzma
+2021/09/19 | 24362 | language/korean/pmhanguljamo/doc/pmhanguljamo-doc.tex
+2021/09/19 | 243426 | systems/win32/miktex/tm/packages/next/miktex-pmx-bin-x64-2.9.tar.lzma
+2021/09/19 | 241811 | systems/win32/miktex/tm/packages/next/miktex-console-bin-2.9.tar.lzma
+2021/09/19 | 240974 | systems/win32/miktex/tm/packages/next/miktex-zzdb1-2.9.tar.lzma
+2021/09/19 | 240950 | systems/win32/miktex/tm/packages/miktex-zzdb1-2.9.tar.lzma
+2021/09/19 | 23903 | systems/win32/miktex/tm/packages/next/miktex-popt-bin-x64-2.9.tar.lzma
+2021/09/19 | 2341 | support/light-latex-make/llmk.1
+2021/09/19 | 23364 | systems/win32/miktex/tm/packages/next/miktex-devnag-bin-x64-2.9.tar.lzma
+2021/09/19 | 2312048 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.xz
+2021/09/19 | 22584 | systems/texlive/tlnet/archive/pdfextra.r60542.tar.xz
+2021/09/19 | 22048 | systems/win32/miktex/tm/packages/next/miktex-fribidixetex-bin-2.9.tar.lzma
+2021/09/19 | 215248 | systems/win32/miktex/tm/packages/next/miktex-metafont-bin-x64-2.9.tar.lzma
+2021/09/19 | 21225 | systems/win32/miktex/tm/packages/next/miktex-devnag-bin-2.9.tar.lzma
+2021/09/19 | 20875499 | systems/texlive/tlnet/install-tl-windows.exe
+2021/09/19 | 208047 | systems/win32/miktex/tm/packages/next/miktex-pmx-bin-2.9.tar.lzma
+2021/09/19 | 206603 | systems/win32/miktex/tm/packages/next/miktex-omegaware-bin-x64-2.9.tar.lzma
+2021/09/19 | 202797 | systems/win32/miktex/tm/packages/next/miktex-pixman-bin-x64-2.9.tar.lzma
+2021/09/19 | 20138 | systems/win32/miktex/tm/packages/next/miktex-popt-bin-2.9.tar.lzma
+2021/09/19 | 2009906 | systems/win32/miktex/tm/packages/next/miktex-luatex-bin-x64-2.9.tar.lzma
+2021/09/19 | 20067 | macros/luatex/generic/penlight/penlight.pdf
+2021/09/19 | 199732 | systems/win32/miktex/tm/packages/next/miktex-mpfr-bin-2.9.tar.lzma
+2021/09/19 | 198171 | systems/win32/miktex/tm/packages/next/miktex-pixman-bin-2.9.tar.lzma
+2021/09/19 | 196871 | systems/win32/miktex/tm/packages/next/miktex-metafont-bin-2.9.tar.lzma
+2021/09/19 | 196740 | systems/win32/miktex/tm/packages/next/miktex-mo-bin-x64-2.9.tar.lzma
+2021/09/19 | 195636 | systems/win32/miktex/tm/packages/next/miktex-curl-bin-x64-2.9.tar.lzma
+2021/09/19 | 194383 | systems/win32/miktex/tm/packages/next/miktex-omegaware-bin-2.9.tar.lzma
+2021/09/19 | 1939872 | systems/texlive/tlnet/archive/texlive-en.doc.r60536.tar.xz
+2021/09/19 | 190612 | systems/texlive/tlnet/archive/luatexja.r60541.tar.xz
+2021/09/19 | 1869 | info/dtk-bibliography/dtk-authoryear.bbx
+2021/09/19 | 18176 | systems/win32/miktex/tm/packages/next/miktex-tex2xindy-bin-x64-2.9.tar.lzma
+2021/09/19 | 180119 | systems/win32/miktex/tm/packages/next/miktex-mpfr-bin-x64-2.9.tar.lzma
+2021/09/19 | 180009 | systems/win32/miktex/tm/packages/next/miktex-curl-bin-2.9.tar.lzma
+2021/09/19 | 179998 | support/light-latex-make.zip
+2021/09/19 | 17980 | systems/win32/miktex/tm/packages/next/miktex-kpathsea-bin-x64-2.9.tar.lzma
+2021/09/19 | 178353 | systems/win32/miktex/tm/packages/next/miktex-mo-bin-2.9.tar.lzma
+2021/09/19 | 178108 | systems/texlive/tlnet/archive/exesheet.doc.r60537.tar.xz
+2021/09/19 | 17488 | systems/texlive/tlnet/archive/expkv-def.source.r60538.tar.xz
+2021/09/19 | 1713894 | systems/win32/miktex/tm/packages/next/miktex-luatex-bin-2.9.tar.lzma
+2021/09/19 | 169241 | systems/win32/miktex/tm/packages/next/miktex-tex-bin-x64-2.9.tar.lzma
+2021/09/19 | 16885163 | systems/texlive/tlnet/tlpkg/texlive.tlpdb
+2021/09/19 | 16413 | systems/win32/miktex/tm/packages/next/miktex-tex2xindy-bin-2.9.tar.lzma
+2021/09/19 | 16357 | systems/win32/miktex/tm/packages/next/miktex-kpathsea-bin-2.9.tar.lzma
+2021/09/19 | 16324 | systems/texlive/tlnet/archive/exesheet.source.r60537.tar.xz
+2021/09/19 | 1618 | macros/luatex/generic/luakeys/luakeys-debug.tex
+2021/09/19 | 159579 | systems/win32/miktex/tm/packages/next/miktex-hunspell-bin-x64-2.9.tar.lzma
+2021/09/19 | 159022 | macros/luatex/generic/luakeys.zip
+2021/09/19 | 1550 | macros/luatex/generic/penlight/penlight.sty
+2021/09/19 | 1548 | systems/texlive/tlnet/archive/clicks.r60539.tar.xz
+2021/09/19 | 154806 | systems/win32/miktex/tm/packages/next/miktex-tex-bin-2.9.tar.lzma
+2021/09/19 | 154425 | systems/win32/miktex/tm/packages/next/miktex-fontconfig-bin-x64-2.9.tar.lzma
+2021/09/19 | 153 | systems/texlive/tlnet/install-tl-windows.exe.sha512
+2021/09/19 | 153582 | macros/luatex/generic/luakeys/luakeys.pdf
+2021/09/19 | 152 | systems/texlive/tlnet/install-tl-unx.tar.gz.sha512
+2021/09/19 | 152964 | macros/luatex/generic/penlight.zip
+2021/09/19 | 152528 | macros/latex/contrib/tiscreen.zip
+2021/09/19 | 147372 | systems/win32/miktex/tm/packages/next/miktex-hunspell-bin-2.9.tar.lzma
+2021/09/19 | 14658 | macros/luatex/generic/luakeys/luakeys.lua
+2021/09/19 | 1464203 | systems/win32/miktex/tm/packages/next/miktex-runtime-bin-x64-2.9.tar.lzma
+2021/09/19 | 145 | systems/texlive/tlnet/install-tl.zip.sha512
+2021/09/19 | 144 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512
+2021/09/19 | 143726 | language/korean/pmhanguljamo/doc/munjangganghwa.jpg
+2021/09/19 | 14349 | systems/win32/miktex/tm/packages/next/miktex-zzip-bin-x64-2.9.tar.lzma
+2021/09/19 | 1433 | support/light-latex-make/llmk-logo-code.tex
+2021/09/19 | 1421867 | systems/win32/miktex/tm/packages/next/miktex-dvisvgm-bin-x64-2.9.tar.lzma
+2021/09/19 | 13750 | language/korean/pmhanguljamo/tex/pmhanguljamo.sty
+2021/09/19 | 136819 | systems/win32/miktex/tm/packages/next/miktex-mktex-bin-x64-2.9.tar.lzma
+2021/09/19 | 136816 | systems/win32/miktex/tm/packages/next/miktex-fontconfig-bin-2.9.tar.lzma
+2021/09/19 | 1357977 | systems/win32/miktex/tm/packages/next/miktex-zzdb2-2.9.tar.lzma
+2021/09/19 | 1356796 | systems/win32/miktex/tm/packages/miktex-zzdb2-2.9.tar.lzma
+2021/09/19 | 13465 | macros/luatex/generic/luakeys/luakeys.tex
+2021/09/19 | 13464 | systems/win32/miktex/tm/packages/next/miktex-zzip-bin-2.9.tar.lzma
+2021/09/19 | 1340929 | systems/win32/miktex/tm/packages/next/miktex-runtime-bin-2.9.tar.lzma
+2021/09/19 | 133864 | systems/win32/miktex/tm/packages/next/miktex-dvips-bin-x64-2.9.tar.lzma
+2021/09/19 | 1319 | language/korean/pmhanguljamo/README.md
+2021/09/19 | 1301194 | systems/win32/miktex/tm/packages/next/miktex-poppler-bin-x64-2.9.tar.lzma
+2021/09/19 | 1300 | macros/luatex/generic/penlight/README.md
+2021/09/19 | 1291300 | systems/win32/miktex/tm/packages/next/miktex-dvisvgm-bin-2.9.tar.lzma
+2021/09/19 | 128727 | language/korean/pmhanguljamo/doc/pmhanguljamo-doc.pdf
+2021/09/19 | 12851 | language/korean/pmhanguljamo/tex/pmhanguljamo-rrk.sty
+2021/09/19 | 1271 | macros/luatex/generic/luakeys/README.md
+2021/09/19 | 1261692 | systems/win32/miktex/tm/packages/miktex-zzdb3-2.9.tar.lzma
+2021/09/19 | 1260116 | systems/win32/miktex/tm/packages/next/miktex-zzdb3-2.9.tar.lzma
+2021/09/19 | 124282 | systems/win32/miktex/tm/packages/next/miktex-mktex-bin-2.9.tar.lzma
+2021/09/19 | 119820 | systems/win32/miktex/tm/packages/next/miktex-dvips-bin-2.9.tar.lzma
+2021/09/19 | 1169718 | systems/win32/miktex/tm/packages/next/miktex-poppler-bin-2.9.tar.lzma
+2021/09/19 | 1153 | systems/win32/miktex/tm/packages/next/pr.ini
+2021/09/19 | 115350 | systems/win32/miktex/tm/packages/next/miktex-gregorio-bin-x64-2.9.tar.lzma
+2021/09/19 | 112982 | systems/win32/miktex/tm/packages/next/miktex-epstopdf-bin-x64-2.9.tar.lzma
+2021/09/19 | 112086 | systems/win32/miktex/tm/packages/next/miktex-gd-bin-x64-2.9.tar.lzma
+2021/09/19 | 1091 | support/light-latex-make/LICENSE
+2021/09/19 | 109032 | systems/win32/miktex/tm/packages/next/miktex-jpeg-bin-x64-2.9.tar.lzma
+2021/09/19 | 108292 | systems/texlive/tlnet/archive/texlive-scripts.r60532.tar.xz
+2021/09/19 | 1077990 | systems/win32/miktex/tm/packages/next/miktex-asymptote-bin-x64-2.9.tar.lzma
+2021/09/19 | 105222 | systems/win32/miktex/tm/packages/next/miktex-texware-bin-x64-2.9.tar.lzma
+2021/09/19 | 103400 | systems/win32/miktex/tm/packages/next/miktex-lua53-bin-x64-2.9.tar.lzma
+2021/09/19 | 103205 | systems/win32/miktex/tm/packages/next/miktex-mfware-bin-x64-2.9.tar.lzma
+2021/09/19 | 100624 | systems/win32/miktex/tm/packages/next/miktex-epstopdf-bin-2.9.tar.lzma
2021/09/18 | 9765 | macros/luatex/generic/luatexja/src/addons/luatexja-ruby.sty
-2021/09/18 | 975018 | systems/win32/miktex/tm/packages/next/miktex-asymptote-bin-2.9.tar.lzma
-2021/09/18 | 97338 | systems/win32/miktex/tm/packages/next/miktex-gd-bin-2.9.tar.lzma
2021/09/18 | 9730 | systems/win32/miktex/tm/packages/luaxml__source.tar.lzma
2021/09/18 | 968940 | systems/win32/miktex/tm/packages/latexindent__doc.tar.lzma
-2021/09/18 | 96632 | systems/win32/miktex/tm/packages/next/miktex-gregorio-bin-2.9.tar.lzma
-2021/09/18 | 95403 | systems/win32/miktex/tm/packages/next/miktex-bibtex8bit-bin-x64-2.9.tar.lzma
-2021/09/18 | 94799 | systems/win32/miktex/tm/packages/next/miktex-jpeg-bin-2.9.tar.lzma
2021/09/18 | 945 | macros/luatex/generic/luatexja/tool/table_ivd_aj1.lua
2021/09/18 | 945 | macros/luatex/generic/luatexja/doc/jfm-testf.lua
-2021/09/18 | 94310 | systems/win32/miktex/tm/packages/next/miktex-lua53-bin-2.9.tar.lzma
2021/09/18 | 930244 | systems/win32/miktex/tm/packages/newtx__doc.tar.lzma
-2021/09/18 | 927 | systems/win32/miktex/tm/packages/pr.ini
2021/09/18 | 9236 | systems/texlive/tlnet/archive/njuthesis.r60529.tar.xz
2021/09/18 | 908344 | systems/texlive/tlnet/archive/tex4ht.r60530.tar.xz
2021/09/18 | 896 | macros/luatex/generic/luatexja/tool/otf-KozMinPr6N-Regular.txt
2021/09/18 | 885 | macros/luatex/generic/luatexja/src/patches/lltjp-stfloats.sty
-2021/09/18 | 87777 | systems/win32/miktex/tm/packages/next/miktex-png-bin-x64-2.9.tar.lzma
-2021/09/18 | 8767 | systems/win32/miktex/tm/packages/next/miktex-md5-bin-x64-2.9.tar.lzma
2021/09/18 | 8609 | macros/luatex/generic/luatexja/src/ltj-otf.lua
2021/09/18 | 8607 | macros/luatex/generic/luatexja/src/ltjltxdoc.dtx
-2021/09/18 | 83540 | systems/win32/miktex/tm/packages/next/miktex-bibtex8bit-bin-2.9.tar.lzma
2021/09/18 | 825 | macros/luatex/generic/luatexja/src/patches/lltjp-collcell.sty
-2021/09/18 | 815242 | systems/win32/miktex/tm/packages/next/miktex-pdftex-bin-x64-2.9.tar.lzma
2021/09/18 | 812 | systems/texlive/tlnet/archive/collection-pstricks.r60526.tar.xz
2021/09/18 | 8106 | macros/luatex/generic/luatexja/src/jfm-min.lua
-2021/09/18 | 80449 | systems/win32/miktex/tm/packages/next/miktex-freeglut-bin-x64-2.9.tar.lzma
2021/09/18 | 78995 | macros/generic/expkv-def/expkv-def.dtx
-2021/09/18 | 78773 | systems/win32/miktex/tm/packages/next/miktex-png-bin-2.9.tar.lzma
-2021/09/18 | 77952 | systems/win32/miktex/tm/packages/next/miktex-fonts-bin-x64-2.9.tar.lzma
2021/09/18 | 7790 | macros/luatex/generic/luatexja/tool/jfm-convert.lua
-2021/09/18 | 7720 | systems/win32/miktex/tm/packages/next/miktex-md5-bin-2.9.tar.lzma
2021/09/18 | 7691 | macros/luatex/generic/luatexja/src/ltj-unicode-ccfix.lua
-2021/09/18 | 76826 | systems/win32/miktex/tm/packages/next/miktex-mpmcli-bin-x64-2.9.tar.lzma
-2021/09/18 | 75792 | systems/win32/miktex/tm/packages/next/miktex-bibtex-bin-x64-2.9.tar.lzma
-2021/09/18 | 744117 | systems/win32/miktex/tm/packages/next/miktex-pdftex-bin-2.9.tar.lzma
-2021/09/18 | 74205 | systems/win32/miktex/tm/packages/next/miktex-lzma-bin-x64-2.9.tar.lzma
+2021/09/18 | 763 | macros/latex/contrib/tiscreen/README
2021/09/18 | 73725 | systems/win32/miktex/tm/packages/lua-uca__doc.tar.lzma
2021/09/18 | 7370 | macros/luatex/generic/luatexja/src/ltj-math.lua
-2021/09/18 | 73195 | systems/win32/miktex/tm/packages/next/miktex-fonts-bin-2.9.tar.lzma
-2021/09/18 | 73148 | systems/win32/miktex/tm/packages/next/miktex-graphite2-bin-x64-2.9.tar.lzma
2021/09/18 | 72763 | systems/win32/miktex/tm/packages/lua-uca.tar.lzma
2021/09/18 | 7234446 | systems/win32/miktex/tm/packages/latex2nemeth.tar.lzma
2021/09/18 | 7234364 | systems/texlive/tlnet/archive/latex2nemeth.r60528.tar.xz
-2021/09/18 | 72208 | systems/win32/miktex/tm/packages/next/miktex-ps2pk-bin-x64-2.9.tar.lzma
-2021/09/18 | 71668 | systems/win32/miktex/tm/packages/next/miktex-freeglut-bin-2.9.tar.lzma
2021/09/18 | 714993 | macros/luatex/optex/pdfextra.zip
-2021/09/18 | 70648 | systems/win32/miktex/tm/packages/next/miktex-web-bin-x64-2.9.tar.lzma
2021/09/18 | 7026 | macros/luatex/generic/luatexja/src/ltj-pretreat.lua
2021/09/18 | 702212 | systems/texlive/tlnet/archive/njuthesis.doc.r60529.tar.xz
-2021/09/18 | 69913 | systems/win32/miktex/tm/packages/next/miktex-tex4ht-bin-x64-2.9.tar.lzma
2021/09/18 | 694840 | macros/luatex/generic/luatexja/doc/luatexja-en.pdf
2021/09/18 | 69366 | macros/luatex/generic/luatexja/doc/ltjltxdoc.pdf
2021/09/18 | 6891 | macros/luatex/generic/luatexja/src/addons/luatexja-otf.sty
+2021/09/18 | 6845 | systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl
2021/09/18 | 68418 | systems/win32/miktex/tm/packages/njuthesis.tar.lzma
2021/09/18 | 6822 | systems/win32/miktex/tm/packages/pst-tools.tar.lzma
-2021/09/18 | 681250 | systems/win32/miktex/tm/packages/next/miktex-lcdf-typetools-bin-x64-2.9.tar.lzma
-2021/09/18 | 6811635 | systems/texlive/tlnet/install-tl-unx.tar.gz
-2021/09/18 | 68023 | systems/win32/miktex/tm/packages/next/miktex-mkfntmap-bin-x64-2.9.tar.lzma
-2021/09/18 | 67819 | systems/win32/miktex/tm/packages/next/miktex-bibtex-bin-2.9.tar.lzma
2021/09/18 | 6768 | systems/win32/miktex/tm/packages/mindflow__source.tar.lzma
-2021/09/18 | 67619 | systems/win32/miktex/tm/packages/next/miktex-lzma-bin-2.9.tar.lzma
-2021/09/18 | 67483 | systems/win32/miktex/tm/packages/next/miktex-mpmcli-bin-2.9.tar.lzma
2021/09/18 | 6726284 | systems/win32/miktex/tm/packages/pst-mirror__doc.tar.lzma
2021/09/18 | 66986 | systems/win32/miktex/tm/packages/latexindent.tar.lzma
-2021/09/18 | 65786 | systems/win32/miktex/tm/packages/next/miktex-texify-bin-x64-2.9.tar.lzma
-2021/09/18 | 65618 | systems/win32/miktex/tm/packages/next/miktex-web-bin-2.9.tar.lzma
2021/09/18 | 6497 | macros/luatex/generic/luatexja/src/patches/lltjdefs.sty
2021/09/18 | 647086 | systems/win32/miktex/tm/packages/njuthesis__doc.tar.lzma
-2021/09/18 | 64427 | systems/win32/miktex/tm/packages/next/miktex-graphite2-bin-2.9.tar.lzma
-2021/09/18 | 64185 | systems/win32/miktex/tm/packages/next/miktex-ps2pk-bin-2.9.tar.lzma
-2021/09/18 | 63776 | systems/win32/miktex/tm/packages/next/miktex-tex4ht-bin-2.9.tar.lzma
2021/09/18 | 63100 | systems/win32/miktex/tm/packages/pst-mirror__source.tar.lzma
-2021/09/18 | 629398 | systems/win32/miktex/tm/packages/next/miktex-lcdf-typetools-bin-2.9.tar.lzma
-2021/09/18 | 62784 | systems/win32/miktex/tm/packages/next/miktex-fmt-bin-x64-2.9.tar.lzma
-2021/09/18 | 624652 | systems/win32/miktex/tm/packages/next/miktex-yap-bin-x64-2.9.tar.lzma
-2021/09/18 | 62228 | systems/win32/miktex/tm/packages/next/miktex-ttf2pk2-bin-x64-2.9.tar.lzma
2021/09/18 | 6203 | macros/luatex/generic/luatexja/src/ltj-base.sty
2021/09/18 | 62008 | macros/latex/contrib/exesheet/exesheet.dtx
-2021/09/18 | 61814 | systems/win32/miktex/tm/packages/next/miktex-cweb-bin-x64-2.9.tar.lzma
2021/09/18 | 617 | macros/luatex/generic/luatexja/src/patches/lltjp-array.sty
2021/09/18 | 6164 | systems/win32/miktex/tm/packages/pst-tools__source.tar.lzma
2021/09/18 | 61640 | systems/win32/miktex/tm/packages/siunitx.tar.lzma
-2021/09/18 | 614598 | systems/win32/miktex/tm/packages/files.csv.lzma
-2021/09/18 | 614469 | systems/win32/miktex/tm/packages/next/files.csv.lzma
2021/09/18 | 608917 | support/TeX4ht/source/tex4ht-html4.tex
-2021/09/18 | 60636 | systems/win32/miktex/tm/packages/next/miktex-mkfntmap-bin-2.9.tar.lzma
2021/09/18 | 6060 | macros/luatex/generic/luatexja/tool/kyoikukanjiChars.tex
-2021/09/18 | 60039 | systems/win32/miktex/tm/packages/next/miktex-tdsutil-bin-x64.tar.lzma
-2021/09/18 | 596930 | systems/win32/miktex/tm/packages/next/miktex-libressl-bin-x64-2.9.tar.lzma
2021/09/18 | 5940 | macros/luatex/generic/luatexja/src/ltj-lotf_aux.lua
-2021/09/18 | 589843 | systems/win32/miktex/tm/packages/next/miktex-yap-bin-2.9.tar.lzma
2021/09/18 | 5850 | macros/luatex/generic/luatexja/src/patches/lltjp-fontspec.sty
2021/09/18 | 580 | macros/luatex/generic/luatexja/tool/blocks2defcharrange.rb
-2021/09/18 | 57728 | systems/win32/miktex/tm/packages/next/miktex-psutils-bin-x64-2.9.tar.lzma
-2021/09/18 | 57548 | systems/win32/miktex/tm/packages/next/miktex-bibarts-bin-x64-2.9.tar.lzma
2021/09/18 | 5720 | systems/win32/miktex/tm/packages/zref-check.tar.lzma
-2021/09/18 | 57139 | systems/win32/miktex/tm/packages/next/miktex-xml2pmx-bin-x64.tar.lzma
-2021/09/18 | 57013 | systems/win32/miktex/tm/packages/next/miktex-texify-bin-2.9.tar.lzma
+2021/09/18 | 568734 | macros/latex/contrib/schooldocs/schooldocs.pdf
2021/09/18 | 5682 | macros/luatex/generic/luatexja/src/patches/lltjp-tascmac.sty
-2021/09/18 | 56685 | systems/win32/miktex/tm/packages/next/miktex-cjkutils-bin-x64-2.9.tar.lzma
-2021/09/18 | 566529 | systems/win32/miktex/tm/packages/next/miktex-harfbuzz-bin-x64-2.9.tar.lzma
-2021/09/18 | 56513 | systems/win32/miktex/tm/packages/next/miktex-tdsutil-bin.tar.lzma
-2021/09/18 | 56476 | systems/win32/miktex/tm/packages/next/miktex-arctrl-bin-x64-2.9.tar.lzma
-2021/09/18 | 56355 | systems/win32/miktex/tm/packages/next/miktex-ttf2pk2-bin-2.9.tar.lzma
-2021/09/18 | 56318 | systems/win32/miktex/tm/packages/next/miktex-fmt-bin-2.9.tar.lzma
-2021/09/18 | 55498 | systems/win32/miktex/tm/packages/next/miktex-xml2pmx-bin.tar.lzma
+2021/09/18 | 5642 | macros/luatex/generic/penlight/pl/README-pl.md
2021/09/18 | 5544 | macros/luatex/generic/luatexja/src/ltj-latex.sty
2021/09/18 | 55255 | macros/luatex/generic/luatexja/src/ltj-jfmglue.lua
2021/09/18 | 551 | macros/luatex/generic/luatexja/tool/jfm-readable.rb
2021/09/18 | 5516 | systems/texlive/tlnet/archive/zref-check.r60527.tar.xz
-2021/09/18 | 54631 | systems/win32/miktex/tm/packages/next/miktex-cweb-bin-2.9.tar.lzma
2021/09/18 | 54427 | macros/luatex/generic/luatexja/src/ltj-jisx0208.lua
2021/09/18 | 534065 | macros/luatex/generic/luatexja/doc/luatexja.dtx
-2021/09/18 | 531850 | systems/win32/miktex/tm/packages/next/miktex-libressl-bin-2.9.tar.lzma
2021/09/18 | 52633 | macros/luatex/generic/luatexja/doc/luatexja-ruby.tex
-2021/09/18 | 52523 | systems/win32/miktex/tm/packages/next/miktex-m-tx-bin-x64-2.9.tar.lzma
2021/09/18 | 5222 | macros/luatex/generic/luatexja/src/ltj-debug.lua
-2021/09/18 | 51751 | systems/win32/miktex/tm/packages/next/miktex-synctex-bin-x64-2.9.tar.lzma
2021/09/18 | 513483 | macros/latex/contrib/readarray.zip
-2021/09/18 | 51309 | systems/win32/miktex/tm/packages/next/miktex-cjkutils-bin-2.9.tar.lzma
-2021/09/18 | 51203 | systems/win32/miktex/tm/packages/next/miktex-dvicopy-bin-x64-2.9.tar.lzma
-2021/09/18 | 51113 | systems/win32/miktex/tm/packages/next/miktex-arctrl-bin-2.9.tar.lzma
-2021/09/18 | 510587 | systems/win32/miktex/tm/packages/next/miktex-log4cxx-bin-x64-2.9.tar.lzma
-2021/09/18 | 50394 | systems/win32/miktex/tm/packages/next/miktex-psutils-bin-2.9.tar.lzma
-2021/09/18 | 50152 | systems/win32/miktex/tm/packages/next/miktex-bibarts-bin-2.9.tar.lzma
2021/09/18 | 49557 | macros/luatex/generic/luatexja/src/patches/lltjfont.sty
-2021/09/18 | 48 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.md5
2021/09/18 | 4871 | macros/luatex/generic/luatexja/src/ltj-stack.lua
2021/09/18 | 48445 | macros/luatex/generic/luatexja/src/ltj-jfont.lua
-2021/09/18 | 483654 | systems/win32/miktex/tm/packages/next/miktex-harfbuzz-bin-2.9.tar.lzma
-2021/09/18 | 48251 | systems/win32/miktex/tm/packages/next/miktex-expat-bin-x64-2.9.tar.lzma
2021/09/18 | 4808 | macros/luatex/generic/luatexja/src/jfm-tmin.lua
-2021/09/18 | 476923 | systems/win32/miktex/tm/packages/next/miktex-log4cxx-bin-2.9.tar.lzma
-2021/09/18 | 46907 | systems/win32/miktex/tm/packages/next/miktex-mtprint-bin-x64-2.9.tar.lzma
-2021/09/18 | 461808 | systems/win32/miktex/tm/packages/next/miktex-doc.tar.lzma
-2021/09/18 | 45971 | systems/win32/miktex/tm/packages/next/miktex-m-tx-bin-2.9.tar.lzma
-2021/09/18 | 45904 | systems/win32/miktex/tm/packages/next/miktex-dvicopy-bin-2.9.tar.lzma
2021/09/18 | 457434 | macros/luatex/generic/luatexja/doc/ltjsclasses.pdf
-2021/09/18 | 455 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc
-2021/09/18 | 455 | systems/texlive/tlnet/install-tl.zip.sha512.asc
-2021/09/18 | 455 | systems/texlive/tlnet/install-tl-windows.exe.sha512.asc
-2021/09/18 | 455 | systems/texlive/tlnet/install-tl-unx.tar.gz.sha512.asc
2021/09/18 | 4559 | macros/luatex/generic/luatexja/src/addons/luatexja-adjust.sty
-2021/09/18 | 45313 | systems/win32/miktex/tm/packages/next/miktex-dvipng-bin-x64-2.9.tar.lzma
2021/09/18 | 4452 | macros/luatex/generic/luatexja/doc/jfm-ujisc33.lua
2021/09/18 | 4421 | systems/win32/miktex/tm/packages/unicode-data__doc.tar.lzma
+2021/09/18 | 4414 | macros/latex/contrib/tiscreen/doc/tiscreen-doc.tex
2021/09/18 | 4408 | macros/luatex/generic/luatexja/src/ltj-plain.sty
-2021/09/18 | 43425 | systems/win32/miktex/tm/packages/next/miktex-zlib-bin-x64-2.9.tar.lzma
-2021/09/18 | 43026 | systems/win32/miktex/tm/packages/next/miktex-expat-bin-2.9.tar.lzma
-2021/09/18 | 43003 | systems/win32/miktex/tm/packages/next/miktex-gsf2pk-bin-x64-2.9.tar.lzma
-2021/09/18 | 42890 | systems/win32/miktex/tm/packages/next/miktex-mtprint-bin-2.9.tar.lzma
-2021/09/18 | 42712 | systems/win32/miktex/tm/packages/next/miktex-mthelp-bin-x64-2.9.tar.lzma
2021/09/18 | 4262 | macros/luatex/generic/luatexja/tool/kyoikukanji.txt
-2021/09/18 | 42385 | systems/win32/miktex/tm/packages/next/miktex-synctex-bin-2.9.tar.lzma
2021/09/18 | 4196 | macros/luatex/generic/luatexja/src/patches/lltjp-atbegshi.sty
2021/09/18 | 419541 | install/macros/generic/expkv-def.tds.zip
2021/09/18 | 413556 | macros/generic/expkv-def.zip
-2021/09/18 | 41194 | systems/win32/miktex/tm/packages/next/miktex-dvipng-bin-2.9.tar.lzma
2021/09/18 | 407494 | macros/generic/expkv-def/expkv-def.pdf
2021/09/18 | 405 | macros/luatex/generic/luatexja/src/patches/lltjp-everyshi.sty
-2021/09/18 | 40230 | systems/win32/miktex/tm/packages/next/miktex-patgen-bin-x64.tar.lzma
2021/09/18 | 400691 | macros/luatex/generic/luatexja/doc/ltjclasses.pdf
-2021/09/18 | 400152 | systems/texlive/tlnet/archive/texlive-scripts.doc.r60525.tar.xz
2021/09/18 | 39711 | macros/luatex/generic/luatexja/src/ltj-direction.lua
2021/09/18 | 3961 | macros/luatex/generic/luatexja/src/ltj-charrange.lua
-2021/09/18 | 392792 | systems/win32/miktex/tm/packages/next/miktex-cairo-bin-x64-2.9.tar.lzma
-2021/09/18 | 39128 | systems/win32/miktex/tm/packages/next/miktex-zlib-bin-2.9.tar.lzma
2021/09/18 | 385 | macros/luatex/generic/luatexja/src/ltjsclasses.ins
2021/09/18 | 3843 | systems/win32/miktex/tm/packages/lua-uca__source.tar.lzma
-2021/09/18 | 38141 | systems/win32/miktex/tm/packages/next/miktex-gsf2pk-bin-2.9.tar.lzma
2021/09/18 | 381296 | macros/luatex/generic/luatexja/doc/luatexja-ruby.pdf
+2021/09/18 | 38020 | macros/latex/contrib/schooldocs/schooldocs.dtx
2021/09/18 | 3797 | macros/luatex/generic/luatexja/README
2021/09/18 | 3780655 | macros/luatex/generic/luatexja.zip
-2021/09/18 | 37805 | systems/win32/miktex/tm/packages/next/miktex-mthelp-bin-2.9.tar.lzma
2021/09/18 | 3763 | systems/win32/miktex/tm/packages/huawei__source.tar.lzma
2021/09/18 | 3755 | macros/luatex/generic/luatexja/src/ltj-lineskip.lua
2021/09/18 | 3748 | macros/luatex/generic/luatexja/src/ltj-compat.lua
-2021/09/18 | 37359 | systems/win32/miktex/tm/packages/next/miktex-upmendex-bin-x64.tar.lzma
-2021/09/18 | 37240 | systems/win32/miktex/tm/packages/next/miktex-posix-bin-x64-2.9.tar.lzma
2021/09/18 | 3693 | macros/luatex/generic/luatexja/src/patches/lltjp-siunitx.sty
-2021/09/18 | 36630 | systems/win32/miktex/tm/packages/next/miktex-chktex-bin-x64-2.9.tar.lzma
-2021/09/18 | 35691 | systems/win32/miktex/tm/packages/next/miktex-patgen-bin.tar.lzma
2021/09/18 | 355 | macros/luatex/generic/luatexja/tool/otf-AdobeMingStd-Light.txt
2021/09/18 | 355 | macros/luatex/generic/luatexja/doc/luatexja.ins
-2021/09/18 | 35088 | systems/win32/miktex/tm/packages/next/miktex-mspack-bin-x64-2.9.tar.lzma
2021/09/18 | 349992 | systems/win32/miktex/tm/packages/crumbs__doc.tar.lzma
-2021/09/18 | 34666 | systems/win32/miktex/tm/packages/next/miktex-axohelp-bin-x64-2.9.tar.lzma
-2021/09/18 | 345739 | systems/win32/miktex/tm/packages/next/miktex-xetex-bin-x64-2.9.tar.lzma
2021/09/18 | 3451 | macros/luatex/generic/luatexja/src/luatexja-compat.sty
2021/09/18 | 339242 | systems/win32/miktex/tm/packages/huawei__doc.tar.lzma
2021/09/18 | 3371 | macros/luatex/generic/luatexja/src/patches/lltjp-xunicode.sty
-2021/09/18 | 33632 | systems/win32/miktex/tm/packages/next/miktex-uriparser-bin-x64-2.9.tar.lzma
-2021/09/18 | 33543 | systems/win32/miktex/tm/packages/next/miktex-upmendex-bin.tar.lzma
2021/09/18 | 33461 | macros/luatex/generic/luatexja/src/addons/luatexja-ajmacros.sty
2021/09/18 | 3313 | macros/luatex/generic/luatexja/tool/ltj-unicode-ccfix_make2.pl
-2021/09/18 | 330788 | systems/win32/miktex/tm/packages/next/miktex-cairo-bin-2.9.tar.lzma
2021/09/18 | 330496 | support/TeX4ht/source/tex4ht-ooffice.tex
2021/09/18 | 326 | macros/luatex/generic/luatexja/tool/otf-AdobeMyungjoStd-Medium.txt
-2021/09/18 | 32673 | systems/win32/miktex/tm/packages/next/miktex-chktex-bin-2.9.tar.lzma
-2021/09/18 | 325042 | systems/win32/miktex/tm/packages/next/miktex-dvipdfmx-bin-x64-2.9.tar.lzma
2021/09/18 | 3238 | macros/luatex/generic/luatexja/doc/jfm-test.lua
2021/09/18 | 32250 | systems/win32/miktex/tm/packages/luaxml.tar.lzma
2021/09/18 | 32099 | macros/luatex/generic/luatexja/src/ltj-ruby.lua
2021/09/18 | 317 | macros/luatex/generic/luatexja/tool/otf-AdobeSongStd-Light.txt
-2021/09/18 | 31621 | systems/win32/miktex/tm/packages/next/miktex-makeindex-bin-x64-2.9.tar.lzma
-2021/09/18 | 31539 | systems/win32/miktex/tm/packages/next/miktex-posix-bin-2.9.tar.lzma
2021/09/18 | 3140 | macros/latex/contrib/clicks/clicks.tex
-2021/09/18 | 31351 | systems/win32/miktex/tm/packages/next/miktex-autosp-bin-x64-2.9.tar.lzma
2021/09/18 | 31321 | systems/win32/miktex/tm/packages/nicematrix.tar.lzma
-2021/09/18 | 31231 | systems/win32/miktex/tm/packages/next/miktex-mspack-bin-2.9.tar.lzma
-2021/09/18 | 312107 | systems/win32/miktex/tm/packages/next/miktex-xetex-bin-2.9.tar.lzma
+2021/09/18 | 3110 | macros/latex/contrib/tiscreen/tex/tiscreen.sty
2021/09/18 | 3102 | systems/win32/miktex/tm/packages/mlacls.tar.lzma
2021/09/18 | 3100 | macros/luatex/generic/luatexja/tool/table_jisx0208.tex
-2021/09/18 | 30823 | systems/win32/miktex/tm/packages/next/miktex-bzip2-bin-x64-2.9.tar.lzma
2021/09/18 | 306251 | systems/win32/miktex/tm/packages/unicode-data.tar.lzma
2021/09/18 | 3044003 | systems/win32/miktex/tm/packages/mlacls__doc.tar.lzma
-2021/09/18 | 30059 | systems/win32/miktex/tm/packages/next/miktex-lacheck-bin-x64-2.9.tar.lzma
-2021/09/18 | 29841 | systems/win32/miktex/tm/packages/next/miktex-axohelp-bin-2.9.tar.lzma
2021/09/18 | 29756 | systems/win32/miktex/tm/packages/newtx__source.tar.lzma
2021/09/18 | 29536 | macros/luatex/generic/luatexja/src/luatexja-core.sty
-2021/09/18 | 295202 | systems/win32/miktex/tm/packages/next/miktex-freetype2-bin-x64-2.9.tar.lzma
2021/09/18 | 2918 | systems/win32/miktex/tm/packages/mindflow.tar.lzma
2021/09/18 | 29181 | macros/luatex/generic/luatexja/src/addons/luatexja-preset.sty
-2021/09/18 | 29037 | systems/win32/miktex/tm/packages/next/miktex-autosp-bin-2.9.tar.lzma
-2021/09/18 | 284580 | systems/win32/miktex/tm/packages/next/miktex-dvipdfmx-bin-2.9.tar.lzma
-2021/09/18 | 284025 | systems/win32/miktex/tm/packages/next/miktex-metapost-bin-x64-2.9.tar.lzma
-2021/09/18 | 28389 | systems/win32/miktex/tm/packages/next/miktex-lacheck-bin-2.9.tar.lzma
-2021/09/18 | 28181 | systems/win32/miktex/tm/packages/next/miktex-findtexmf-bin-x64-2.9.tar.lzma
2021/09/18 | 27 | macros/latex/contrib/clicks/DEPENDS.txt
-2021/09/18 | 2796725 | systems/win32/miktex/tm/packages/next/miktex-texworks-bin-x64-2.9.tar.lzma
2021/09/18 | 2787 | systems/win32/miktex/tm/packages/crumbs__source.tar.lzma
-2021/09/18 | 27727 | systems/win32/miktex/tm/packages/next/miktex-uriparser-bin-2.9.tar.lzma
-2021/09/18 | 27391 | systems/win32/miktex/tm/packages/next/miktex-teckit-bin-x64-2.9.tar.lzma
-2021/09/18 | 27358 | systems/win32/miktex/tm/packages/next/miktex-makeindex-bin-2.9.tar.lzma
2021/09/18 | 27278 | systems/win32/miktex/tm/packages/luamplib__source.tar.lzma
-2021/09/18 | 27197 | systems/win32/miktex/tm/packages/next/miktex-bzip2-bin-2.9.tar.lzma
2021/09/18 | 26934 | macros/luatex/generic/luatexja/src/lltjext.sty
-2021/09/18 | 2672610 | systems/win32/miktex/tm/packages/next/miktex-texworks-bin-2.9.tar.lzma
-2021/09/18 | 263957 | systems/win32/miktex/tm/packages/next/miktex-console-bin-x64-2.9.tar.lzma
2021/09/18 | 261843 | systems/win32/miktex/tm/packages/huawei.tar.lzma
2021/09/18 | 2613 | systems/win32/miktex/tm/packages/ffcode__source.tar.lzma
2021/09/18 | 260709 | macros/latex/contrib/clicks.zip
2021/09/18 | 2601 | macros/luatex/generic/luatexja/src/jfm-banjiao.lua
-2021/09/18 | 258880 | systems/win32/miktex/tm/packages/next/miktex-freetype2-bin-2.9.tar.lzma
2021/09/18 | 2586 | macros/luatex/generic/luatexja/src/jfm-kaiming.lua
2021/09/18 | 258472 | systems/win32/miktex/tm/packages/clicks__doc.tar.lzma
2021/09/18 | 2575 | macros/luatex/generic/luatexja/src/jfm-quanjiao.lua
2021/09/18 | 257190 | macros/latex/contrib/clicks/clicks.pdf
-2021/09/18 | 25629533 | systems/texlive/tlnet/install-tl.zip
-2021/09/18 | 25290 | systems/win32/miktex/tm/packages/next/miktex-teckit-bin-2.9.tar.lzma
-2021/09/18 | 250264 | systems/win32/miktex/tm/packages/next/miktex-metapost-bin-2.9.tar.lzma
-2021/09/18 | 25019 | systems/win32/miktex/tm/packages/next/miktex-findtexmf-bin-2.9.tar.lzma
-2021/09/18 | 24995 | systems/win32/miktex/tm/packages/next/miktex-fribidixetex-bin-x64-2.9.tar.lzma
-2021/09/18 | 243427 | systems/win32/miktex/tm/packages/next/miktex-pmx-bin-x64-2.9.tar.lzma
-2021/09/18 | 241812 | systems/win32/miktex/tm/packages/next/miktex-console-bin-2.9.tar.lzma
-2021/09/18 | 240961 | systems/win32/miktex/tm/packages/next/miktex-zzdb1-2.9.tar.lzma
-2021/09/18 | 240935 | systems/win32/miktex/tm/packages/miktex-zzdb1-2.9.tar.lzma
-2021/09/18 | 23901 | systems/win32/miktex/tm/packages/next/miktex-popt-bin-x64-2.9.tar.lzma
2021/09/18 | 2374 | macros/luatex/generic/luatexja/src/patches/lltjp-unicode-math.sty
2021/09/18 | 234318 | macros/latex/contrib/exesheet/exesheet.pdf
2021/09/18 | 2342 | macros/luatex/generic/luatexja/src/jfm-jis.lua
-2021/09/18 | 23365 | systems/win32/miktex/tm/packages/next/miktex-devnag-bin-x64-2.9.tar.lzma
2021/09/18 | 2332 | systems/win32/miktex/tm/packages/crumbs.tar.lzma
-2021/09/18 | 2311652 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.xz
2021/09/18 | 2274 | macros/latex/contrib/clicks/clicks.sty
2021/09/18 | 22294 | macros/luatex/generic/luatexja/src/luatexja.lua
2021/09/18 | 2207 | macros/luatex/generic/luatexja/src/jfm-CCT.lua
-2021/09/18 | 22049 | systems/win32/miktex/tm/packages/next/miktex-fribidixetex-bin-2.9.tar.lzma
2021/09/18 | 2193 | macros/luatex/generic/luatexja/src/addons/luatexja-zhfonts.sty
2021/09/18 | 21524 | systems/win32/miktex/tm/packages/luapstricks.tar.lzma
-2021/09/18 | 215248 | systems/win32/miktex/tm/packages/next/miktex-metafont-bin-x64-2.9.tar.lzma
2021/09/18 | 215064 | systems/win32/miktex/tm/packages/mindflow__doc.tar.lzma
2021/09/18 | 21316 | systems/texlive/tlnet/archive/luapstricks.r60526.tar.xz
-2021/09/18 | 21225 | systems/win32/miktex/tm/packages/next/miktex-devnag-bin-2.9.tar.lzma
2021/09/18 | 21191 | systems/win32/miktex/tm/packages/zref-check__source.tar.lzma
2021/09/18 | 2092456 | systems/win32/miktex/tm/packages/kpfonts-otf__doc.tar.lzma
-2021/09/18 | 20877598 | systems/texlive/tlnet/install-tl-windows.exe
-2021/09/18 | 208049 | systems/win32/miktex/tm/packages/next/miktex-pmx-bin-2.9.tar.lzma
-2021/09/18 | 206605 | systems/win32/miktex/tm/packages/next/miktex-omegaware-bin-x64-2.9.tar.lzma
2021/09/18 | 20613 | systems/win32/miktex/tm/packages/kpfonts-otf__source.tar.lzma
2021/09/18 | 2057 | macros/luatex/generic/luatexja/src/patches/lltjp-microtype.sty
2021/09/18 | 2052 | systems/win32/miktex/tm/packages/ffcode.tar.lzma
2021/09/18 | 2037095 | systems/win32/miktex/tm/packages/siunitx__doc.tar.lzma
2021/09/18 | 2033 | systems/win32/miktex/tm/packages/clicks__source.tar.lzma
-2021/09/18 | 202797 | systems/win32/miktex/tm/packages/next/miktex-pixman-bin-x64-2.9.tar.lzma
-2021/09/18 | 20135 | systems/win32/miktex/tm/packages/next/miktex-popt-bin-2.9.tar.lzma
2021/09/18 | 20106 | systems/win32/miktex/tm/packages/njuthesis__source.tar.lzma
2021/09/18 | 200 | macros/luatex/generic/luatexja/src/jfm-prop.lua
-2021/09/18 | 2009903 | systems/win32/miktex/tm/packages/next/miktex-luatex-bin-x64-2.9.tar.lzma
2021/09/18 | 200880 | macros/latex/contrib/exesheet.zip
-2021/09/18 | 199731 | systems/win32/miktex/tm/packages/next/miktex-mpfr-bin-2.9.tar.lzma
-2021/09/18 | 198170 | systems/win32/miktex/tm/packages/next/miktex-pixman-bin-2.9.tar.lzma
+2021/09/18 | 1997 | macros/latex/contrib/schooldocs/schooldocs.ins
2021/09/18 | 19740 | systems/texlive/tlnet/archive/zref-check.source.r60527.tar.xz
2021/09/18 | 19732 | systems/texlive/tlnet/archive/njuthesis.source.r60529.tar.xz
2021/09/18 | 196 | macros/luatex/generic/luatexja/src/jfm-propw.lua
2021/09/18 | 196 | macros/luatex/generic/luatexja/src/jfm-propv.lua
-2021/09/18 | 196872 | systems/win32/miktex/tm/packages/next/miktex-metafont-bin-2.9.tar.lzma
-2021/09/18 | 196742 | systems/win32/miktex/tm/packages/next/miktex-mo-bin-x64-2.9.tar.lzma
2021/09/18 | 1959 | systems/win32/miktex/tm/packages/luapstricks__doc.tar.lzma
2021/09/18 | 19572 | macros/luatex/generic/luatexja/src/ltj-adjust.lua
-2021/09/18 | 195635 | systems/win32/miktex/tm/packages/next/miktex-curl-bin-x64-2.9.tar.lzma
2021/09/18 | 19490 | systems/win32/miktex/tm/packages/latex2nemeth__doc.tar.lzma
-2021/09/18 | 194383 | systems/win32/miktex/tm/packages/next/miktex-omegaware-bin-2.9.tar.lzma
2021/09/18 | 192 | macros/luatex/generic/luatexja/src/jfm-mono.lua
2021/09/18 | 189 | macros/luatex/generic/luatexja/src/patches/lltjp-preview.sty
2021/09/18 | 189820 | systems/win32/miktex/tm/packages/ffcode__doc.tar.lzma
@@ -311,97 +423,53 @@
2021/09/18 | 18722 | macros/luatex/generic/luatexja/src/ltj-kinsoku.tex
2021/09/18 | 18543 | macros/luatex/generic/luatexja/src/patches/lltjp-geometry.sty
2021/09/18 | 1832 | macros/luatex/generic/luatexja/src/luatexja.sty
-2021/09/18 | 18174 | systems/win32/miktex/tm/packages/next/miktex-tex2xindy-bin-x64-2.9.tar.lzma
-2021/09/18 | 180120 | systems/win32/miktex/tm/packages/next/miktex-mpfr-bin-x64-2.9.tar.lzma
-2021/09/18 | 180011 | systems/win32/miktex/tm/packages/next/miktex-curl-bin-2.9.tar.lzma
-2021/09/18 | 17980 | systems/win32/miktex/tm/packages/next/miktex-kpathsea-bin-x64-2.9.tar.lzma
2021/09/18 | 1788905 | systems/win32/miktex/tm/packages/nicematrix__doc.tar.lzma
-2021/09/18 | 178352 | systems/win32/miktex/tm/packages/next/miktex-mo-bin-2.9.tar.lzma
2021/09/18 | 177581 | macros/luatex/generic/luatexja/src/ltjclasses.dtx
2021/09/18 | 1765 | macros/luatex/generic/luatexja/src/ltj-inputbuf.lua
2021/09/18 | 1740 | systems/win32/miktex/tm/packages/clicks.tar.lzma
-2021/09/18 | 1713895 | systems/win32/miktex/tm/packages/next/miktex-luatex-bin-2.9.tar.lzma
-2021/09/18 | 169242 | systems/win32/miktex/tm/packages/next/miktex-tex-bin-x64-2.9.tar.lzma
-2021/09/18 | 16884922 | systems/texlive/tlnet/tlpkg/texlive.tlpdb
2021/09/18 | 166557 | macros/luatex/generic/luatexja/doc/lltjp-geometry.pdf
2021/09/18 | 165264 | systems/texlive/tlnet/archive/latex2nemeth.doc.r60528.tar.xz
-2021/09/18 | 16414 | systems/win32/miktex/tm/packages/next/miktex-tex2xindy-bin-2.9.tar.lzma
2021/09/18 | 1637 | macros/luatex/generic/luatexja/src/addons/luatexja-fontspec.sty
-2021/09/18 | 16355 | systems/win32/miktex/tm/packages/next/miktex-kpathsea-bin-2.9.tar.lzma
2021/09/18 | 162360 | support/TeX4ht/source/ChangeLog
-2021/09/18 | 159579 | systems/win32/miktex/tm/packages/next/miktex-hunspell-bin-x64-2.9.tar.lzma
2021/09/18 | 15839 | macros/luatex/generic/luatexja/src/addons/luatexja-fontspec-27c.sty
2021/09/18 | 154928 | macros/luatex/generic/luatexja/src/ltj-ivd_aj1.lua
-2021/09/18 | 154805 | systems/win32/miktex/tm/packages/next/miktex-tex-bin-2.9.tar.lzma
-2021/09/18 | 154425 | systems/win32/miktex/tm/packages/next/miktex-fontconfig-bin-x64-2.9.tar.lzma
2021/09/18 | 154051 | systems/win32/miktex/tm/packages/luamplib__doc.tar.lzma
-2021/09/18 | 153 | systems/texlive/tlnet/install-tl-windows.exe.sha512
2021/09/18 | 1536 | macros/luatex/generic/luatexja/COPYING
-2021/09/18 | 152 | systems/texlive/tlnet/install-tl-unx.tar.gz.sha512
+2021/09/18 | 150094 | macros/latex/contrib/tiscreen/doc/tiscreen-doc.pdf
2021/09/18 | 1492 | systems/texlive/tlnet/archive/luapstricks.doc.r60526.tar.xz
2021/09/18 | 148345 | systems/win32/miktex/tm/packages/pst-tools__doc.tar.lzma
2021/09/18 | 147725 | systems/win32/miktex/tm/packages/latex2nemeth__source.tar.lzma
-2021/09/18 | 147372 | systems/win32/miktex/tm/packages/next/miktex-hunspell-bin-2.9.tar.lzma
2021/09/18 | 14709 | macros/luatex/generic/luatexja/src/patches/lltjp-listings.sty
-2021/09/18 | 1464202 | systems/win32/miktex/tm/packages/next/miktex-runtime-bin-x64-2.9.tar.lzma
-2021/09/18 | 145 | systems/texlive/tlnet/install-tl.zip.sha512
2021/09/18 | 14517 | macros/luatex/generic/luatexja/src/ltj-rmlgbm.lua
-2021/09/18 | 144 | systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512
-2021/09/18 | 14348 | systems/win32/miktex/tm/packages/next/miktex-zzip-bin-x64-2.9.tar.lzma
-2021/09/18 | 1421868 | systems/win32/miktex/tm/packages/next/miktex-dvisvgm-bin-x64-2.9.tar.lzma
2021/09/18 | 1397 | macros/luatex/generic/luatexja/tool/ivslist.tex
2021/09/18 | 13899 | macros/luatex/generic/luatexja/src/jfm-ujis.lua
2021/09/18 | 13726 | macros/luatex/generic/luatexja/doc/lltjp-geometry.tex
-2021/09/18 | 136822 | systems/win32/miktex/tm/packages/next/miktex-mktex-bin-x64-2.9.tar.lzma
-2021/09/18 | 136818 | systems/win32/miktex/tm/packages/next/miktex-fontconfig-bin-2.9.tar.lzma
2021/09/18 | 136384 | systems/texlive/tlnet/archive/tex4ht.doc.r60530.tar.xz
2021/09/18 | 1362 | macros/luatex/generic/luatexja/src/patches/lltjp-footmisc.sty
-2021/09/18 | 1357611 | systems/win32/miktex/tm/packages/miktex-zzdb2-2.9.tar.lzma
-2021/09/18 | 1357420 | systems/win32/miktex/tm/packages/next/miktex-zzdb2-2.9.tar.lzma
2021/09/18 | 135485 | systems/win32/miktex/tm/packages/nicematrix__source.tar.lzma
-2021/09/18 | 13467 | systems/win32/miktex/tm/packages/next/miktex-zzip-bin-2.9.tar.lzma
-2021/09/18 | 1340928 | systems/win32/miktex/tm/packages/next/miktex-runtime-bin-2.9.tar.lzma
-2021/09/18 | 133865 | systems/win32/miktex/tm/packages/next/miktex-dvips-bin-x64-2.9.tar.lzma
2021/09/18 | 13240 | macros/luatex/generic/luatexja/src/ltj-setwidth.lua
-2021/09/18 | 1301194 | systems/win32/miktex/tm/packages/next/miktex-poppler-bin-x64-2.9.tar.lzma
-2021/09/18 | 1291299 | systems/win32/miktex/tm/packages/next/miktex-dvisvgm-bin-2.9.tar.lzma
2021/09/18 | 1278 | macros/luatex/generic/luatexja/src/ltjclasses.ins
2021/09/18 | 126 | macros/luatex/generic/luatexja/src/ltjltxdoc.ins
2021/09/18 | 1269 | macros/generic/expkv-def/README.md
-2021/09/18 | 1261208 | systems/win32/miktex/tm/packages/miktex-zzdb3-2.9.tar.lzma
-2021/09/18 | 1260183 | systems/win32/miktex/tm/packages/next/miktex-zzdb3-2.9.tar.lzma
2021/09/18 | 12439 | systems/win32/miktex/tm/packages/pst-mirror.tar.lzma
-2021/09/18 | 124282 | systems/win32/miktex/tm/packages/next/miktex-mktex-bin-2.9.tar.lzma
2021/09/18 | 12389 | macros/luatex/generic/luatexja/src/patches/lltjcore.sty
2021/09/18 | 1224 | macros/luatex/generic/luatexja/tool/chars2defcharrange.rb
2021/09/18 | 12244 | systems/win32/miktex/tm/packages/mlacls__source.tar.lzma
2021/09/18 | 12177 | systems/win32/miktex/tm/packages/luamplib.tar.lzma
2021/09/18 | 12151 | macros/luatex/generic/luatexja/src/ltj-base.lua
2021/09/18 | 1206112 | systems/texlive/tlnet/archive/tex4ht.source.r60530.tar.xz
-2021/09/18 | 119821 | systems/win32/miktex/tm/packages/next/miktex-dvips-bin-2.9.tar.lzma
2021/09/18 | 1181 | macros/latex/contrib/clicks/README.md
-2021/09/18 | 1169717 | systems/win32/miktex/tm/packages/next/miktex-poppler-bin-2.9.tar.lzma
2021/09/18 | 116875 | systems/win32/miktex/tm/packages/latexindent__source.tar.lzma
-2021/09/18 | 1153 | systems/win32/miktex/tm/packages/next/pr.ini
-2021/09/18 | 115351 | systems/win32/miktex/tm/packages/next/miktex-gregorio-bin-x64-2.9.tar.lzma
2021/09/18 | 11506 | macros/luatex/generic/luatexja/src/jfm-ujisv.lua
2021/09/18 | 114768 | systems/win32/miktex/tm/packages/luaxml__doc.tar.lzma
2021/09/18 | 11335 | macros/luatex/generic/luatexja/tool/unicodeBlocks.tex
2021/09/18 | 1130 | macros/luatex/generic/luatexja/tool/function_uax50.lua
-2021/09/18 | 112983 | systems/win32/miktex/tm/packages/next/miktex-epstopdf-bin-x64-2.9.tar.lzma
2021/09/18 | 1126393 | macros/luatex/generic/luatexja/doc/luatexja-ja.pdf
-2021/09/18 | 112087 | systems/win32/miktex/tm/packages/next/miktex-gd-bin-x64-2.9.tar.lzma
-2021/09/18 | 109031 | systems/win32/miktex/tm/packages/next/miktex-jpeg-bin-x64-2.9.tar.lzma
-2021/09/18 | 108296 | systems/texlive/tlnet/archive/texlive-scripts.r60525.tar.xz
+2021/09/18 | 1114 | macros/luatex/generic/penlight/pl/LICENSE-pl.md
2021/09/18 | 1082356 | systems/win32/miktex/tm/packages/kpfonts-otf.tar.lzma
-2021/09/18 | 1077991 | systems/win32/miktex/tm/packages/next/miktex-asymptote-bin-x64-2.9.tar.lzma
2021/09/18 | 107238 | systems/win32/miktex/tm/packages/siunitx__source.tar.lzma
2021/09/18 | 1055686 | systems/win32/miktex/tm/packages/zref-check__doc.tar.lzma
2021/09/18 | 1054416 | systems/texlive/tlnet/archive/zref-check.doc.r60527.tar.xz
-2021/09/18 | 105222 | systems/win32/miktex/tm/packages/next/miktex-texware-bin-x64-2.9.tar.lzma
-2021/09/18 | 103400 | systems/win32/miktex/tm/packages/next/miktex-lua53-bin-x64-2.9.tar.lzma
-2021/09/18 | 103204 | systems/win32/miktex/tm/packages/next/miktex-mfware-bin-x64-2.9.tar.lzma
-2021/09/18 | 100624 | systems/win32/miktex/tm/packages/next/miktex-epstopdf-bin-2.9.tar.lzma
2021/09/17 | 9600 | macros/luatex/optex/pdfextra/examples/pdfextra-example.tex
2021/09/17 | 9358 | macros/luatex/optex/pdfextra/pdfextra.tex
2021/09/17 | 84739 | macros/unicodetex/latex/njuthesis/figure/njuname-large.pdf
@@ -414,7 +482,6 @@
2021/09/17 | 72250 | macros/unicodetex/latex/njuthesis/njuthesis.dtx
2021/09/17 | 69 | macros/luatex/optex/pdfextra/pdfextra.sty
2021/09/17 | 68819 | macros/luatex/optex/pdfextra/pdfextra.opm
-2021/09/17 | 6834 | systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl
2021/09/17 | 67528 | systems/texlive/tlnet/archive/latexindent.r60522.tar.xz
2021/09/17 | 662 | macros/luatex/optex/pdfextra/LICENSE
2021/09/17 | 6416 | systems/texlive/tlnet/archive/mindflow.source.r60520.tar.xz
@@ -687,7 +754,6 @@
2021/09/14 | 2697255 | macros/latex/contrib/mlacls/mla-example.pdf
2021/09/14 | 259780 | systems/texlive/tlnet/archive/huawei.r60501.tar.xz
2021/09/14 | 25939 | macros/latex/contrib/siunitx/siunitx-complex.dtx
-2021/09/14 | 258104 | systems/texlive/tlnet/archive/clicks.doc.r60498.tar.xz
2021/09/14 | 2426 | macros/latex/contrib/siunitx/siunitx-code.tex
2021/09/14 | 2289437 | install/macros/latex/contrib/siunitx.tds.zip
2021/09/14 | 2265 | macros/luatex/generic/luaxml/luaxml-pretty.lua
@@ -710,7 +776,6 @@
2021/09/14 | 16299 | macros/luatex/generic/luaxml/luaxml-domobject.lua
2021/09/14 | 1628 | macros/latex/contrib/mlacls/mla.ins
2021/09/14 | 161793 | macros/luatex/generic/luaxml.zip
-2021/09/14 | 1552 | systems/texlive/tlnet/archive/clicks.r60498.tar.xz
2021/09/14 | 15317 | macros/latex/contrib/siunitx/siunitx-quantity.dtx
2021/09/14 | 1513922 | macros/latex/contrib/siunitx/siunitx-code.pdf
2021/09/14 | 14954 | macros/luatex/generic/luaxml/luaxml-cssquery.lua
@@ -835,39 +900,3 @@
2021/09/13 | 11396 | systems/texlive/tlnet/archive/mcexam.r60481.tar.xz
2021/09/13 | 112776 | systems/texlive/tlcontrib/tlpkg/texlive.tlpdb.xz
2021/09/13 | 1020392 | systems/win32/miktex/tm/packages/libertinust1math.tar.lzma
-2021/09/12 | 97930 | macros/latex/contrib/media9/pdfbase.sty
-2021/09/12 | 80805 | fonts/newtx/doc/newtxdoc.tex
-2021/09/12 | 7977 | macros/latex/contrib/mcexam/mcexam_example.tex
-2021/09/12 | 79213 | macros/latex/contrib/easybook/easybook.dtx
-2021/09/12 | 78193 | macros/latex/contrib/easybook/easybook.tex
-2021/09/12 | 780619 | fonts/newpx/doc/newpxdoc.pdf
-2021/09/12 | 7472934 | install/macros/latex/contrib/media9.tds.zip
-2021/09/12 | 7467644 | macros/latex/contrib/media9.zip
-2021/09/12 | 72782 | macros/latex/contrib/mcexam/mcexam.sty
-2021/09/12 | 726 | macros/latex/contrib/mcexam/README.md
-2021/09/12 | 617981 | macros/latex/contrib/easybook.zip
-2021/09/12 | 5983168 | fonts/libertinust1math.zip
-2021/09/12 | 577947 | macros/latex/contrib/easybook/easybook.pdf
-2021/09/12 | 4526305 | fonts/xcharter.zip
-2021/09/12 | 4496 | fonts/libertinust1math/README
-2021/09/12 | 44181656 | fonts/noto.zip
-2021/09/12 | 42536 | systems/texlive/tlnet/archive/biblatex-gb7714-2015.r60477.tar.xz
-2021/09/12 | 42123 | fonts/newpx/doc/newpxdoc.tex
-2021/09/12 | 4096884 | fonts/newpx.zip
-2021/09/12 | 39686 | fonts/newtx/README
-2021/09/12 | 3748216 | macros/latex/contrib/media9/doc/media9.pdf
-2021/09/12 | 34339 | macros/latex/contrib/mcexam/mcexam.tex
-2021/09/12 | 340900 | systems/texlive/tlnet/archive/pst-dart.doc.r60476.tar.xz
-2021/09/12 | 245405 | macros/latex/contrib/mcexam.zip
-2021/09/12 | 24315 | fonts/libertinust1math/doc/LibertinusT1Math-doc.tex
-2021/09/12 | 2228 | systems/texlive/tlnet/archive/pst-dart.r60476.tar.xz
-2021/09/12 | 222389 | macros/latex/contrib/mcexam/mcexam.pdf
-2021/09/12 | 2136901 | fonts/libertinust1math/doc/LibertinusT1Math-doc.pdf
-2021/09/12 | 18913 | fonts/newpx/README
-2021/09/12 | 159762 | macros/latex/contrib/media9/media9.sty
-2021/09/12 | 15403 | macros/latex/contrib/media9/ChangeLog
-2021/09/12 | 1467168 | systems/texlive/tlnet/archive/biblatex-gb7714-2015.doc.r60477.tar.xz
-2021/09/12 | 136106091 | install/fonts/notocondensed.tds.zip
-2021/09/12 | 134811965 | fonts/notocondensed.zip
-2021/09/12 | 13199 | fonts/xcharter/README
-2021/09/12 | 1194179 | fonts/newtx/doc/newtxdoc.pdf
diff --git a/indexing/makeindexk/ChangeLog b/indexing/makeindexk/ChangeLog
index f722fb766a..05ce7ab80b 100644
--- a/indexing/makeindexk/ChangeLog
+++ b/indexing/makeindexk/ChangeLog
@@ -1,3 +1,14 @@
+2021-09-19 TANAKA Takuji <ttk@t-lab.opal.ne.jp>
+
+ * mkind.h, scanid.[ch], scanst.c:
+ Fix a bug about detection of page number types.
+ Default string "rnaRA" for page_precedence was not initialized.
+ Improve heuristic detection of page number types.
+ * tests/makeindex.test, tests/range.idx, tests/range[123].{ind,ist},
+ tests/pprec[AB].idx, tests/pprec[01234].ist, tests/pprec[AB]-[01234].ind:
+ Add new tests. These are compatible with (up)mendex.
+ * configure.ac, mkind.h: Bump version to 2.16
+
2019-11-01 Karl Berry <karl@tug.org>
* mkindex: "quote" arguments, doc tweaks.
diff --git a/indexing/makeindexk/configure.ac b/indexing/makeindexk/configure.ac
index eb90d364f9..4ae1fc21fe 100644
--- a/indexing/makeindexk/configure.ac
+++ b/indexing/makeindexk/configure.ac
@@ -6,7 +6,7 @@ dnl This file is free software; the copyright holder
dnl gives unlimited permission to copy and/or distribute it,
dnl with or without modifications, as long as this notice is preserved.
dnl
-AC_INIT([makeindex (TeX Live)], [2.15], [tex-k@tug.org])
+AC_INIT([makeindex (TeX Live)], [2.16], [tex-k@tug.org])
AC_PREREQ([2.65])
AC_CONFIG_SRCDIR([mkind.c])
AC_CONFIG_AUX_DIR([../../build-aux])
diff --git a/indexing/makeindexk/configure.texk b/indexing/makeindexk/configure.texk
index 81c2ce909e..5fd1591673 100755
--- a/indexing/makeindexk/configure.texk
+++ b/indexing/makeindexk/configure.texk
@@ -1,6 +1,6 @@
#! /bin/sh
# Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.71 for makeindex (TeX Live) 2.15.
+# Generated by GNU Autoconf 2.71 for makeindex (TeX Live) 2.16.
#
# Report bugs to <tex-k@tug.org>.
#
@@ -629,8 +629,8 @@ MAKEFLAGS=
# Identity of this package.
PACKAGE_NAME='makeindex (TeX Live)'
PACKAGE_TARNAME='makeindex--tex-live-'
-PACKAGE_VERSION='2.15'
-PACKAGE_STRING='makeindex (TeX Live) 2.15'
+PACKAGE_VERSION='2.16'
+PACKAGE_STRING='makeindex (TeX Live) 2.16'
PACKAGE_BUGREPORT='tex-k@tug.org'
PACKAGE_URL=''
@@ -1374,7 +1374,7 @@ if test "$ac_init_help" = "long"; then
# Omit some internal or obsolete options to make the list less imposing.
# This message is too long to be a string in the A/UX 3.1 sh.
cat <<_ACEOF
-\`configure' configures makeindex (TeX Live) 2.15 to adapt to many kinds of systems.
+\`configure' configures makeindex (TeX Live) 2.16 to adapt to many kinds of systems.
Usage: $0 [OPTION]... [VAR=VALUE]...
@@ -1446,7 +1446,7 @@ fi
if test -n "$ac_init_help"; then
case $ac_init_help in
- short | recursive ) echo "Configuration of makeindex (TeX Live) 2.15:";;
+ short | recursive ) echo "Configuration of makeindex (TeX Live) 2.16:";;
esac
cat <<\_ACEOF
@@ -1565,7 +1565,7 @@ fi
test -n "$ac_init_help" && exit $ac_status
if $ac_init_version; then
cat <<\_ACEOF
-makeindex (TeX Live) configure 2.15
+makeindex (TeX Live) configure 2.16
generated by GNU Autoconf 2.71
Copyright (C) 2021 Free Software Foundation, Inc.
@@ -2089,7 +2089,7 @@ cat >config.log <<_ACEOF
This file contains any messages produced by compilers while
running configure, to aid debugging if configure makes a mistake.
-It was created by makeindex (TeX Live) $as_me 2.15, which was
+It was created by makeindex (TeX Live) $as_me 2.16, which was
generated by GNU Autoconf 2.71. Invocation command line was
$ $0$ac_configure_args_raw
@@ -8411,7 +8411,7 @@ fi
# Define the identity of the package.
PACKAGE='makeindex--tex-live-'
- VERSION='2.15'
+ VERSION='2.16'
# Some tools Automake needs.
@@ -14563,7 +14563,7 @@ Usage: $0 [OPTIONS]
Report bugs to <bug-libtool@gnu.org>."
lt_cl_version="\
-makeindex (TeX Live) config.lt 2.15
+makeindex (TeX Live) config.lt 2.16
configured by $0, generated by GNU Autoconf 2.71.
Copyright (C) 2011 Free Software Foundation, Inc.
@@ -16219,7 +16219,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
# report actual input values of CONFIG_FILES etc. instead of their
# values after options handling.
ac_log="
-This file was extended by makeindex (TeX Live) $as_me 2.15, which was
+This file was extended by makeindex (TeX Live) $as_me 2.16, which was
generated by GNU Autoconf 2.71. Invocation command line was
CONFIG_FILES = $CONFIG_FILES
@@ -16287,7 +16287,7 @@ ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
ac_cs_config='$ac_cs_config_escaped'
ac_cs_version="\\
-makeindex (TeX Live) config.status 2.15
+makeindex (TeX Live) config.status 2.16
configured by $0, generated by GNU Autoconf 2.71,
with options \\"\$ac_cs_config\\"
diff --git a/indexing/makeindexk/mkind.h b/indexing/makeindexk/mkind.h
index 596637ecfa..2860bbc03c 100644
--- a/indexing/makeindexk/mkind.h
+++ b/indexing/makeindexk/mkind.h
@@ -343,9 +343,9 @@ If any array overflows, please report to tex-k@tug.org.
/*====================================================================*/
#if USE_KPATHSEA
-#define VERSION "version 2.15 [TeX Live " TEX_LIVE_VERSION "] (kpathsea + Thai support)"
+#define VERSION "version 2.16 [TeX Live " TEX_LIVE_VERSION "] (kpathsea + Thai support)"
#else
-#define VERSION "version 2.15 [20-Nov-2007] (with Thai support)"
+#define VERSION "version 2.16 [19-Sep-2021] (with Thai support)"
#endif
#define PUT_VERSION { \
@@ -428,6 +428,7 @@ extern char idx_escape;
extern char page_comp[ARRAY_MAX];
extern int page_offset[PAGETYPE_MAX];
+extern char page_prec[ARRAY_MAX];
extern char preamble[ARRAY_MAX];
extern char postamble[ARRAY_MAX];
diff --git a/indexing/makeindexk/scanid.c b/indexing/makeindexk/scanid.c
index 8076a6be47..aea5272d47 100644
--- a/indexing/makeindexk/scanid.c
+++ b/indexing/makeindexk/scanid.c
@@ -424,26 +424,28 @@ scan_no(char no[], int npg[], short *count, short *type)
if (!scan_arabic(no, npg, count))
return (FALSE);
/* simple heuristic to determine if a letter is Roman or Alpha */
- } else if (IS_ROMAN_LOWER(no[0]) && (!IS_COMPOSITOR)) {
+ } else if (IS_ROMAN_LOWER(no[0]) && strchr(page_prec,ROMAN_LOWER) &&
+ (!strchr(page_prec,ALPHA_LOWER) || (!IS_COMPOSITOR))) {
*type = ROML;
if (!scan_roman_lower(no, npg, count))
return (FALSE);
/* simple heuristic to determine if a letter is Roman or Alpha */
- } else if (IS_ROMAN_UPPER(no[0]) &&
- ((no[0] == ROMAN_I) || (!IS_COMPOSITOR))) {
+ } else if (IS_ROMAN_UPPER(no[0]) && strchr(page_prec,ROMAN_UPPER) &&
+ (!strchr(page_prec,ALPHA_UPPER) || (!IS_COMPOSITOR))) {
*type = ROMU;
if (!scan_roman_upper(no, npg, count))
return (FALSE);
- } else if (IS_ALPHA_LOWER(no[0])) {
+ } else if (IS_ALPHA_LOWER(no[0]) && strchr(page_prec,ALPHA_LOWER)) {
*type = ALPL;
if (!scan_alpha_lower(no, npg, count))
return (FALSE);
- } else if (IS_ALPHA_UPPER(no[0])) {
+ } else if (IS_ALPHA_UPPER(no[0]) && strchr(page_prec,ALPHA_UPPER)) {
*type = ALPU;
if (!scan_alpha_upper(no, npg, count))
return (FALSE);
} else {
- IDX_ERROR1("Illegal page number %s.\n", no);
+ IDX_ERROR2("Illegal page number %s or page_precedence %s.\n",
+ no, page_prec);
return (FALSE);
}
return (TRUE);
diff --git a/indexing/makeindexk/scanid.h b/indexing/makeindexk/scanid.h
index 781ae3f709..874bda61c9 100644
--- a/indexing/makeindexk/scanid.h
+++ b/indexing/makeindexk/scanid.h
@@ -77,6 +77,12 @@
((C == ROMAN_I) || (C == ROMAN_V) || (C == ROMAN_X) || \
(C == ROMAN_L) || (C == ROMAN_C) || (C == ROMAN_D) || (C == ROMAN_M))
+#define ROMAN_LOWER 'r'
+#define ROMAN_UPPER 'R'
+#define ARABIC 'n'
+#define ALPHA_LOWER 'a'
+#define ALPHA_UPPER 'A'
+
#define ALPHA_VAL(C) \
((('A' <= C) && (C <= 'Z')) ? C - 'A' : \
(('a' <= C) && (C <= 'z')) ? C - 'a' : 0)
diff --git a/indexing/makeindexk/scanst.c b/indexing/makeindexk/scanst.c
index a91fbce042..410bec5b84 100644
--- a/indexing/makeindexk/scanst.c
+++ b/indexing/makeindexk/scanst.c
@@ -101,7 +101,7 @@ ROMAN_LOWER_OFFSET + ARABIC_OFFSET,
ROMAN_LOWER_OFFSET + ARABIC_OFFSET + ALPHA_LOWER_OFFSET,
ROMAN_LOWER_OFFSET + ARABIC_OFFSET + ALPHA_LOWER_OFFSET + ROMAN_UPPER_OFFSET
};
-static char page_prec[ARRAY_MAX] = PRECEDENCE_DEF;
+char page_prec[ARRAY_MAX] = PRECEDENCE_DEF;
static int put_dot;
@@ -228,7 +228,6 @@ scan_sty(void)
/* page precedence */
} else if (STREQ(spec, PRECEDENCE)) {
(void) scan_string(page_prec);
- (void) process_precedence();
/* index input format */
} else if (STREQ(spec, KEYWORD))
(void) scan_string(idx_keyword);
@@ -260,6 +259,8 @@ scan_sty(void)
STY_DOT;
}
}
+ /* page precedence */
+ (void) process_precedence();
/* check if quote and escape are distinct */
if (idx_quote == idx_escape) {
diff --git a/indexing/makeindexk/tests/makeindex.test b/indexing/makeindexk/tests/makeindex.test
index c631ea6cc3..4727a96970 100755
--- a/indexing/makeindexk/tests/makeindex.test
+++ b/indexing/makeindexk/tests/makeindex.test
@@ -1,5 +1,5 @@
#! /bin/sh -vx
-# $Id: makeindex.test 45809 2017-11-15 00:36:56Z karl $
+# $Id: makeindex.test 60547 2021-09-19 08:43:57Z takuji $
# Copyright 2017 Karl Berry <tex-live@tug.org>
# Copyright 2012 Peter Breitenlohner <tex-live@tug.org>
# You may freely use, modify and/or distribute this file.
@@ -7,7 +7,7 @@
TEXMFCNF=$srcdir/../kpathsea
export TEXMFCNF
-rm -f sample.*
+rm -f sample.* range*.* pprec*.*
./makeindex $srcdir/tests/sample.idx -o sample.ind1 -t sample.ilg1 \
&& diff $srcdir/tests/sample.ind sample.ind1 || exit 1
@@ -15,3 +15,36 @@ rm -f sample.*
cat $srcdir/tests/sample.idx | ./makeindex >sample.ind2 2>sample.ilg2 \
&& diff $srcdir/tests/sample.ind sample.ind2 || exit 1
+# test for range suffix_2p, suffix_3p, suffix_mp
+./makeindex -s $srcdir/tests/range1.ist $srcdir/tests/range.idx \
+ -o range1.ind1 -t range1.ilg \
+ && diff $srcdir/tests/range1.ind range1.ind1 || exit 1
+
+./makeindex -s $srcdir/tests/range2.ist $srcdir/tests/range.idx \
+ -o range2.ind1 -t range2.ilg \
+ && diff $srcdir/tests/range2.ind range2.ind1 || exit 1
+
+./makeindex -s $srcdir/tests/range3.ist $srcdir/tests/range.idx \
+ -o range3.ind1 -t range3.ilg \
+ && diff $srcdir/tests/range3.ind range3.ind1 || exit 1
+
+# test for page_precedence and suffix_3p
+./makeindex -s $srcdir/tests/pprec0.ist $srcdir/tests/pprecA.idx \
+ -o pprecA-0.ind1 -t pprecA-0.ilg \
+ && diff $srcdir/tests/pprecA-0.ind pprecA-0.ind1 || exit 1
+
+./makeindex -s $srcdir/tests/pprec1.ist $srcdir/tests/pprecA.idx \
+ -o pprecA-1.ind1 -t pprecA-1.ilg \
+ && diff $srcdir/tests/pprecA-1.ind pprecA-1.ind1 || exit 1
+
+./makeindex -s $srcdir/tests/pprec2.ist $srcdir/tests/pprecA.idx \
+ -o pprecA-2.ind1 -t pprecA-2.ilg \
+ && diff $srcdir/tests/pprecA-2.ind pprecA-2.ind1 || exit 1
+
+./makeindex -s $srcdir/tests/pprec3.ist $srcdir/tests/pprecB.idx \
+ -o pprecB-3.ind1 -t pprecB-3.ilg \
+ && diff $srcdir/tests/pprecB-3.ind pprecB-3.ind1 || exit 1
+
+./makeindex -s $srcdir/tests/pprec4.ist $srcdir/tests/pprecB.idx \
+ -o pprecB-4.ind1 -t pprecB-4.ilg \
+ && diff $srcdir/tests/pprecB-4.ind pprecB-4.ind1 || exit 1
diff --git a/indexing/makeindexk/tests/pprec0.ist b/indexing/makeindexk/tests/pprec0.ist
new file mode 100644
index 0000000000..ec1242bbba
--- /dev/null
+++ b/indexing/makeindexk/tests/pprec0.ist
@@ -0,0 +1,11 @@
+% -*- coding: utf-8 -*-
+
+%page_precedence "rnaRA"
+
+delim_0 "[[delim0]]"
+delim_1 "[[delim1]]"
+delim_2 "[[delim2]]"
+delim_r "[[delimr]]"
+suffix_2p "[[sfx 2p]]"
+suffix_3p "[[sfx 3p]]"
+
diff --git a/indexing/makeindexk/tests/pprec1.ist b/indexing/makeindexk/tests/pprec1.ist
new file mode 100644
index 0000000000..8f9221ebce
--- /dev/null
+++ b/indexing/makeindexk/tests/pprec1.ist
@@ -0,0 +1,11 @@
+% -*- coding: utf-8 -*-
+
+page_precedence "nrR"
+
+delim_0 "[[delim0]]"
+delim_1 "[[delim1]]"
+delim_2 "[[delim2]]"
+delim_r "[[delimr]]"
+suffix_2p "[[sfx 2p]]"
+suffix_3p "[[sfx 3p]]"
+
diff --git a/indexing/makeindexk/tests/pprec2.ist b/indexing/makeindexk/tests/pprec2.ist
new file mode 100644
index 0000000000..eeec1e95cf
--- /dev/null
+++ b/indexing/makeindexk/tests/pprec2.ist
@@ -0,0 +1,11 @@
+% -*- coding: utf-8 -*-
+
+page_precedence "Rnr"
+
+delim_0 "[[delim0]]"
+delim_1 "[[delim1]]"
+delim_2 "[[delim2]]"
+delim_r "[[delimr]]"
+suffix_2p "[[sfx 2p]]"
+suffix_3p "[[sfx 3p]]"
+
diff --git a/indexing/makeindexk/tests/pprec3.ist b/indexing/makeindexk/tests/pprec3.ist
new file mode 100644
index 0000000000..b8f33af1dd
--- /dev/null
+++ b/indexing/makeindexk/tests/pprec3.ist
@@ -0,0 +1,11 @@
+% -*- coding: utf-8 -*-
+
+page_precedence "naA"
+
+delim_0 "[[delim0]]"
+delim_1 "[[delim1]]"
+delim_2 "[[delim2]]"
+delim_r "[[delimr]]"
+suffix_2p "[[sfx 2p]]"
+suffix_3p "[[sfx 3p]]"
+
diff --git a/indexing/makeindexk/tests/pprec4.ist b/indexing/makeindexk/tests/pprec4.ist
new file mode 100644
index 0000000000..768fec7642
--- /dev/null
+++ b/indexing/makeindexk/tests/pprec4.ist
@@ -0,0 +1,11 @@
+% -*- coding: utf-8 -*-
+
+page_precedence "Ana"
+
+delim_0 "[[delim0]]"
+delim_1 "[[delim1]]"
+delim_2 "[[delim2]]"
+delim_r "[[delimr]]"
+suffix_2p "[[sfx 2p]]"
+suffix_3p "[[sfx 3p]]"
+
diff --git a/indexing/makeindexk/tests/pprecA-0.ind b/indexing/makeindexk/tests/pprecA-0.ind
new file mode 100644
index 0000000000..245eacacdd
--- /dev/null
+++ b/indexing/makeindexk/tests/pprecA-0.ind
@@ -0,0 +1,5 @@
+\begin{theindex}
+
+ \item entryA[[delim0]]i[[sfx 3p]], 1[[sfx 3p]], I[[sfx 3p]]
+
+\end{theindex}
diff --git a/indexing/makeindexk/tests/pprecA-1.ind b/indexing/makeindexk/tests/pprecA-1.ind
new file mode 100644
index 0000000000..772e302d7c
--- /dev/null
+++ b/indexing/makeindexk/tests/pprecA-1.ind
@@ -0,0 +1,5 @@
+\begin{theindex}
+
+ \item entryA[[delim0]]1[[sfx 3p]], i[[sfx 3p]], I[[sfx 3p]]
+
+\end{theindex}
diff --git a/indexing/makeindexk/tests/pprecA-2.ind b/indexing/makeindexk/tests/pprecA-2.ind
new file mode 100644
index 0000000000..52d35cb3d7
--- /dev/null
+++ b/indexing/makeindexk/tests/pprecA-2.ind
@@ -0,0 +1,5 @@
+\begin{theindex}
+
+ \item entryA[[delim0]]I[[sfx 3p]], 1[[sfx 3p]], i[[sfx 3p]]
+
+\end{theindex}
diff --git a/indexing/makeindexk/tests/pprecA.idx b/indexing/makeindexk/tests/pprecA.idx
new file mode 100644
index 0000000000..8e770b9d0d
--- /dev/null
+++ b/indexing/makeindexk/tests/pprecA.idx
@@ -0,0 +1,9 @@
+\indexentry{entryA}{i}
+\indexentry{entryA}{ii}
+\indexentry{entryA}{iii}
+\indexentry{entryA}{1}
+\indexentry{entryA}{2}
+\indexentry{entryA}{3}
+\indexentry{entryA}{I}
+\indexentry{entryA}{II}
+\indexentry{entryA}{III}
diff --git a/indexing/makeindexk/tests/pprecB-3.ind b/indexing/makeindexk/tests/pprecB-3.ind
new file mode 100644
index 0000000000..1e23d9fb4a
--- /dev/null
+++ b/indexing/makeindexk/tests/pprecB-3.ind
@@ -0,0 +1,5 @@
+\begin{theindex}
+
+ \item entryA[[delim0]]1[[sfx 3p]], a[[sfx 3p]], A[[sfx 3p]]
+
+\end{theindex}
diff --git a/indexing/makeindexk/tests/pprecB-4.ind b/indexing/makeindexk/tests/pprecB-4.ind
new file mode 100644
index 0000000000..3943ad4854
--- /dev/null
+++ b/indexing/makeindexk/tests/pprecB-4.ind
@@ -0,0 +1,5 @@
+\begin{theindex}
+
+ \item entryA[[delim0]]A[[sfx 3p]], 1[[sfx 3p]], a[[sfx 3p]]
+
+\end{theindex}
diff --git a/indexing/makeindexk/tests/pprecB.idx b/indexing/makeindexk/tests/pprecB.idx
new file mode 100644
index 0000000000..a1d8fc31ae
--- /dev/null
+++ b/indexing/makeindexk/tests/pprecB.idx
@@ -0,0 +1,9 @@
+\indexentry{entryA}{a}
+\indexentry{entryA}{b}
+\indexentry{entryA}{c}
+\indexentry{entryA}{1}
+\indexentry{entryA}{2}
+\indexentry{entryA}{3}
+\indexentry{entryA}{A}
+\indexentry{entryA}{B}
+\indexentry{entryA}{C}
diff --git a/indexing/makeindexk/tests/range.idx b/indexing/makeindexk/tests/range.idx
new file mode 100644
index 0000000000..3eccb113aa
--- /dev/null
+++ b/indexing/makeindexk/tests/range.idx
@@ -0,0 +1,126 @@
+\indexentry{entryA}{1}
+\indexentry{entryA}{2}
+\indexentry{entryA}{iii}
+\indexentry{entryA}{iv}
+\indexentry{entryA}{IX}
+\indexentry{entryA}{X}
+
+\indexentry{entryB}{1}
+\indexentry{entryB}{2}
+\indexentry{entryB}{3}
+\indexentry{entryB}{iii}
+\indexentry{entryB}{iv}
+\indexentry{entryB}{v}
+\indexentry{entryB}{IX}
+\indexentry{entryB}{X}
+\indexentry{entryB}{XI}
+
+\indexentry{entryC}{1}
+\indexentry{entryC}{2}
+\indexentry{entryC}{3}
+\indexentry{entryC}{4}
+\indexentry{entryC|bold}{iii}
+\indexentry{entryC|bold}{iv}
+\indexentry{entryC|bold}{v}
+\indexentry{entryC|bold}{vi}
+\indexentry{entryC}{IX}
+\indexentry{entryC|bold}{X}
+\indexentry{entryC}{XI}
+\indexentry{entryC}{XII}
+
+\indexentry{entryD}{c-1}
+\indexentry{entryD}{c-2}
+\indexentry{entryD}{i-iii}
+\indexentry{entryD}{i-iv}
+\indexentry{entryD}{d-IX}
+\indexentry{entryD}{d-X}
+
+\indexentry{entryE}{c-1}
+\indexentry{entryE}{c-2}
+\indexentry{entryE}{c-3}
+\indexentry{entryE}{i-iii}
+\indexentry{entryE}{i-iv}
+\indexentry{entryE}{i-v}
+\indexentry{entryE}{d-IX}
+\indexentry{entryE}{d-X}
+\indexentry{entryE}{d-XI}
+
+\indexentry{entryF}{C-I}
+\indexentry{entryF}{C-II}
+\indexentry{entryF}{C-III}
+\indexentry{entryF}{C-IV}
+\indexentry{entryF}{V-LIII}
+\indexentry{entryF}{V-LIV}
+\indexentry{entryF}{V-LV}
+\indexentry{entryF}{V-LVI}
+\indexentry{entryF}{D-DI}
+\indexentry{entryF}{D-DII}
+\indexentry{entryF}{D-DIII}
+\indexentry{entryF}{D-DIV}
+
+\indexentry{entryP}{A-a-1-VII-iii}
+\indexentry{entryP}{A-a-2-VII-iii}
+\indexentry{entryP}{A-a-3-VII-iii}
+\indexentry{entryP}{A-a-1-VIII-iii}
+\indexentry{entryP}{A-a-1-IX-iii}
+\indexentry{entryP}{A-a-1-VII-iv}
+\indexentry{entryP}{A-a-1-VII-v}
+
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-10}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-11}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-14}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-15}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-16}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-22}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-23}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-24}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-25}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-i}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-ii}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-iii}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-iv}
+\indexentry{entryQ}{1-2-3-4-5-6-7-8-9-v}
+
+\indexentry{entryA1|(}{1}
+\indexentry{entryA1|)}{2}
+\indexentry{entryA1|(}{iii}
+\indexentry{entryA1|)}{iv}
+\indexentry{entryA1|(}{IX}
+\indexentry{entryA1|)}{X}
+
+\indexentry{entryB1|(}{1}
+\indexentry{entryB1|)}{3}
+\indexentry{entryB1|(}{iii}
+\indexentry{entryB1|)}{v}
+\indexentry{entryB1|(}{IX}
+\indexentry{entryB1|)}{XI}
+
+\indexentry{entryC1|(}{1}
+\indexentry{entryC1|)}{4}
+\indexentry{entryC1|(bold}{iii}
+\indexentry{entryC1|)}{vi}
+\indexentry{entryC1|(}{IX}
+\indexentry{entryC1|bold}{X}
+\indexentry{entryC1|)}{XII}
+
+\indexentry{entryD1|(}{c-1}
+\indexentry{entryD1|)}{c-2}
+\indexentry{entryD1|(}{i-iii}
+\indexentry{entryD1|)}{i-iv}
+\indexentry{entryD1|(}{d-IX}
+\indexentry{entryD1|)}{d-X}
+
+\indexentry{entryE1|(}{c-1}
+\indexentry{entryE1|)}{c-3}
+\indexentry{entryE1|(}{i-iii}
+\indexentry{entryE1|)}{i-v}
+\indexentry{entryE1|(}{d-IX}
+\indexentry{entryE1|)}{d-XI}
+
+\indexentry{entryF1|(}{C-I}
+\indexentry{entryF1|)}{C-IV}
+\indexentry{entryF1|(}{V-LIII}
+\indexentry{entryF1|)}{V-LVI}
+\indexentry{entryF1|(}{D-DI}
+\indexentry{entryF1|bold}{D-DII}
+\indexentry{entryF1|)}{D-DIV}
diff --git a/indexing/makeindexk/tests/range1.ind b/indexing/makeindexk/tests/range1.ind
new file mode 100644
index 0000000000..c5f047a55e
--- /dev/null
+++ b/indexing/makeindexk/tests/range1.ind
@@ -0,0 +1,25 @@
+\begin{theindex}
+
+ \item entryA, 1[[sfx 2p]], iii[[sfx 2p]], IX[[sfx 2p]]
+ \item entryA1, 1[[sfx 2p]], iii[[sfx 2p]], IX[[sfx 2p]]
+ \item entryB, 1[[sfx mp]], iii[[sfx mp]], IX[[sfx mp]]
+ \item entryB1, 1[[sfx mp]], iii[[sfx mp]], IX[[sfx mp]]
+ \item entryC, 1[[sfx mp]], \bold{iii[[sfx mp]]}, IX, \bold{X},
+ XI[[sfx 2p]]
+ \item entryC1, 1[[sfx mp]], \bold{iii[[sfx mp]]}, \bold{X},
+ IX[[sfx mp]]
+ \item entryD, c-1[[sfx 2p]], d-IX[[sfx 2p]], i-iii[[sfx 2p]]
+ \item entryD1, c-1[[sfx 2p]], d-IX[[sfx 2p]], i-iii[[sfx 2p]]
+ \item entryE, c-1[[sfx mp]], d-IX[[sfx mp]], i-iii[[sfx mp]]
+ \item entryE1, c-1[[sfx mp]], d-IX[[sfx mp]], i-iii[[sfx mp]]
+ \item entryF, C-I[[sfx mp]], D-DI[[sfx mp]], V-LIII[[sfx mp]]
+ \item entryF1, C-I[[sfx mp]], \bold{D-DII}, D-DI[[sfx mp]],
+ V-LIII[[sfx mp]]
+ \item entryP, A-a-1-VII-iii[[sfx mp]], A-a-1-VIII-iii, A-a-1-IX-iii,
+ A-a-2-VII-iii, A-a-3-VII-iii
+ \item entryQ, 1-2-3-4-5-6-7-8-9-10[[sfx 2p]],
+ 1-2-3-4-5-6-7-8-9-14[[sfx mp]],
+ 1-2-3-4-5-6-7-8-9-22[[sfx mp]],
+ 1-2-3-4-5-6-7-8-9-i[[sfx mp]]
+
+\end{theindex}
diff --git a/indexing/makeindexk/tests/range1.ist b/indexing/makeindexk/tests/range1.ist
new file mode 100644
index 0000000000..116c2d8713
--- /dev/null
+++ b/indexing/makeindexk/tests/range1.ist
@@ -0,0 +1,3 @@
+page_precedence "nrRaA"
+suffix_2p "[[sfx 2p]]"
+suffix_mp "[[sfx mp]]"
diff --git a/indexing/makeindexk/tests/range2.ind b/indexing/makeindexk/tests/range2.ind
new file mode 100644
index 0000000000..789eb3f75c
--- /dev/null
+++ b/indexing/makeindexk/tests/range2.ind
@@ -0,0 +1,26 @@
+\begin{theindex}
+
+ \item entryA, 1[[sfx 2p]], iii[[sfx 2p]], IX[[sfx 2p]]
+ \item entryA1, 1[[sfx 2p]], iii[[sfx 2p]], IX[[sfx 2p]]
+ \item entryB, 1[[sfx 3p]], iii[[sfx 3p]], IX[[sfx 3p]]
+ \item entryB1, 1[[sfx 3p]], iii[[sfx 3p]], IX[[sfx 3p]]
+ \item entryC, 1[[delim r]]4, \bold{iii[[delim r]]vi}, IX, \bold{X},
+ XI[[sfx 2p]]
+ \item entryC1, 1[[delim r]]4, \bold{iii[[delim r]]vi}, \bold{X},
+ IX[[delim r]]XII
+ \item entryD, c-1[[sfx 2p]], d-IX[[sfx 2p]], i-iii[[sfx 2p]]
+ \item entryD1, c-1[[sfx 2p]], d-IX[[sfx 2p]], i-iii[[sfx 2p]]
+ \item entryE, c-1[[sfx 3p]], d-IX[[sfx 3p]], i-iii[[sfx 3p]]
+ \item entryE1, c-1[[sfx 3p]], d-IX[[sfx 3p]], i-iii[[sfx 3p]]
+ \item entryF, C-I[[delim r]]C-IV, D-DI[[delim r]]D-DIV,
+ V-LIII[[delim r]]V-LVI
+ \item entryF1, C-I[[delim r]]C-IV, \bold{D-DII}, D-DI[[delim r]]D-DIV,
+ V-LIII[[delim r]]V-LVI
+ \item entryP, A-a-1-VII-iii[[sfx 3p]], A-a-1-VIII-iii, A-a-1-IX-iii,
+ A-a-2-VII-iii, A-a-3-VII-iii
+ \item entryQ, 1-2-3-4-5-6-7-8-9-10[[sfx 2p]],
+ 1-2-3-4-5-6-7-8-9-14[[sfx 3p]],
+ 1-2-3-4-5-6-7-8-9-22[[delim r]]1-2-3-4-5-6-7-8-9-25,
+ 1-2-3-4-5-6-7-8-9-i[[delim r]]1-2-3-4-5-6-7-8-9-v
+
+\end{theindex}
diff --git a/indexing/makeindexk/tests/range2.ist b/indexing/makeindexk/tests/range2.ist
new file mode 100644
index 0000000000..552e5fb674
--- /dev/null
+++ b/indexing/makeindexk/tests/range2.ist
@@ -0,0 +1,4 @@
+page_precedence "nrRaA"
+delim_r "[[delim r]]"
+suffix_2p "[[sfx 2p]]"
+suffix_3p "[[sfx 3p]]"
diff --git a/indexing/makeindexk/tests/range3.ind b/indexing/makeindexk/tests/range3.ind
new file mode 100644
index 0000000000..519730a138
--- /dev/null
+++ b/indexing/makeindexk/tests/range3.ind
@@ -0,0 +1,25 @@
+\begin{theindex}
+
+ \item entryA, 1[[sfx 2p]], iii[[sfx 2p]], IX[[sfx 2p]]
+ \item entryA1, 1[[sfx 2p]], iii[[sfx 2p]], IX[[sfx 2p]]
+ \item entryB, 1[[sfx 3p]], iii[[sfx 3p]], IX[[sfx 3p]]
+ \item entryB1, 1[[sfx 3p]], iii[[sfx 3p]], IX[[sfx 3p]]
+ \item entryC, 1[[sfx mp]], \bold{iii[[sfx mp]]}, IX, \bold{X},
+ XI[[sfx 2p]]
+ \item entryC1, 1[[sfx mp]], \bold{iii[[sfx mp]]}, \bold{X},
+ IX[[sfx mp]]
+ \item entryD, c-1[[sfx 2p]], d-IX[[sfx 2p]], i-iii[[sfx 2p]]
+ \item entryD1, c-1[[sfx 2p]], d-IX[[sfx 2p]], i-iii[[sfx 2p]]
+ \item entryE, c-1[[sfx 3p]], d-IX[[sfx 3p]], i-iii[[sfx 3p]]
+ \item entryE1, c-1[[sfx 3p]], d-IX[[sfx 3p]], i-iii[[sfx 3p]]
+ \item entryF, C-I[[sfx mp]], D-DI[[sfx mp]], V-LIII[[sfx mp]]
+ \item entryF1, C-I[[sfx mp]], \bold{D-DII}, D-DI[[sfx mp]],
+ V-LIII[[sfx mp]]
+ \item entryP, A-a-1-VII-iii[[sfx 3p]], A-a-1-VIII-iii, A-a-1-IX-iii,
+ A-a-2-VII-iii, A-a-3-VII-iii
+ \item entryQ, 1-2-3-4-5-6-7-8-9-10[[sfx 2p]],
+ 1-2-3-4-5-6-7-8-9-14[[sfx 3p]],
+ 1-2-3-4-5-6-7-8-9-22[[sfx mp]],
+ 1-2-3-4-5-6-7-8-9-i[[sfx mp]]
+
+\end{theindex}
diff --git a/indexing/makeindexk/tests/range3.ist b/indexing/makeindexk/tests/range3.ist
new file mode 100644
index 0000000000..b751471b43
--- /dev/null
+++ b/indexing/makeindexk/tests/range3.ist
@@ -0,0 +1,4 @@
+page_precedence "nrRaA"
+suffix_2p "[[sfx 2p]]"
+suffix_3p "[[sfx 3p]]"
+suffix_mp "[[sfx mp]]"
diff --git a/info/dtk-bibliography/README.md b/info/dtk-bibliography/README.md
index 7d385436ad..df7b5b8ed2 100644
--- a/info/dtk-bibliography/README.md
+++ b/info/dtk-bibliography/README.md
@@ -3,7 +3,7 @@
This package contains the bibliography for "Die TeXnische Komödie",
the journal of the German-speaking user group.
-It is updated on a quarterly basis, last update 2021-06-03 for DTK 2021-02.
+It is updated on a quarterly basis, last update $2021-09-19 for issue 2021/03$
# How to produce dtk-bibliography.pdf
diff --git a/info/dtk-bibliography/dtk-bibliography.bib b/info/dtk-bibliography/dtk-bibliography.bib
index 9972606a4a..f80e038bc7 100644
--- a/info/dtk-bibliography/dtk-bibliography.bib
+++ b/info/dtk-bibliography/dtk-bibliography.bib
@@ -11214,7 +11214,7 @@ ich mir so zu {\TeX} gedacht habe, bisher aber niemand hören wollte},
keywords = {bretter}
}
@article {dtk09.1:thanh:truetypefonts,
- author = {Han The Thanh},
+ author = {{Han The Thanh}},
title = {TrueType-Fonts für {\pdfTeX}},
journal = dtk,
volume = {21},
@@ -18327,4 +18327,118 @@ to \LaTeX{} quickly and convincingly},
}
+@article {dtk21.3:Dietsche:editorial,
+ author = {Dietsche, Luzia},
+ title = {Editorial},
+ journal = dtk,
+ date = {2021},
+ volume = {37},
+ number = {3},
+ month = {9},
+ pages = {2},
+ keywords = {editorial}
+}
+
+@article {dtk21.3:sievers:grusswort,
+ author = {Sievers, Martin},
+ title = {Grußwort},
+ journal = dtk,
+ volume = {37},
+ number = {3},
+ date = {2021},
+ month = {9},
+ pages = {4--5},
+ keywords = {grusswort}
+}
+
+
+@article {dtk21.3:sievers:meyer:einladung,
+ author = {Sievers, Martin and Meyer, Thomas Hilarius},
+ title = {Einladung zur Herbsttagung und 63. Mitgliederversammlung von DANTE e.V.},
+ journal = dtk,
+ volume = {37},
+ number = {3},
+ date = {2021},
+ month = {9},
+ pages = {5--7},
+ keywords = {einladung}
+}
+
+
+@article {dtk21.3:Dietsche:vorstand,
+ author = {Dietsche, Luzia},
+ title = {Vorstellung des Vorstands},
+ journal = dtk,
+ date = {2021},
+ volume = {37},
+ number = {3},
+ month = {9},
+ pages = {7--10},
+ keywords = {vorstellung, vorstand}
+}
+
+@article {dtk21.3:bonnetsmueller:boxen,
+ author = {Bonnetsmüller, Adelheid},
+ title = {Having Fun with LaTeX: Jetzt werden Boxen bunt},
+ journal = dtk,
+ volume = {37},
+ number = {3},
+ date = {2021},
+ month = {9},
+ pages = {11--18},
+ keywords = {bretter}
+}
+
+
+@article {dtk21.3:fritsch:arara,
+ author = {Fritsch, Rainer-Maria},
+ title = {Mit arara externe LaTeX-Dokumente erzeugen und einfügen},
+ journal = dtk,
+ volume = {37},
+ number = {3},
+ date = {2021},
+ month = {9},
+ pages = {19--29},
+ keywords = {bretter}
+}
+
+@article {dtk21.3:mittelbach:rowley:tagged,
+ author = {Frank Mittelbach and Chris Rowley},
+ title = {LaTeX Tagged PDF - Ein Entwurf für ein großes Projekt},
+ journal = dtk,
+ volume = {37},
+ number = {3},
+ date = {2021},
+ month = {9},
+ pages = {30--44},
+ keywords = {bretter}
+}
+
+
+@article {dtk21.3:mittelbach:rowley:news,
+ author = {Frank Mittelbach and Chris Rowley},
+ title = {LaTeX News - Issue 32, Oktober 2021},
+ journal = dtk,
+ volume = {37},
+ number = {3},
+ date = {2021},
+ month = {9},
+ pages = {44--58},
+ keywords = {bretter}
+}
+
+
+@article {dtk21.3:fenn:ctan,
+ author = {Fenn, Jürgen},
+ title = {Neue Pakete auf CTAN},
+ journal = dtk,
+ date = {2021},
+ volume = {37},
+ number = {3},
+ month = {9},
+ pages = {59--62},
+ keywords = {fremdebuehne}
+}
+
+
@Comment{jabref-meta: databaseType:biblatex;}
diff --git a/info/dtk-bibliography/dtk-bibliography.pdf b/info/dtk-bibliography/dtk-bibliography.pdf
index 3cdb607e18..2b0380468e 100644
--- a/info/dtk-bibliography/dtk-bibliography.pdf
+++ b/info/dtk-bibliography/dtk-bibliography.pdf
Binary files differ
diff --git a/info/dtk-bibliography/dtk-bibliography.tex b/info/dtk-bibliography/dtk-bibliography.tex
index 9efe00f508..8567d414dc 100644
--- a/info/dtk-bibliography/dtk-bibliography.tex
+++ b/info/dtk-bibliography/dtk-bibliography.tex
@@ -12,7 +12,7 @@
\usepackage[bibstyle=dtk-authoryear, backend=biber, dashed=false]{biblatex}
\usepackage[babel, german=quotes]{csquotes}
-\usepackage{dtk-logos}
+\usepackage{dtk-logos}
\addbibresource{dtk-bibliography.bib}
@@ -22,7 +22,7 @@
\begin{document}
-\noindent Last Update: \today~ for DTK 2020-04
+\noindent Last Update: \today~ for DTK 2021-03
\nocite{*}
diff --git a/info/dtk-bibliography/dtk-logos.sty b/info/dtk-bibliography/dtk-logos.sty
new file mode 100644
index 0000000000..30a1a97be8
--- /dev/null
+++ b/info/dtk-bibliography/dtk-logos.sty
@@ -0,0 +1,169 @@
+%% Paket fuer Logos
+%%
+%% This is file `dtk-logos.sty',
+%%
+%% LaTeX Class for ``Die TeXnische Komoedie''.
+%%
+%% Copyright (C) 2019
+%% Herbert Voss
+%%
+%% It may be distributed and/or modified under the
+%% conditions of the LaTeX Project Public License, either version 1.3c
+%% of this license or (at your option) any later version.
+%% The latest version of this license is in
+%% http://www.latex-project.org/lppl.txt
+%% and version 1.3c or later is part of all distributions of LaTeX
+%% version 2005/12/01 or later.
+%
+%
+% -------------------------------------
+\ProvidesPackage{dtk-logos}[2020/02/02 v. 1.10 Dante Edition: Logos (hv)]
+%
+\newif\if@dtk@newengine \@dtk@newenginetrue
+\RequirePackage{iftex}
+%
+\ifluatex\else\ifxetex\else\@dtk@newenginefalse\fi\fi
+%
+\RequirePackage{fetamont,hologo,dantelogo,xspace}
+\if@dtk@newengine
+ \RequirePackage{unicode-math}
+ \setmathfont{LatinModernMath-Regular}
+ \setmathfont[version=XITS]{XITSMath-Regular.otf}
+\fi
+%
+\iffalse
+\DeclareRobustCommand\ffmfamily{\not@math@alphabet\ffmfamily\relax
+ \fontencoding{T1}\fontfamily{ffm}\selectfont}
+\DeclareTextFontCommand{\textffm}{\ffmfamily}
+\def\MF{\textffm{META}\@dischyph\textffm{FONT}\@}
+\def\MP{\textffm{META}\@dischyph\textffm{POST}\@}
+\def\MT{\textffm{META}\@dischyph\textffm{TYPE1}\@}
+\fi
+\def\mpShort{\textffm{MP}\@}
+\def\mfShort{\textffm{MF}\@}
+
+\def\HoLogo@La#1{%
+ L%
+ \ifx\f@family\sfdefault\kern-.15em\else\kern-.23em\fi
+ \begingroup
+ \setbox\ltx@zero\hbox{T}%
+ \vbox to\ht\ltx@zero{%
+ \hbox{%
+ \ltx@ifundefined{check@mathfonts}{%
+ \csname sevenrm\endcsname
+ }{%
+ \check@mathfonts
+ \fontsize\sf@size{0pt}%
+ \math@fontsfalse\selectfont
+ }%
+ A%
+ }%
+ \vss
+ }%
+ \endgroup
+}
+
+
+\def\LaTeXTeX{\hologo{(La)TeX}}
+
+\def\AmS{\textsf{AMS}}
+\let\AMS\AmS
+%{\ifluatex\mathversion{XITS}\fi%
+% $\mathcal{A}$\kern-.1667em\lower.5ex\hbox{$\mathcal{M}$}\kern-.125em$\mathcal{S}$}}% $
+\def\amsmath{\AMS\textsf{math}}
+\def\AmSLaTeX{\mbox{\textsf{AMS}-\hologo{LaTeX}}}
+\def\AmSTeX{\mbox{\textsf{AMS}-\hologo{TeX}}}
+\def\biber{\hologo{biber}}
+\def\Biber{\Hologo{biber}}
+\def\BibTeX{\@ifnextchar8\@@BibTeX\@BibTeX}
+\def\@BibTeX{{\hologoLogoSetup{BibTeX}{variant=sc}\hologo{BibTeX}}}
+\def\BibTeXacht{{\hologoLogoSetup{BibTeX8}{variant=sc}\hologo{BibTeX8}}}
+\def\@@BibTeX8{{\hologoLogoSetup{BibTeX8}{variant=sc}\hologo{BibTeX8}}}
+\def\ConTeXt{\hologo{ConTeXt}}
+\let\context\ConTeXt
+\def\emTeX{\hologo{emTeX}}
+\def\eTeX{\hologo{eTeX}}
+\def\ExTeX{\hologo{ExTeX}}
+\def\HanTheThanh{\hologo{HanTheThanh}}
+\def\iniTeX{\hologo{iniTeX}}
+\def\KOMAScript{\hologo{KOMAScript}}
+\def\LaTeX{\hologo{LaTeX}}
+\def\LaTeXe{\hologo{LaTeX2e}}
+\def\LaTeXIII{\hologo{LaTeX3}}
+\def\LaTeXML{\hologo{LaTeXML}}
+\def\LuaLaTeX{\hologo{LuaLaTeX}}
+\let\lualatex\LuaLaTeX
+\def\LuaTeX{\hologo{LuaTeX}}
+\let\luatex\LuaTeX
+\def\LyX{\hologo{LyX}}
+\def\METAFONT{\hologo{METAFONT}}
+\let\MF\METAFONT
+\def\MetaFun{\textffm{METAFUN}}
+\let\MFun\MetaFun
+\def\METAPOST{\hologo{METAPOST}}
+\def\MetaPost{\hologo{MetaPost}}
+\let\MP\METAPOST
+\def\MiKTeX{\hologo{MiKTeX}}
+\def\NTS{\hologo{NTS}}
+\def\OzMF{\hologo{OzMF}}
+\def\OzMP{\hologo{OzMP}}
+\def\OzTeX{\hologo{OzTeX}}
+\def\OzTtH{\hologo{OzTtH}}
+\def\PCTeX{\hologo{PCTeX}}
+\def\pdfTeX{\hologo{pdfTeX}}
+\let\pdftex\pdfTeX
+\def\pdfLaTeX{\hologo{pdfLaTeX}}
+\let\pdflatex\pdfLaTeX
+\def\PiC{\hologo{PiC}}
+\def\PiCTeX{\hologo{PiCTeX}}
+\def\plainTeX{\hologo{plainTeX}}
+\def\PostScript{PostScript}
+\let\PS\PostScript
+\def\SageTeX{\hologo{SageTeX}}
+\def\SLiTeX{\hologo{SLiTeX}}
+\def\teTeX{\hologo{teTeX}}
+\def\TeXivht{\hologo{TeX4ht}}
+\def\TTH{\hologo{TTH}}
+\def\virTeX{\hologo{virTeX}}
+\def\VTeX{\hologo{VTeX}}
+\def\XeLaTeX{\hologo{XeLaTeX}}
+\def\XeTeX{\hologo{XeTeX}}
+%%
+%
+\newcommand\BibTool{\textsc{Bib\hskip-.1em
+ T\hskip-.15emo\hskip-.05emo\hskip-.05eml}\xspace}
+\def\tikz{\textsf{Ti\textit{k}Z}}
+\def\ALEPH{\ensuremath{\aleph}}
+\let\TikZ\tikz
+
+\providecommand\eV{e.V\kern-0.18em\@ifnextchar.{}{.}\kern0.18em}
+\providecommand\dante{\mbox{DANTE~\eV}}
+\providecommand\Dante{DANTE,
+ Deutschsprachige Anwendervereinigung \TeX~\eV}
+\providecommand\DTK{Die \TeX\-ni\-sche Ko\-m{\"o}\-die}
+\providecommand\PS{Post\-Script}
+\providecommand\TUG{\TeX{} Users Group}
+\providecommand\TUGboat{\textsl{TUGboat}}
+\let\DANTE\dantelogo
+\def\pgf/tikz{\textsf{pgf/Ti\textit{k}Z}}
+\def\pgftikz{\textsf{pgf/Ti\textit{k}Z}}
+\def\TeXLive{\TeX{}Live\xspace}
+
+\def\BibLaTeX{Bib\hologo{LaTeX}}
+\let\biblatex\BibLaTeX
+\def\CTAN{\texttt{CTAN}\xspace}
+
+\def\PSTricks{\textsf{PSTricks}\xspace}
+\let\pstricks\PSTricks
+
+\if@dtk@newengine
+ \newfontfamily\dtk@LinWikipedia{Linux Libertine O}%
+ \def\WikipediA{{\dtk@LinWikipedia\char"E02F\textsc{ikipediA}}} % hexadezimal
+\else
+ \def\WikipediA{WikipediA}%
+\fi
+\let\wikipedia\WikipediA
+%
+\def\macOS{\textsf{macOS}\xspace}
+
+\endinput
diff --git a/language/korean/pmhanguljamo/README.md b/language/korean/pmhanguljamo/README.md
index 4864d6a5d8..274dcb4939 100644
--- a/language/korean/pmhanguljamo/README.md
+++ b/language/korean/pmhanguljamo/README.md
@@ -4,15 +4,15 @@
This LaTeX package provides Hangul *transliteration* input method, which allows to typeset Korean Letters (Hangul) with the help of proper fonts. The name is from “Poor man’s Hangul Jamo Input Method.”
-It is mainly for the people who have a system without Korean IM, but want to typeset Hangul in their document. Not only modern Hangul, but so-colled “Old Hangul” characters that uses the lost letters can also be typeset.
+It is mainly for the people who have a system without Korean keyboard IM, but want to typeset Hangul in their document. Not only modern Hangul, but so-colled “Old Hangul” characters that uses the lost letters can also be typeset.
-In version 0.3, it provides two kinds of transliteration rules. The default one is this package's unique method. The other is a more familiar *Korean Romanization Rule*. With the latter only Modern Hangul can be typeset.
+In version 0.3, it provides two kinds of transliteration rules. The default way is this package's unique method. The alternative one is a more familiar *Korean Romanization Rule*. With the latter only Modern Hangul can be typeset.
-XeLaTeX recommended. And the legacy pdfTeX is not supported.
+XeLaTeX or LuaLaTeX is recommended. And the legacy pdfTeX is not supported.
## Copyright and License
-(C) 2020 Kangsoo Kim <modviv2k15 at gmail.com>
+(C) 2020-2021 Kangsoo Kim <modviv2k15 at gmail.com>
This work may be distributed and/or modified under the conditions of the LaTeX Project Public License, either version 1.3 of this license or (at your option) any later version.
The latest version of this license is in
diff --git a/language/korean/pmhanguljamo/doc/pmhanguljamo-doc.pdf b/language/korean/pmhanguljamo/doc/pmhanguljamo-doc.pdf
index 300318d574..1f3c519d89 100644
--- a/language/korean/pmhanguljamo/doc/pmhanguljamo-doc.pdf
+++ b/language/korean/pmhanguljamo/doc/pmhanguljamo-doc.pdf
Binary files differ
diff --git a/language/korean/pmhanguljamo/doc/pmhanguljamo-doc.tex b/language/korean/pmhanguljamo/doc/pmhanguljamo-doc.tex
index b83d5f688f..1bf5a46ffa 100644
--- a/language/korean/pmhanguljamo/doc/pmhanguljamo-doc.tex
+++ b/language/korean/pmhanguljamo/doc/pmhanguljamo-doc.tex
@@ -1,5 +1,6 @@
\documentclass[a4paper]{article}
-\usepackage{fontspec}
+\usepackage{kotex}
+%\usepackage{fontspec}
%\setmainfont{Noto Serif}
%\setsansfont{Noto Sans}
%\setmonofont{Noto Sans Mono}
@@ -14,13 +15,16 @@
\usepackage{pmhanguljamo}
\usepackage[colorlinks]{hyperref}
-\usepackage{polyglossia}
-\setotherlanguage{korean}
-\newfontfamily\hangulfont{UnBatang.ttf}[Script=Hangul]
-%\newcommand*\jamo[1]{\begingroup\hangulfont\jamoword{#1}}
-\newcommand*\jamo[1]{\textkorean{\jamoword{#1}}}
+%\usepackage{polyglossia}
+%\setotherlanguage{korean}
+%\newfontfamily\hangulfont{UnBatang.ttf}[Script=Hangul,Renderer=OpenType]
+%%\newcommand*\jamo[1]{\begingroup\hangulfont\jamoword{#1}}
+\setmainhangulfont{Noto Serif CJK KR}[Script=Hangul,Renderer=OpenType]
+\providecommand*\textkorean[1]{#1}
+\newenvironment{korean}{}{}
+\newcommand*\jamo[1]{\jamoword{#1}}
\newcommand*\pkg[1]{\textsf{#1}\index{#1}\index{Package!#1}}
\newcommand*\thispkg{\pkg{pmhanguljamo}}
@@ -30,7 +34,8 @@
\title{Poorman's Hangul Jamo Input Method \\ \Large \texttt{pmhanguljamo.sty} }
\author{Kangsoo Kim}
-\date{15 Mar 2020\quad version 0.3.4}
+%\date{10 Mar 2021\quad version 0.3.5}
+\date{20 Sep 2021\quad version 0.3.6}
\maketitle
@@ -50,8 +55,8 @@ Not only modern Hangul, but so-colled ``Old Hangul'' characters
that uses the lost letters such as `Arae-A'(\textkorean{\jmcc{@}}), `Yet Ieung'(\textkorean{\jmcc{Q}}) or `Pan-Sios'(\textkorean{\jmcc{Z}}) etc.\ can also be typeset.
\XeLaTeX\ or \LuaLaTeX\ is required. The legacy pdf\TeX\ is not supported.
-The Korean Language supporting packages such as \pkg{xetexko} or \pkg{luatexko} (in the ko.\TeX\ bundle)
-or \pkg{polyglossia} under \XeLaTeX\
+The Korean Language supporting packages such as \pkg{xetexko} and \pkg{luatexko} (in the ko.\TeX\ bundle)
+or \pkg{polyglossia} package with Korean support %under \XeLaTeX\
are recommended, but without them typesetting Hangul is of no problem
with this package \thispkg.
@@ -65,12 +70,13 @@ Put the following line in your preamble:
\end{verbatim}
The option \texttt{[RRK]} or \texttt{[rrk]}
-can be fed, in which case the \emph{Standard Korean Romanization Transliteration}
+can be given, in which case the \emph{Standard Korean Romanization Transliteration}
input method is activated. In section~\ref{sec:rrk},
we will explain about it.
Without option, the default input method of this package is used, that
is explained in sections~\ref{sec:rule}--\ref{sec:exam}.
-They are incompatible with each other.
+You are to specify just one of them. Using both methods in one document is
+not allowed.
\subsection{Commands and Environment Provided}
@@ -98,7 +104,7 @@ ip/sai/ei i/nvn ba/ram/ei/do \\
na/nvn goi/ro/ue/haiss/da/.
\end{jamotext}
\end{verbatim}
-The result will be like:
+The result:
\begin{quote}
\begin{korean}
\begin{jamotext}
@@ -112,7 +118,7 @@ na/nvn goi/ro/ue/haiss/da/.
\end{quote}
-\subsection{Preamble of Document}
+\subsection{Setting up in your Preamble}
Without any other packages, you can type Hangul. Be sure that
proper fonts should be declared for Hangul, utilizing \pkg{fontspec}.
@@ -131,7 +137,7 @@ proper fonts should be declared for Hangul, utilizing \pkg{fontspec}.
\texttt{UnBatang.ttf} is a font shipped in \pkg{unfonts-core} package of \TeX\,Live.
Not all Korean fonts can be available to typeset so-called `Old Hangul',
because they happen to lack
-the feature of composing Korean Syllables from conjoining Jamo codes. Fortunately,
+the feature of composing Korean syllables from conjoining Jamo codes. Fortunately,
we have a few more fonts besides \texttt{UnBatang} that are \emph{proper}
and freely available, which are listed in the section~\ref{sec:font}.
In \TeX\,Live distribution, \texttt{UnBatang} is the only \emph{proper} one.
@@ -178,11 +184,11 @@ However when just single or a few Hangul characters are needed, for example in t
the author's name in Hangul, this package provides a casual way to print them out.
-\section{Transliteration Rule}\label{sec:rule}
+\section{Transliteration Rule of This Package}\label{sec:rule}
\subsection{Tone Marks and Syllable Serapator}
-This package assumes that every Hangul syllable has Tone Mark which
+This package assumes that every Hangul syllable has \emph{Tone Mark} which
must be explicitly put at the end of it. Table~\ref{tab:tonemark}
shows the input scheme of them.
@@ -231,8 +237,9 @@ the same as Korean Romanization Method (RRK).\footnote{The \emph{Revised Romaniz
\item The letter for `\textkorean{\jmcc{R}}' is allocated to \texttt{r} not \texttt{l}. The latter is reserved for another purpose, cf. item \ref{itm:llr}.
\item The letter `c' is for `\textkorean{\jmcc{C}}'. In the RRK, it is `\texttt{ch}' there.
\item The letter for `\textkorean{\jmcc{X}}' is \texttt{x}, which is a bit strange, but commonly accepted `\texttt{ng}' was not to be chosen. It is reserved for another consonant `\textkorean{\jamoword{@ng/}}'
-\item And the letters `q, z, f' are more freely selected. These letters are the lost ones in Modern Korean.
-\item The letter `w' is chosen to present \emph{Choseong Filler}.
+\item The letters `q, z, f' are more freely selected. These letters are the lost ones in Modern Korean.
+\item The letter `w' is chosen to present \emph{Choseong Filler}.
+%\footnote{\emph{Jungseong Filler} is not provided.}
\item\label{itm:llr} The letters `l' and `lr' are
prepared for very rare case. In the 15th century Korean, `\textkorean{\jamoword{sla}}' and
`\textkorean{\jamoword{slra}}' were distinguishable. `sl' is chosen for the SIOS with left branch being longer, and `slr' means right branch longer. The same is the case `ssl', `sslr', `jl', `jlr' etc.
@@ -258,18 +265,20 @@ Table~\ref{tab:vow} shows the transliteration rule for vowels.
ㅑ & ya & ㅕ & ye & ㅛ & yo & ㅠ & yu & \kern-.3em\jmcc{@}\kern-.6em\jmcc{@} & @@ \\ \hline
ㅐ & ay, ai & ㅔ & ey, ei & ㅚ & oy, oi & ㅟ & uy, ui & ㅢ & vy, vi \\
ㅒ & yay, yai & ㅖ & yey, yei & \jmcc{YOI} & yoi & \jmcc{YUI} & yui & \jmcc{@I} & @i \\ \hline
-ㅘ & oa & ㅙ & oay, oai & ㅝ & ue & ㅞ & uey, uei & & \\ \hline
+ㅘ & oa & ㅙ & oay, oai & ㅝ & ue & ㅞ & uey, uei & & * \\ \hline
\end{tabular}
\end{table}
\begin{enumerate}
-\item The rule for vowels may look unfamiliar. But it is simple and easily rememberable.
+\item The rule for vowels may look a bit unfamiliar. But it is simple and easily rememberable.
\item The letter `v' is never considered to be a vowel. But this package
determined it to stand for the vowel `\textkorean{\jmcc{V}}'. In fact,
the roman alphabets fall short of the vowels. This allocation is arbitrary.
\item The lost `Arae-A' is allocated to `@', and the rare `Double Arae-A' to `@@', which are forgotten vowels in Modern Korean.
+
+\item The \emph{Jungseong Filler} ([U+1160]) can be input with \verb|*|. For example, \verb|\jamoword{h*n}| renders \jamoword{h*n}.
\end{enumerate}
\subsection{Compatibility Jamos}
@@ -286,7 +295,8 @@ Because the \verb|@| character has no upper case, write \verb|W| instead of \ver
Here is an example:
\begin{quote}
\begin{verbatim}
-\jamoword{W/nvn a/rai/a/ra/go bu/rv/go A/oa bi/svs/ha/gei irg/nvn/da}
+\jamoword{W/nvn a/rai/a/ra/go bu/rv/go A/oa
+ bi/svs/ha/gei irg/nvn/da}
\end{verbatim}
\textkorean{\jamoword{W/nvn a/rai/a/ra/go bu/rv/go A/oa bi/svs/ha/gei irg/nvn/da}}
\end{quote}
@@ -311,16 +321,19 @@ They can be downloaded from the url and used freely. And note that
\item[KoPub World] free fonts by Korea Publisher Society. \url{http://www.kopus.org/biz/electronic/font.aspx}
\item[Malgun Gothic] One of Windows 10 system font.
\item[Nanum Yet Hangul] Two font families from Naver. Nanum Myeongjo Yet Hangeul and Nanum Barun Gothic Yet Hangeul. \url{https://hangeul.naver.com/2014/archaicword}
-\item[Noto CJK] Google's Noto CJK fonts. Or Adobe's Source Han fonts. \url{https://www.google.com/get/noto/help/cjk/}
+\item[Noto CJK or Source Han] Google's Noto CJK fonts. Or Adobe's Source Han fonts. \url{https://www.google.com/get/noto/help/cjk/}
\item[Un Batang] \verb|UnBatang.ttf|. on CTAN. \url{https://ctan.org/pkg/unfonts-core}
\end{description}
To use one of these fonts, do not forget to provide
the \texttt{[Script=Hangul]} option.\footnote{%
- If the document is processed with \LuaLaTeX, \texttt{[Script=Hangul,Renderer=Harfbuzz]}
- or \texttt{[Script=Hangul,Renderer=OpenType]} is recommended.
- The \LuaLaTeX\ version should be over 1.11, that is,
- the \LaTeX\ format of LuaHB\TeX\ is needed.}
+ the option should be \\
+ \texttt{[Script=Hangul,Renderer=Harfbuzz]}\\
+ or\\
+ \texttt{[Script=Hangul,Renderer=OpenType]},\\
+ when the document is processed with \LuaLaTeX.}
+% The \LuaLaTeX\ version should be over 1.11, that is,
+% the \LaTeX\ format of LuaHB\TeX\ is needed.}
For example, you can specify
Hamchorom Batang LVT font as:
\begin{verbatim}
@@ -499,14 +512,13 @@ e/nv na/ri i do/ra/gar h@i/o/.
\ExplSyntaxOff
-\section{The RRK input method} \label{sec:rrk}
+\section{The RRK Input Method: An Alternative Way} \label{sec:rrk}
In chapter 3 section 8 of \emph{Revised Romanization of Korean} (2000),
the `transliteration method' of Hangul romanization is declared.
It is exceptionally permitted when it is required to restore
-the original Hangul writings, while the phonetic transcription method
-is standard.
-So we can make use of it as an input method.
+the original Hangul writings, while standard is the phonetic transcription one.
+So we can make use of the former as an input method.
If the package option \texttt{[RRK]} or \texttt{[rrk]} is given,
the RRK method is activated. In this case, only `Modern Hangul' can
@@ -578,11 +590,11 @@ The `Arae-A' is to be input as `\texttt{@}' character, and the `Double Arae-A' `
has only leading `@', i.e., \textkorean{\jamoword{x@}} should be input by `\verb|x@|'.
If the syllable has trailing consonants or is part of a word, just \verb|@| is
good. \\
-\verb|\jamoword{h@ngeul @@nam-eun}| \textkorean{\jamoword{h@ngeul @@d@b}}.
+\verb|\jamoword{h@ngeul @@nam-eun}| \textkorean{\jamoword{h@ngeul @@nam-eun}}.
\end{enumerate}
-\subsection{Examples of RRK method}
+\subsection{Example of RRK method}
The text is one of the most famous poem in Korean, \textkorean{\jamoword{jindallaekkoch}} \emph{Azalea} by Kim So-wol.\footnote{%
An English translation of this poem can be read in Wikipedia,
@@ -649,11 +661,11 @@ please read the documentation
\section{Acknowledgement}
Thanks go to yihoze at the KTUG board, there he commented,
-``The IMs I've tried such as Korean Windows Old Hangul IM or Saenaru IM
-were all somewhat inconvenient. \ldots\
+``The IM apps I've tried such as Korean Windows Old Hangul IM or Saenaru IM
+were all somewhat inconvenient. $\ldots$\
In my opinion, for the foreign scholar who
studies Korean language, especially Middle Age Korean language,
this kind of transliteration method may be much more convenient and
-confident way of typesetting Korean than the IM solutions.''\footnote{\url{http://www.ktug.org/xe/index.php?mid=KTUG_open_board&document_srl=240134}}
+confident way of typesetting Korean than the keyboard IM solutions.''\footnote{\url{http://www.ktug.org/xe/index.php?mid=KTUG_open_board&document_srl=240134}}
\end{document}
diff --git a/language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.pdf b/language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.pdf
index 13d65b069d..7207343b46 100644
--- a/language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.pdf
+++ b/language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.pdf
Binary files differ
diff --git a/language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.tex b/language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.tex
index 4de722b9e2..d941140d19 100644
--- a/language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.tex
+++ b/language/korean/pmhanguljamo/doc/pmhanguljamo-kdoc.tex
@@ -64,7 +64,9 @@
%\date{2020/01/30\quad v0.3.1}
%\date{2020/02/05\quad v0.3.2}
%\date{2020/03/09\quad v0.3.3}
-\date{2020/03/15\quad v0.3.4}
+%\date{2020/03/15\quad v0.3.4}
+%\date{2020/03/10\quad v0.3.5}
+\date{2021/09/20\quad v0.3.6}
\maketitle
@@ -186,7 +188,7 @@ mun/jj@x;oa;ro; se/rv s@/m@s/di; a/ni;h@r/ss@i;
\subsection{성조표지와 음절}
-입력에는 로마자 알파벳과 \verb|@| 부호, 그리고 세 개의 성조표지(방점) 기호 \verb|/|, \verb|;|, \verb|:|를 사용한다.
+입력에는 로마자 알파벳과 \verb|@| 부호, 그리고 세 개의 성조표지(방점) 기호 \verb|/|, \verb|;|, \verb|:|를 사용하며, 중성 필러를 표현하기 위하여 \verb|*| 부호를 쓴다.
\emph{성조표지가 와야 음절이 완성된 것으로 본다}. 모든 한글 음절은 반드시 성조표지로 끝난다.
평성이 아닌 성조표지는 (적절한 폰트에서) 한글 음절의 왼편에 찍힌다.
@@ -249,10 +251,11 @@ mun/jj@x;oa;ro; se/rv s@/m@s/di; a/ni;h@r/ss@i;
\item `\jmcc{Q}'(옛이응)을 \verb|q|로 한 것은 역시 \pkg{frktex}에서 온 것인데 ``꼭지 달린 이응''이라는 글자 모양이 \verb|q|를 연상시킨다''는 이유였다고 한다. 그럴 법도 하다.
\item `\jmcc{Z}'(반시옷)을 \verb|z|로 한 것은 납득할 수 있다고 본다. z가 s의 유성음에 대응하기 때문에.
\item 가장 곤란했던 것은 \jmcc{F}(여린히읗)이었다. 사용되지 않은 자판 가운데서 선택할 수밖에 없는데 \texttt{l}, \texttt{v}, \texttt{w}, \texttt{f} 정도가 선택 가능한 범위였다. 알파벳이 아닌 부호문자 중에서 선택하는 것도 생각해보았으나 역시 자판의 윗줄로 올라가는 것은 꺼려져서 \verb|f|로 선택하였다. 순전히 편의에 의한 대응이다.
-\item `\jamotextcmd{sl@}'나 `\jamotextcmd{slr@}'와 같은 특별한 글자, `\jamotextcmd{ssla/sslra/jla/jlra/jjla/jjlra/cla/clra}'의 초성 글자들, 즉 훈민정음 언해의 ``\jamoword{dyuq/guyg;so/ri;yeys; ni;sso/ri;}'' 표기에서 쓰이는 齒頭\cntrdot 正齒音을 구분하기 위한 글자들은 \texttt{s}, \texttt{j}, \texttt{c}에 \texttt{l}, \texttt{lr}을 붙여서 표기한다. \verb|l|이 왼쪽(치두음), \verb|lr|이 오른쪽(정치음)이 더 길어지는 글자가 된다.
+\item `\jamotextcmd{sl@}'나 `\jamotextcmd{slr@}'와 같은 특별한 글자, `\jamotextcmd{jl*/cl*/jjl*/sl*/ss*}, \jamotextcmd{jlr*/clr*/jjlr*/slr*/sslr*}'의 초성 글자들, 즉 훈민정음 언해의 ``\jamoword{dyuq/guyg;so/ri;yeys; ni;sso/ri;}'' 표기에서 쓰이는 齒頭\cntrdot 正齒音을 구분하기 위한 글자들은 \texttt{s}, \texttt{j}, \texttt{c}에 \texttt{l}, \texttt{lr}을 붙여서 표기한다. \verb|l|이 왼쪽(치두음), \verb|lr|이 오른쪽(정치음)이 더 길어지는 글자가 된다.
\item `\jmcc{BX}', `\jmcc{BBX}', `\jmcc{MX}', `\jmcc{PX}'와 같은 ``\jamoword{ib/si/ur;ga/b@i;ya/bx@n; so/ri;}'' 표기는 \verb|x|를 이어써서 표기한다. 각각, \texttt{bx}, \texttt{bbx}, \texttt{mx}, \texttt{px}.
\item 겹자음\cntrdot 복자음은 이어쓴다. \texttt{gg} \jmcc{GG}, \texttt{bsg} \jmcc{BSG}, \texttt{bsd} \jmcc{BSD}, \texttt{rf} \jmcc{RF}.
-\item `제로자음'인 초성 이응은 생략할 수 있다. 모음으로 시작하면 초성 이응을 식자해준다. \verb|\jamoword{an}| \jamoword{an}. 만약 정말로 초성을 비우기를 원한다면(즉 초성 필러를 쓰려 한다면) \verb|w|를 그 위치에 쓰도록 하라. 이것은 종성에는 적용되지 않는다. \verb|\jamoword{wan}| \jamoword{wan}.
+\item `제로자음'인 초성 이응은 생략할 수 있다. 모음으로 시작하면 초성 이응을 식자해준다. \verb|\jamoword{an}| \jamoword{an}.
+\item 정말로 초성을 비우기를 원한다면(즉 초성 필러를 쓰려 한다면) \verb|w|를 그 위치에 쓸 수 있다. 이것은 종성에는 적용되지 않는다. \verb|\jamoword{wan}| \jamoword{wan}.
\end{enumerate}
\subsubsection{모음}
@@ -292,6 +295,7 @@ mun/jj@x;oa;ro; se/rv s@/m@s/di; a/ni;h@r/ss@i;
\verb|\jamoword{@@/nam/vn}| \jamoword{@@/nam/vn}.
\item 표준 로마자 표기법에서 \texttt{wa}, \texttt{wo}, \texttt{we}, \texttt{wi}로 표기할 때 나타나는 반자음 \verb|w|은 채택하지 않았다. `ㅘ, ㅝ, ㅟ'는 생긴 대로 \texttt{oa}, \texttt{ue}, \texttt{ui}로 적는다.
\item 이 패키지는 Unicode 4.0의 한글 자모 확장 A, B를 지원한다. 그러므로 한글 자모 영역([U+11XX])에는 없는 `\jamoword{wuye}'와 같은 모음을 표기할 수 있다. \verb|\jamoword{sa/guye}| \jamoword{sa/guye/}. 물론 폰트가 이를 지원해야 한다.
+\item 중성 필러([U+1160])는 \verb|*| 부호로 적을 수 있다. \verb|\jamoword{h*n}| \jamoword{h*n}. 초성과 중성을 모두 filler로 채우는 경우에는 종성이 있어야 한다. \verb|\jamoword{w*f}| \jamoword{w*f}.
\end{enumerate}
\subsubsection{호환자모 음절} \label{sec:compjamo}
@@ -799,6 +803,9 @@ Expl3는 내가 원하는 일을 너무나 간단하게 구현할 수 있게 해
\textbullet\ v0.3.4 (2020/03/15): PM방식 xetexko에서 단어 내 행나눔.
+\textbullet\ v0.3.5 (2021/03/10): 유지보수.
+
+\textbullet\ v0.3.6 (2021/09/20): 중성 필러 \verb|*| 추가.
%%% APPENDICES
\ExplSyntaxOn
\int_new:N \l_appsec_int
diff --git a/language/korean/pmhanguljamo/tex/pmhanguljamo.sty b/language/korean/pmhanguljamo/tex/pmhanguljamo.sty
index 0d0d8e01ff..0b9138c057 100644
--- a/language/korean/pmhanguljamo/tex/pmhanguljamo.sty
+++ b/language/korean/pmhanguljamo/tex/pmhanguljamo.sty
@@ -2,7 +2,7 @@
%%
%% Poorman's Hangul Jamo Input Method.
%%
-%% (C) 2020 Kangsoo Kim
+%% (C) 2020-2021 Kangsoo Kim
%%
%% This work may be distributed and/or modified under the
%% conditions of the LaTeX Project Public License, either version 1.3
@@ -22,8 +22,8 @@
\RequirePackage{xparse}
\ProvidesExplPackage
{pmhanguljamo}
- {2020/03/15}
- {v0.3.4}
+ {2021/09/20}
+ {v0.3.6}
{Poorman's Hangul Jamo Input Method}
\RequirePackage{l3keys2e}
@@ -272,7 +272,7 @@
}
}
- \regex_match:nnTF { [a-z;:@|] } { #1 }
+ \regex_match:nnTF { [a-z;:@|*] } { #1 }
{
\str_case:nnTF { #1 }
{
@@ -284,6 +284,7 @@
{ i } { \tl_put_right:Nn \l_jun_tl { #1 } }
{ y } { \tl_put_right:Nn \l_jun_tl { #1 } }
{ @ } { \tl_put_right:Nn \l_jun_tl { #1 } }
+ { * } { \tl_put_right:Nn \l_jun_tl { #1 } } %% jungseong filler
}
{
\int_compare:nT { \tl_count:N \l_cho_tl == 0 }
@@ -545,7 +546,9 @@
ui = \char"1171,
oai = \char"116B,
uei = \char"1170,
- vy = \char"1174
+ vy = \char"1174,
+%% Jungseong filler
+ * = \char"1160
}
\prop_const_from_keyval:Nn \c_jong_prop
diff --git a/macros/latex/contrib/schooldocs/schooldocs.dtx b/macros/latex/contrib/schooldocs/schooldocs.dtx
index 284d239bcd..6d66e09605 100644
--- a/macros/latex/contrib/schooldocs/schooldocs.dtx
+++ b/macros/latex/contrib/schooldocs/schooldocs.dtx
@@ -1,6 +1,6 @@
% \iffalse meta-comment
%
-% Copyright (C) 2020 by Antoine Missier <antoine.missier@ac-toulouse.fr>
+% Copyright (C) 2020-2021 by Antoine Missier <antoine.missier@ac-toulouse.fr>
% ----------------------------------------------------------------------
%
% This file may be distributed and/or modified under the
@@ -22,7 +22,7 @@
%<package>\NeedsTeXFormat{LaTeX2e}[2005/12/01]
%<package>\ProvidesPackage{schooldocs}
%<*package>
- [2020/07/13 v1.0 .dtx schooldocs file]
+ [2021/09/18 v1.1 .dtx schooldocs file]
%</package>
%
%<*driver>
@@ -58,7 +58,7 @@
% corresponds to \textsf{schooldocs}~\fileversion, dated \filedate.}}
\subject{Antoine Missier \\ %
\texttt{antoine.missier@ac-toulouse.fr}}
-\date{July 13, 2020, \fileversion}
+\date{\filedate, \fileversion}
\hypersetup{%
colorlinks,
@@ -76,7 +76,7 @@
%</driver>
% \fi
%
-% \CheckSum{792}
+% \CheckSum{766}
%
% \CharacterTable
% {Upper-case \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z
@@ -96,9 +96,10 @@
%
%
% \changes{v0.0}{2010/09/01}{Initial personal version}
-% \changes{v0.1}{2020/01/05}{Initial documentation in french}
+% \changes{v0.1}{2020/01/05}{Initial documentation in French}
% \changes{v0.2}{2020/05/04}{Examples file}
-% \changes{v1.0}{2020/07/13}{Initial public version with documentation in english}
+% \changes{v1.0}{2020/07/13}{Initial public version with documentation in English}
+% \changes{v1.1}{2021/09/18}{Using the translations package for internationalization}
%
% \GetFileInfo{schooldocs.dtx}
%
@@ -107,7 +108,7 @@
% \section{Introduction}
%
% The purpose of this package is to provide several layout styles for school documents.
-% It can be used for exercices sheets, exams, course materials.
+% It can be used for exercises sheets, exams, course materials.
% The package sets the page geometry (dimensions of text and margins)
% and the title typesetting;
% the various \emph{styles} define the header, footer and title formatting,
@@ -140,7 +141,7 @@
% |\thispagestyle|\marg{style} at the page you want.
% For example, you can place the command |\thispagestyle{identity}|
% in an appendix page to render.
-% \subsection{Informations fields}
+% \subsection{Information fields}
%
% \DescribeMacro{\title}
% Usually a \LaTeX\ document title is made up of information
@@ -179,7 +180,7 @@
% So, if you do not want any date, just write |\date{}|.
% When title elements like subject, date are missing, the title height will be reduced accordingly.
%
-% Except |\date|, all these information macros accept multiline arguments,
+% Except |\date|, all these information macros accept multi-line arguments,
% for example |\school{Royal College of Pataphysics\\London}|.
% To typeset the main title on two (or more) lines, use |\\[1ex]|
% for a good vertical spacing.
@@ -213,7 +214,7 @@
% provided by the macro |\subtitle|\marg{text}. The parameter \meta{text}
% will be placed under the other title elements and before the separation rule,
% if there is one. For instance |\subtitle{Duration of the test: 2\,h}|.
-% For a multi-line argument, the linebreak instruction must be
+% For a multi-line argument, the line break instruction must be
% |\par| and not |\\|.
%
% \subsection{Making the title}
@@ -255,7 +256,7 @@
% which adds \textsf{-- Correct Version} to the title.
% It should be placed in the preamble.
% Combined with packages that deal with multiple versions of a document,
-% this macro avoids to define different titles for a an exercice sheet an its correct version.
+% this macro avoids to define different titles for a an exercise sheet an its correct version.
% However in the \textbf{exam} style, the text \textsf{Correct version}
% will not be added in the title (except in headers), but will be substituted for
% the subtitle.
@@ -419,9 +420,9 @@
% \textsf{geometry} (by Hideo Umeki and David Carlisle).
% According to good typographic practice (see the \LaTeX\ Companion,
% the \textsf{typearea} package or \textsf{KOMA-Script} bundle documentation)
-% the ratio between the text body (the type area as named by typographs)
+% the ratio between the text body (the type area as named by typographers)
% and the page size must be the same horizontally and vertically.
-% This ratio gauges the ``shades of grey'' of a page.
+% This ratio gauges the ``shades of gray'' of a page.
% The default scale in the \textsf{geometry} package is 0.7.
% For school documents, it seems that this setting leaves too much white space in the margins, thus this ratio has been set to 0.75.
% Of course, it can be freely redefined with the |\geometry| command.
@@ -443,7 +444,7 @@
% and lengths with |\setlength|.
% Most of command names are explicit enough to give some information about their use.
%
-% Let us specifiy nevertheless that |\titleflush| manages the alignment:
+% Let us specify nevertheless that |\titleflush| manages the alignment:
% centered by default, it can be left-aligned with
% |\renewcommand{\titleflush}{flushleft}| or right-aligned with
% |\renewcommand{\titleflush}{flushright}|\footnote{This macro has no effect
@@ -481,7 +482,7 @@
% \end{center}
%
% \DescribeMacro{\subtitlestyle}\DescribeMacro{\titlecorrectstyle}
-% The \textbf{exam} style has two additionnal setting macros:
+% The \textbf{exam} style has two additional setting macros:
% |\subtitlestyle|, set by default to |\large\slshape|,
% and |\titlecorrectstyle|, for the composition of \textsf{Correct Version}
% produced by |\correct|, set by default to |\color{red}\large\scshape|.
@@ -497,7 +498,7 @@
%
% \medskip
% \DescribeMacro{\boxedshape}
-% A particular formatting has been predefined to produce a slighlty shaded box around the title: |\boxedshape|\marg{text}
+% A particular formatting has been predefined to produce a slightly shaded box around the title: |\boxedshape|\marg{text}
% (thanks to the \textsf{fancybox} package by Timothy Van Zandt).
% It is not activated by default but it can be used within the
% command |\titlestyle| if we want to get this kind of effect, e.g.
@@ -553,19 +554,20 @@
% |\renewcommand\footrulewidth{0.4pt}|
%
% \subsection{Internationalization}
-% Four keywords are used by the package: page, Correct version, Name and Forename,
+% Four keywords are used by the package: \emph{page}, \emph{Correct version},
+% \emph{Name} and \emph{Forename},
% defined by the macros |\pagename|, |\correctname|, |\identityname| and |\identityforename|.
% |\pagename| is used in header or footer, |\correctname| by the macro |\correct|
% and both others by the \textbf{identity} style.
% These keywords get automatic translation in a few languages\footnote{Translation
% is currently integrated into the package for the following languages:
-% French, German, Spanish, Italian, Portuguese.}
-% when the \textsf{babel} package is loaded.
+% French, German, Spanish, Italian, Portuges.}
+% according to the selected language by \textsf{babel} package or \textsf{polyglossia}.
% They can be redefined, with |\renewcommand|,
% which should be placed \emph{after} |\begin{document}|,
-% or else we can use macros from the \textsf{babel} package
+% or else we can use macros from the \textsf{translations} package by Clemens Niederberger
% (allowing dynamic language change), e.g.\\
-% |\addto\captionsromanian{\def\pagename{pagin\u{a}}}| (in the preamble).
+% |\DeclareTranslation{romanian}{pagin\u{a}}| (in the preamble).
%
%
% \StopEventually{}
@@ -585,6 +587,7 @@
\RequirePackage{lastpage}
\RequirePackage{fancybox}
\RequirePackage{xcolor}
+\RequirePackage{translations}
\geometry{scale=0.75,vmarginratio=3:4,heightrounded}
@@ -597,47 +600,45 @@
% \subsection{Internationalization}
%
% \begin{macrocode}
-\providecommand{\pagename}{page}
-\def\correctname{Correct version}
-\def\identityname{Name:}
-\def\identityforename{Forename:}
+\DeclareTranslationFallback{schooldocs-pagename}{page}
+\DeclareTranslationFallback{schooldocs-correct}{Correct version}
+\DeclareTranslationFallback{schooldocs-name}{Name:}
+\DeclareTranslationFallback{schooldocs-forename}{Forename:}
-\newcommand\schooldocstranslate{%
- \@ifpackageloaded{babel}{%
- \addto\captionsfrench{%
- \def\correctname{Corrig\'e}
- \def\identityname{Nom :}
- \def\identityforename{Pr\'enom :}
- }
- \addto\captionsgerman{%
- \def\pagename{Seite}
- \def\correctname{Musterl\"osung}
- \def\identityname{Name :}
- \def\identityforename{Vorname :}
- }
- \addto\captionsspanish{%
- \def\pagename{p\'agina}
- \def\correctname{Correccion}
- \def\identityname{Apellido:}
- \def\identityforename{Nombre:}
- }
- \addto\captionsitalian{%
- \def\pagename{Seite}
- \def\correctname{Corretto}
- \def\identityname{Nome :}
- \def\identityforename{Conome :}
- }
- \addto\captionsportuges{%
- \def\pagename{p\'agina}
- \def\correctname{Corrigiu}
- \def\identityname{Nome :}
- \def\identityforename{Primeiro nome :}
- }
- }{}
- }
+\DeclareTranslation{english}{schooldocs-pagename}{page}
+\DeclareTranslation{english}{schooldocs-correct}{Correct version}
+\DeclareTranslation{english}{schooldocs-name}{Name:}
+\DeclareTranslation{english}{schooldocs-forename}{Forename:}
+
+\DeclareTranslation{french}{schooldocs-pagename}{page}
+\DeclareTranslation{french}{schooldocs-correct}{Corrigé}
+\DeclareTranslation{french}{schooldocs-name}{Nom :}
+\DeclareTranslation{french}{schooldocs-forename}{Prénom :}
+
+\DeclareTranslation{german}{schooldocs-pagename}{Seite}
+\DeclareTranslation{german}{schooldocs-correct}{Musterlösung}
+\DeclareTranslation{german}{schooldocs-name}{Name:}
+\DeclareTranslation{german}{schooldocs-forename}{Vorname:}
+
+\DeclareTranslation{spanish}{schooldocs-pagename}{página}
+\DeclareTranslation{spanish}{schooldocs-correct}{Correccion}
+\DeclareTranslation{spanish}{schooldocs-name}{Apellido:}
+\DeclareTranslation{spanish}{schooldocs-forename}{Nombre:}
+
+\DeclareTranslation{italian}{schooldocs-pagename}{pagina}
+\DeclareTranslation{italian}{schooldocs-correct}{Corretto}
+\DeclareTranslation{italian}{schooldocs-name}{Nome:}
+\DeclareTranslation{italian}{schooldocs-forename}{Conome:}
+
+\DeclareTranslation{portuges}{schooldocs-pagename}{página}
+\DeclareTranslation{portuges}{schooldocs-correct}{Corrigiu}
+\DeclareTranslation{portuges}{schooldocs-name}{Nome:}
+\DeclareTranslation{portuges}{schooldocs-forename}{Primeiro nome:}
-\AtBeginDocument{\schooldocstranslate} % if loaded before babel
-\schooldocstranslate % necessary when loaded after babel
+\newcommand*\pagename{\GetTranslation{schooldocs-pagename}}
+\newcommand*\correctname{\GetTranslation{schooldocs-correct}}
+\newcommand*\identityname{\GetTranslation{schooldocs-name}}
+\newcommand*\identityforename{\GetTranslation{schooldocs-forename}}
% \end{macrocode}
%
diff --git a/macros/latex/contrib/schooldocs/schooldocs.ins b/macros/latex/contrib/schooldocs/schooldocs.ins
index f764055184..cc1b62e92c 100644
--- a/macros/latex/contrib/schooldocs/schooldocs.ins
+++ b/macros/latex/contrib/schooldocs/schooldocs.ins
@@ -1,5 +1,5 @@
%%
-%% Copyright (C) 2020 by Antoine Missier <antoine.missier@ac-toulouse.fr>
+%% Copyright (C) 2020-2021 by Antoine Missier <antoine.missier@ac-toulouse.fr>
%%
%% This file may be distributed and/or modified under the conditions of
%% the LaTeX Project Public License, either version 1.3 of this license
@@ -21,7 +21,7 @@
This is a generated file.
-Copyright (C) 2020 by Antoine Missier <antoine.missier@ac-toulouse.fr>
+Copyright (C) 2020-2021 by Antoine Missier <antoine.missier@ac-toulouse.fr>
This file may be distributed and/or modified under the conditions of
the LaTeX Project Public License, either version 1.3 of this license
diff --git a/macros/latex/contrib/schooldocs/schooldocs.pdf b/macros/latex/contrib/schooldocs/schooldocs.pdf
index 28eb97e8bd..63716e22f3 100644
--- a/macros/latex/contrib/schooldocs/schooldocs.pdf
+++ b/macros/latex/contrib/schooldocs/schooldocs.pdf
Binary files differ
diff --git a/macros/latex/contrib/tiscreen/README b/macros/latex/contrib/tiscreen/README
index b7e593c24e..17f81e8592 100644
--- a/macros/latex/contrib/tiscreen/README
+++ b/macros/latex/contrib/tiscreen/README
@@ -1,9 +1,10 @@
tiscreen (TI calculator screen)
-This package mimics the screen of older Texas instruments dot matrix display
-calculators, specifically the TI-82 STATS.
+This package mimics the screen and buttons of older Texas instruments dot
+matrix display calculators, specifically the TI-82 STATS.
Changes:
+- 2021/09/18 Added buttons
- 2021/07/22 Added bracket symbols
- 2021/06/10 Simplified example code in documentation
- 2021/06/07 Simplified characters table in documentation
diff --git a/macros/latex/contrib/tiscreen/doc/tiscreen-doc.pdf b/macros/latex/contrib/tiscreen/doc/tiscreen-doc.pdf
index 33e5b2d735..3b8f41078b 100644
--- a/macros/latex/contrib/tiscreen/doc/tiscreen-doc.pdf
+++ b/macros/latex/contrib/tiscreen/doc/tiscreen-doc.pdf
Binary files differ
diff --git a/macros/latex/contrib/tiscreen/doc/tiscreen-doc.tex b/macros/latex/contrib/tiscreen/doc/tiscreen-doc.tex
index dfe6216dc6..b1e689ba74 100644
--- a/macros/latex/contrib/tiscreen/doc/tiscreen-doc.tex
+++ b/macros/latex/contrib/tiscreen/doc/tiscreen-doc.tex
@@ -1,3 +1,5 @@
+% TODO: make funciton for printing \LCDcmd
+
\documentclass[a4paper,12pt]{article}
\usepackage[iso,english]{isodate}
\usepackage[margin=1in]{geometry}
@@ -17,9 +19,9 @@
}
\newcommand{\LCDsymb}[1]{\large \textLCD{1}|{#1}~|}
-\newcommand{\LCDcmd}{\texttt{\textbackslash LCD}}
+\newcommand{\printcmd}[1]{\texttt{\textbackslash #1}}
-\title{TI calculator display\\{\small(TI-82 STATS)}}
+\title{TI calculator screen (and buttons)\\{\small TI-82 STATS}}
\author{Mustafa Ibrahim}
\begin{document}
@@ -36,13 +38,15 @@
| 62.11246|
|{fcur} |
| |
+
+\tibtnmatrixextra
\end{center}
\tableofcontents\newpage
\section{Quickstart}
-\begin{SideBySideExample}[xrightmargin=6cm]
+\begin{SideBySideExample}[xrightmargin=5.5cm]
%\usepackage[color]{tiscreen}
\tiscreen
@@ -54,15 +58,23 @@
| |
| |
| |
+
+\tibtnmatrixextra
+
+Lorem
+\tibtn[white]{tibtngray}{MATH}
+ipsum
+
+\tibtnextra{\tibtn[white]{tibtngray}{MATH}}{TEST}{A}
\end{SideBySideExample}
\section{Package option(s)}
\subsection{Color}
-Using the \texttt{color} option will change the colors used by the \LCDcmd{}
-command. The colors are defined as \texttt{tiscreenfg} (foreground. i.e. font
-color) and \texttt{tiscreenbg} (background). These colors can be redefined like
-this:
+Using the \texttt{color} option will change the colors used by the
+\printcmd{LCD} command for printing the screen using \printcmd{tiscreen}. The
+colors are defined as \texttt{tiscreenfg} (foreground, i.e. font color) and
+\texttt{tiscreenbg} (background) and redefined like this:
\begin{Verbatim}
% Add this to your preamble
@@ -70,11 +82,12 @@ this:
\definecolor{tiscreenfg}{HTML}{FFFFFF}
\end{Verbatim}
+\part{Screen}
\section{LCD size}
The default LCD size is $8 \times 16$ (the size of the TI-82 STATS). It can be
changed by redefining the variables used to determine the size of the display
-or by using the original \LCDcmd{} command.
+or by using the original \printcmd{LCD} command.
\begin{Verbatim}
% First method (For entire document)
@@ -95,7 +108,7 @@ or by using the original \LCDcmd{} command.
\begin{tabular}{|l|l|>{\ttfamily}l|}
\multicolumn{3}{c}{\textbf{Added characters}} \\
\hline
- Name & Symbol &\textnormal{\LCDcmd{} Code} \\
+ Name & Symbol &\textnormal{\printcmd{LCD} Code} \\
\hline
E & \LCDsymb{sciE} & \{sciE\} \\
$\sigma$ & \LCDsymb{sigma} & \{sigma\} \\
@@ -127,4 +140,31 @@ or by using the original \LCDcmd{} command.
\end{tabular}
\end{center}
+\part{Buttons}
+\section{Usage}
+
+Use the \printcmd{tibtn} command only prints the button and is usefull when
+needing the buttons to be displayed inline. The \printcmd{tibtnextra} takes
+extra arguments to show extra options for the button (accesed by the
+\tibtnsecond{} and \tibtnalpha{} buttons).
+
+\begin{SideBySideExample}[xrightmargin=5.5cm]
+\tibtnextra{\tibtn[white]{tibtngray}{MATH}}{TEST}{A}
+
+Lorem
+\tibtn[white]{tibtngray}{MATH}
+ipsum
+\end{SideBySideExample}
+
+\section{Defined buttons}
+
+\begin{center}
+ \begin{tabular}{|>{\ttfamily}l|l|}
+ \multicolumn{2}{c}{\textbf{Defined buttons}} \\ \hline
+ \textnormal{Command} & Output \\ \hline
+ \printcmd{tibtnmatrix} & \tibtnmatrix{} \\ \hline
+ \printcmd{tibtnmatrixextra} & \tibtnmatrixextra{} \\ \hline
+ \end{tabular}
+\end{center}
+
\end{document}
diff --git a/macros/latex/contrib/tiscreen/tex/tiscreen.sty b/macros/latex/contrib/tiscreen/tex/tiscreen.sty
index 36cbdedfd4..d4219f1eb7 100644
--- a/macros/latex/contrib/tiscreen/tex/tiscreen.sty
+++ b/macros/latex/contrib/tiscreen/tex/tiscreen.sty
@@ -1,18 +1,20 @@
% Copyright (C) 2021 Mustafa Ibrahim
-% This work may be distributed and/or modified under the conditions of the LaTeX
-% Project Public License, either version 1.3c or higher. The latest version the
-% license is at: http://www.latex-project.org/lppl.txt
+% This work may be distributed and/or modified under the conditions of the
+% LaTeX Project Public License, either version 1.3c or higher. The latest
+% version the license is at: http://www.latex-project.org/lppl.txt
\NeedsTeXFormat{LaTeX2e}
-\ProvidesPackage{tiscreen}[2021/07/22 Ti calculator screen]
-
-% TODO: Scaling option
+\ProvidesPackage{tiscreen}[2021/09/18 Ti calculator screen]
\def\tiscreenX{16}
\def\tiscreenY{8}
\RequirePackage{lcd}
-% Define
+\RequirePackage{tikz}
+\RequirePackage{tcolorbox}
+\RequirePackage{array}
+
+% Define screen characters
\DefineLCDchar{barx}{11111000001000101010001000101010001}
\DefineLCDchar{bary}{11111000001000110001011110000101110}
\DefineLCDchar{ar}{00011000011100100001000010000000000}
@@ -26,7 +28,7 @@
\DefineLCDchar{sub5}{00000000000111001000011000001001100}
\DefineLCDchar{sub6}{00000000000011001000011100101001110}
\DefineLCDchar{cx}{00101000100010100000000000000000000}
-% Redefine
+% Redefine screen characters
\DefineLCDchar{sqrt}{00111001000010000100101000110000100}
\DefineLCDchar{!}{00100001000010000100000000010000100}
\DefineLCDchar{e}{00000000000111010001111101000001100}
@@ -39,11 +41,39 @@
\DefineLCDchar{rb}{11000001000010000010001000010011000}
\RequirePackage{xcolor}
+% Screen colors
\definecolor{tiscreenfg}{HTML}{000000}
\definecolor{tiscreenbg}{HTML}{5d9345}
+% Button colors
+\definecolor{tibtngray}{HTML}{494F54}
+\definecolor{tibtnyellow}{HTML}{F1E78B}
+\definecolor{tibtngreen}{HTML}{D5DE93}
+\definecolor{tibtnwhite}{HTML}{E2E2E2}
+\definecolor{tibtnblue}{HTML}{CDE2E9}
+\definecolor{tibtncaseblack}{HTML}{232A32}
\DeclareOption{color}{\LCDcolors{tiscreenfg}{tiscreenbg}}
\DeclareOption*{\PackageWarning{tiscreen}{Unknown ‘\CurrentOption’}}
\ProcessOptions\relax
+% Screen commands
\newcommand{\tiscreen}{\LCD{\tiscreenY}{\tiscreenX}}
+
+% Button commands
+\newcommand{\tibtn}[3][black]{\tikz{\bfseries\tiny\sffamily \node[fill=#2, draw, rounded corners, minimum height=2.5em, minimum width=4.5em, align=center] {\textcolor{#1}{#3}};}}
+\newcommand{\tibtnextra}[3]{%
+ \tcbox[colback=tibtncaseblack,size=small]{%
+ \begin{tabular}{l@{}r}%
+ \bfseries\tiny\sffamily \textcolor{tibtnyellow}{#2} &
+ \bfseries\tiny\sffamily \textcolor{tibtngreen}{#3} \\
+ \multicolumn{2}{c}{#1}
+ \end{tabular}%
+ }%
+}
+
+% Button definitions
+\def\tibtnsecond{\tibtn{tibtnyellow}{2nd}}
+\def\tibtnalpha{\tibtn{tibtngreen}{ALPHA}}
+
+\def\tibtnmatrix{\tibtn[white]{tibtngray}{MATRX}}
+\def\tibtnmatrixextra{\tibtnextra{\tibtnmatrix}{ANGLE}{B}}
diff --git a/macros/luatex/generic/luakeys/luakeys-debug.sty b/macros/luatex/generic/luakeys/luakeys-debug.sty
index 77dfa0063e..a96d2d51c6 100644
--- a/macros/luatex/generic/luakeys/luakeys-debug.sty
+++ b/macros/luatex/generic/luakeys/luakeys-debug.sty
@@ -17,23 +17,6 @@
% and luakeys-debug.tex.
\NeedsTeXFormat{LaTeX2e}
-\ProvidesPackage{luakeys-debug}[2021/01/18 Debug package for luakeys.]
+\ProvidesPackage{luakeys-debug}[2021/09/19 Debug package for luakeys.]
-%\input luakeys-debug.tex
-
-\directlua{
- luakeys = require('luakeys')
-}
-
-\newcommand{\luakeysdebug}[2][]{
- {
- \tt
- \parindent=0pt
- \directlua{
- local options_raw = luakeys.parse('#1')
- local result = luakeys.parse('#2', options_raw)
- tex.print(luakeys.stringify(result, true))
- luakeys.print(result)
- }
- }
-}
+\input luakeys-debug.tex
diff --git a/macros/luatex/generic/luakeys/luakeys-debug.tex b/macros/luatex/generic/luakeys/luakeys-debug.tex
index 499b1305f6..b63e67e050 100644
--- a/macros/luatex/generic/luakeys/luakeys-debug.tex
+++ b/macros/luatex/generic/luakeys/luakeys-debug.tex
@@ -20,14 +20,43 @@
luakeys = require('luakeys')
}
-\def\luakeysdebug#1{
+% https://tex.stackexchange.com/a/418401/42311
+\catcode`\@=11
+\long\def\LuaKeysIfNextChar#1#2#3{%
+ \let\@tmpa=#1%
+ \def\@tmpb{#2}%
+ \def\@tmpc{#3}%
+ \futurelet\@future\LuaKeysIfNextChar@i%
+}%
+\def\LuaKeysIfNextChar@i{%
+ \ifx\@tmpa\@future%
+ \expandafter\@tmpb
+ \else
+ \expandafter\@tmpc
+ \fi
+}%
+\def\luakeysdebug@parse@options#1{
+ \directlua{
+ luakeys.save('debug_options', luakeys.parse('#1'))
+ }
+}%
+\def\luakeysdebug@output#1{
{
\tt
\parindent=0pt
\directlua{
- local result = luakeys.parse('#1')
+ local result = luakeys.parse('\luaescapestring{\unexpanded{#1}}', luakeys.get('debug_options'))
tex.print(luakeys.stringify(result, true))
luakeys.print(result)
}
}
-}
+}%
+\def\luakeysdebug@oarg[#1]#2{%
+ \luakeysdebug@parse@options{#1}%
+ \luakeysdebug@output{#2}%
+}%
+\def\luakeysdebug@marg#1{%
+ \luakeysdebug@output{#1}%
+}%
+\def\luakeysdebug{\LuaKeysIfNextChar[{\luakeysdebug@oarg}{\luakeysdebug@marg}}%
+\catcode`\@=12
diff --git a/macros/luatex/generic/luakeys/luakeys.lua b/macros/luatex/generic/luakeys/luakeys.lua
index e4ecaab7f7..b026413800 100644
--- a/macros/luatex/generic/luakeys/luakeys.lua
+++ b/macros/luatex/generic/luakeys/luakeys.lua
@@ -41,16 +41,19 @@ if not tex then
end
end
+--- A table to store parsed key-value results.
+local result_store = {}
+
--- Generate the PEG parser using Lpeg.
--
--- @treturn userdata The parser
+-- @treturn userdata The parser.
local function generate_parser(options)
-- Optional whitespace
- local white_space = lpeg.S(' \t\n\r')^0
+ local white_space = lpeg.S(' \t\n\r')
--- Match literal string surrounded by whitespace
local ws = function(match)
- return white_space * lpeg.P(match) * white_space
+ return white_space^0 * lpeg.P(match) * white_space^0
end
local boolean_true =
@@ -87,17 +90,24 @@ local function generate_parser(options)
local sign = lpeg.S('-+')
local integer = lpeg.R('09')^1
local tex_number = (integer^1 * (lpeg.P('.') * integer^1)^0) + (lpeg.P('.') * integer^1)
- local unit
- -- https://raw.githubusercontent.com/latex3/lualibs/master/lualibs-util-dim.lua
- for _, dimension_extension in ipairs({'bp', 'cc', 'cm', 'dd', 'em', 'ex', 'in', 'mm', 'nc', 'nd', 'pc', 'pt', 'sp'}) do
- if unit then
- unit = unit + lpeg.P(dimension_extension)
- else
- unit = lpeg.P(dimension_extension)
- end
- end
- local dimension = (sign^0 * tex_number * unit)
+ -- https://raw.githubusercontent.com/latex3/lualibs/master/lualibs-util-dim.lua
+ local unit =
+ lpeg.P('bp') + lpeg.P('BP') +
+ lpeg.P('cc') + lpeg.P('CC') +
+ lpeg.P('cm') + lpeg.P('CM') +
+ lpeg.P('dd') + lpeg.P('DD') +
+ lpeg.P('em') + lpeg.P('EM') +
+ lpeg.P('ex') + lpeg.P('EX') +
+ lpeg.P('in') + lpeg.P('IN') +
+ lpeg.P('mm') + lpeg.P('MM') +
+ lpeg.P('nc') + lpeg.P('NC') +
+ lpeg.P('nd') + lpeg.P('ND') +
+ lpeg.P('pc') + lpeg.P('PC') +
+ lpeg.P('pt') + lpeg.P('PT') +
+ lpeg.P('sp') + lpeg.P('SP')
+
+ local dimension = (sign^0 * white_space^0 * tex_number * white_space^0 * unit)
if options.convert_dimensions then
return dimension / tex.sp
@@ -106,7 +116,7 @@ local function generate_parser(options)
end
end
- --- Add values to a table in a two modes:
+ --- Add values to a table in two modes:
--
-- # Key value pair
--
@@ -150,15 +160,7 @@ local function generate_parser(options)
) * ws(',')^-1,
key_value_pair =
- (lpeg.V('key') * ws('=')) * (lpeg.V('list_container') + lpeg.V('value')),
-
- -- ./ for tikz style keys
- key_word = lpeg.R('az', 'AZ', '09', './'),
-
- key = white_space * lpeg.C(
- lpeg.V('key_word')^1 *
- (lpeg.P(' ')^1 * lpeg.V('key_word')^1)^0
- ) * white_space,
+ (lpeg.V('value') * ws('=')) * (lpeg.V('list_container') + lpeg.V('value')),
value =
lpeg.V('boolean') +
@@ -174,25 +176,25 @@ local function generate_parser(options)
dimension = build_dimension_pattern(),
string_quoted =
- white_space * lpeg.P('"') *
+ white_space^0 * lpeg.P('"') *
lpeg.C((lpeg.P('\\"') + 1 - lpeg.P('"'))^0) *
- lpeg.P('"') * white_space,
+ lpeg.P('"') * white_space^0,
string_unquoted =
- white_space *
- lpeg.C((1 - lpeg.S('{},='))^1) *
- white_space,
+ white_space^0 *
+ lpeg.C(
+ lpeg.V('word_unquoted')^1 *
+ (lpeg.S(' \t')^1 * lpeg.V('word_unquoted')^1)^0) *
+ white_space^0,
+
+ word_unquoted = (1 - white_space - lpeg.S('{},='))^1;
number =
- white_space * (number / tonumber) * white_space,
+ white_space^0 * (number / tonumber) * white_space^0,
})
end
-local function trim(input_string)
- return input_string:gsub('^%s*(.-)%s*$', '%1')
-end
-
--- Get the size of an array like table `{ 'one', 'two', 'three' }` = 3.
--
-- @tparam table value A table or any input.
@@ -238,16 +240,19 @@ local function unpack_single_valued_array_table(value)
return value
end
---- This normalization tasks are performed on the raw input table
--- coming directly from the PEG parser:
+--- This normalization tasks are performed on the raw input table coming
+-- directly from the PEG parser:
--
-- 1. Trim all strings: ` text \n` into `text`
--- 2. Unpack all single valued array like tables: `{ 'text' }`
--- into `text`
+-- 2. Unpack all single valued array like tables: `{ 'text' }` into
+-- `text`
--
-- @tparam table raw The raw input table coming directly from the PEG
-- parser
--
+-- @tparam table options Some options. A table with the key
+-- `unpack_single_array_values`
+--
-- @treturn table A normalized table ready for the outside world.
local function normalize(raw, options)
local function normalize_recursive(raw, result, options)
@@ -257,8 +262,6 @@ local function normalize(raw, options)
end
if type(value) == 'table' then
result[key] = normalize_recursive(value, {}, options)
- elseif type(value) == 'string' then
- result[key] = trim(value)
else
result[key] = value
end
@@ -288,8 +291,8 @@ local function stringify(input, for_tex)
if for_tex then
line_break = '\\par'
- start_bracket = '\\{'
- end_bracket = '\\}'
+ start_bracket = '$\\{$'
+ end_bracket = '$\\}$'
indent = '\\ \\ '
else
line_break = '\n'
@@ -306,6 +309,10 @@ local function stringify(input, for_tex)
table.insert(output, string.rep(indent, depth) .. text)
end
+ if type(input) ~= 'table' then
+ return tostring(input)
+ end
+
for key, value in pairs(input) do
if (key and type(key) == 'number' or type(key) == 'string') then
key = string.format('[\'%s\']', key);
@@ -423,7 +430,7 @@ return {
if kv_string == nil then
return {}
end
- options = normalize_parse_options()
+ options = normalize_parse_options(options)
local parser = generate_parser(options)
return normalize(parser:match(kv_string), options)
@@ -480,4 +487,27 @@ return {
print(stringify(tbl, false))
end,
+ --- The function `save(identifier, result): void` saves a result (a
+ -- table from a previous run of `parse`) under an identifier.
+ -- Therefore, it is not necessary to pollute the global namespace to
+ -- store results for the later usage.
+ --
+ -- @tparam string identifier The identifier under which the result is
+ -- saved.
+ --
+ -- @tparam table result A result to be stored and that was created by
+ -- the key-value parser.
+ save = function(identifier, result)
+ result_store[identifier] = result
+ end,
+
+ --- The function `get(identifier): table` retrieves a saved result
+ -- from the result store.
+ --
+ -- @tparam string identifier The identifier under which the result was
+ -- saved.
+ get = function(identifier)
+ return result_store[identifier]
+ end,
+
}
diff --git a/macros/luatex/generic/luakeys/luakeys.pdf b/macros/luatex/generic/luakeys/luakeys.pdf
index ceaebbee04..c65765d5b9 100644
--- a/macros/luatex/generic/luakeys/luakeys.pdf
+++ b/macros/luatex/generic/luakeys/luakeys.pdf
Binary files differ
diff --git a/macros/luatex/generic/luakeys/luakeys.tex b/macros/luatex/generic/luakeys/luakeys.tex
index 75ffbc8c70..fc465f46ae 100644
--- a/macros/luatex/generic/luakeys/luakeys.tex
+++ b/macros/luatex/generic/luakeys/luakeys.tex
@@ -28,9 +28,9 @@
\author{%
Josef Friedrich\\%
\url{josef@friedrich.rocks}\\%
- \href{https://github.com/Josef-Friedrich/nodetree}{github.com/Josef-Friedrich/luakeys}%
+ \href{https://github.com/Josef-Friedrich/luakeys}{github.com/Josef-Friedrich/luakeys}%
}
-\date{v0.1 from 2021/01/18}
+\date{v0.2 from 2021/09/19}
\maketitle
@@ -105,6 +105,27 @@ This package would not be possible without the article
\clearpage
+\section{Usage}
+
+\begin{minted}{latex}
+\documentclass{article}
+\directlua{
+ luakeys = require('luakeys')
+}
+
+\newcommand{\helloworld}[2][]{
+ \directlua{
+ local keys = luakeys.parse('\luaescapestring{\unexpanded{#1}}')
+ luakeys.print(keys)
+ local marg = '#2'
+ tex.print(keys.greeting .. ', ' .. marg .. keys.punctuation)
+ }
+}
+\begin{document}
+\helloworld[greeting=hello,punctuation=!]{world}
+\end{document}
+\end{minted}
+
\section{Syntax of the recognized key-value format}
%%
@@ -123,7 +144,7 @@ lists (\texttt{level1=\{level2=\{key=value,value\}\}}).
%
%%
-\subsection{An (incomplete) attempt to put the syntax into the (Extended) Backus-Naur form}
+\subsection{An (incomplete) attempt to put the syntax into the Extended Backus-Naur Form}
\begin{grammar}
<list> ::= <list-item> | <list-item> <list>
@@ -137,6 +158,25 @@ lists (\texttt{level1=\{level2=\{key=value,value\}\}}).
\alt <number>
\alt <string-quoted>
\alt <string-unquoted>
+
+<sign> ::= `-' | `+'
+
+<integer> ::= `0' | `1' | `2' | `3' | `4' | `5' | `6' | `7' | `8' | `9'
+
+<unit> ::= `bp' | `BP'
+ \alt `cc' | `CC'
+ \alt `cm' | `CM'
+ \alt `dd' | `DD'
+ \alt `em' | `EM'
+ \alt `ex' | `EX'
+ \alt `in' | `IN'
+ \alt `mm' | `MM'
+ \alt `nc' | `NC'
+ \alt `nd' | `ND'
+ \alt `pc' | `PC'
+ \alt `pt' | `PT'
+ \alt `sp' | `SP'
+
\end{grammar}
... to be continued
@@ -226,7 +266,8 @@ local result = parse('dim=1cm', {
})
\end{minted}
-If you want to convert a scale point into a unit string you can used the module
+\noindent
+If you want to convert a scale point into a unit string you can use the module
\href{https://raw.githubusercontent.com/latex3/lualibs/master/lualibs-util-dim.lua}{lualibs-util-dim.lua}.
\begin{minted}{latex}
@@ -317,6 +358,46 @@ marks must be used.
\end{minted}
\end{multicols}
+\subsubsection{Standalone values}
+
+Standalone values are values without a key. They are converted into an
+array. In Lua an array is a table with numeric indexes (The first index
+is 1).
+
+\begin{multicols}{2}
+\begin{minted}{latex}
+\luakeysdebug{one,two,three}
+\end{minted}
+\columnbreak
+\begin{minted}{lua}
+{ 'one', 'two', 'three' }
+\end{minted}
+
+\noindent
+is equivalent to
+
+\begin{minted}{lua}
+{
+ [1] = 'one',
+ [2] = 'two',
+ [3] = 'three',
+}
+\end{minted}
+\end{multicols}
+
+\noindent
+All recognized data types can be used as standalone values.
+
+\begin{multicols}{2}
+\begin{minted}{latex}
+\luakeysdebug{one,2,3cm}
+\end{minted}
+\columnbreak
+\begin{minted}{lua}
+{ 'one', 2, 5594039 }
+\end{minted}
+\end{multicols}
+
%-----------------------------------------------------------------------
%
%-----------------------------------------------------------------------
@@ -325,7 +406,7 @@ marks must be used.
\section{Exported functions of the Lua module \texttt{luakeys.lua}}
-To learn more about the individual functions (local functions), pleasecommand
+To learn more about the individual functions (local functions), please
read the \href{https://josef-friedrich.github.io/luakeys/}{source code
documentation}, which was created with
\href{http://stevedonovan.github.io/ldoc/}{LDoc}. The Lua module exports
@@ -336,6 +417,8 @@ local luakeys = require('luakeys')
local parse = luakeys.parse
local render = luakeys.render
--local print = luakeys.print -- That would overwrite the built-in Lua function
+local save = luakeys.save
+local get = luakeys.get
\end{minted}
%%
@@ -443,6 +526,26 @@ The output should look like this:
}
\end{minted}
+%%
+%
+%%
+
+\subsection{\texttt{save(identifier, result): void}}
+
+The function \lua{save(identifier, result)} saves a result (a
+table from a previous run of \lua{parse}) under an identifier.
+Therefore, it is not necessary to pollute the global namespace to
+store results for the later usage.
+
+%%
+%
+%%
+
+\subsection{\texttt{get(identifier): table}}
+
+The function \lua{get(identifier)} retrieves a saved result from the
+result store.
+
%-----------------------------------------------------------------------
%
%-----------------------------------------------------------------------
@@ -535,8 +638,12 @@ An example of how to use the command in \LaTeX:
\inputminted[linenos=true]{latex}{luakeys-debug.sty}
-\changes{v0.1}{2020/01/17}{Inital release}
-
+\changes{v0.1}{2021/01/18}{Inital release}
+\changes{v0.2}{2021/09/19}{
+* Allow all recognized data types as keys
+* Allow TeX macros in the values
+* New public Lua functions: save(identifier, result), get(identifier)
+}
\pagebreak
\PrintChanges
\pagebreak
diff --git a/macros/luatex/generic/penlight/README.md b/macros/luatex/generic/penlight/README.md
new file mode 100644
index 0000000000..55544e7d9f
--- /dev/null
+++ b/macros/luatex/generic/penlight/README.md
@@ -0,0 +1,29 @@
+# penlight -- Lua libraries for use in LuaLaTeX
+
+This LuaLaTeX package provides a thin-wrapper to use the [penlight](https://github.com/lunarmodules/Penlight) Lua libraries with LuaLaTeX. It uses the `luapackageloader` package.
+
+
+
+# License
+
+Copyright (C) 2021 Kale Ewasiuk
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/macros/luatex/generic/penlight/penlight.pdf b/macros/luatex/generic/penlight/penlight.pdf
new file mode 100644
index 0000000000..3502bb32ec
--- /dev/null
+++ 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
new file mode 100644
index 0000000000..7a04192f58
--- /dev/null
+++ b/macros/luatex/generic/penlight/penlight.sty
@@ -0,0 +1,36 @@
+% Kale Ewasiuk (kalekje@gmail.com)
+% 2021-09-19
+%
+% Copyright (C) 2021 Kale Ewasiuk
+%
+% Permission is hereby granted, free of charge, to any person obtaining a copy
+% of this software and associated documentation files (the "Software"), to deal
+% in the Software without restriction, including without limitation the rights
+% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+% copies of the Software, and to permit persons to whom the Software is
+% furnished to do so, subject to the following conditions:
+%
+% The above copyright notice and this permission notice shall be included in
+% all copies or substantial portions of the Software.
+%
+% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+% ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+% TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+% PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+% SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+% ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+% OR OTHER DEALINGS IN THE SOFTWARE.
+
+\ProvidesPackage{penlight}[2021-09-19]
+
+\RequirePackage{luapackageloader}
+
+\directlua{penlight = require('pl.import_into')()}
+\DeclareOption{pl}{\directlua{pl = penlight}}
+\DeclareOption{stringx}{\directlua{require('pl.stringx').import()}}
+\DeclareOption{format}{\directlua{require('pl.text').format_operator()}}
+\DeclareOption{func}{\directlua{penlight.utils.import('pl.func')}}
+
+\ProcessOptions\relax
diff --git a/macros/luatex/generic/penlight/penlight.tex b/macros/luatex/generic/penlight/penlight.tex
new file mode 100644
index 0000000000..2259cf27fd
--- /dev/null
+++ b/macros/luatex/generic/penlight/penlight.tex
@@ -0,0 +1,84 @@
+% Kale Ewasiuk (kalekje@gmail.com)
+% 2021-09-19
+%
+% Copyright (C) 2021 Kale Ewasiuk
+%
+% Permission is hereby granted, free of charge, to any person obtaining a copy
+% of this software and associated documentation files (the "Software"), to deal
+% in the Software without restriction, including without limitation the rights
+% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+% copies of the Software, and to permit persons to whom the Software is
+% furnished to do so, subject to the following conditions:
+%
+% The above copyright notice and this permission notice shall be included in
+% all copies or substantial portions of the Software.
+%
+% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+% ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+% TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+% PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+% SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+% ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+% ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+% OR OTHER DEALINGS IN THE SOFTWARE.
+
+\documentclass{article}
+\usepackage[pl]{penlight}
+\usepackage{url}
+\begin{document}
+
+ \section*{penlight -- Lua libraries for use in LuaLaTeX}
+ v. 2021-09.19, Kale Ewasiuk, \url{kalekje@gmail.com}
+
+ \subsection*{Default}
+
+ \begin{verbatim}
+ \usepackage{penlight}
+ \end{verbatim}
+
+ If no options are used when using this package,
+ then all penlight packages are available under the lua variable \texttt{penlight}.
+ Here is an example:
+
+ \begin{verbatim}
+ \begin{luacode*}
+ texio.write_nl(penlight.pretty.write(penlight.List('hello')))
+ \end{luacode*}
+ \end{verbatim}
+
+ The official documentation for the lua library can be found here:\\
+ \mbox{\url{https://stevedonovan.github.io/Penlight/api/manual/01-introduction.md.html#}}
+
+ \subsection*{Package Options}
+
+ \noindent
+ \begin{tabular}{lp{4.5in}}
+ \texttt{pl} & will make penlight packages are available under the global lua variable \texttt{pl} or \texttt{penlight}.\\\\
+ \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}\\\\
+ \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}\\\\
+ \end{tabular}
+
+ \begin{verbatim}
+
+ \end{verbatim}
+
+
+
+
+
+ \section*{}
+ Disclaimer: I am not the author of the Lua penlight library.
+ penlight is Copyright \textcopyright 2009-2016 Steve Donovan, David Manura.
+ The distribution of penlight used for this library is:
+\url{https://github.com/lunarmodules/penlight}
+
+
+\end{document} \ No newline at end of file
diff --git a/macros/luatex/generic/penlight/pl/Date.lua b/macros/luatex/generic/penlight/pl/Date.lua
new file mode 100644
index 0000000000..039dbaf7fd
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/Date.lua
@@ -0,0 +1,675 @@
+--- Date and Date Format classes.
+-- See @{05-dates.md|the Guide}.
+--
+-- NOTE: the date module is deprecated! see
+-- https://github.com/lunarmodules/Penlight/issues/285
+--
+-- Dependencies: `pl.class`, `pl.stringx`, `pl.utils`
+-- @classmod pl.Date
+-- @pragma nostrip
+
+local class = require 'pl.class'
+local os_time, os_date = os.time, os.date
+local stringx = require 'pl.stringx'
+local utils = require 'pl.utils'
+local assert_arg,assert_string = utils.assert_arg,utils.assert_string
+
+
+utils.raise_deprecation {
+ source = "Penlight " .. utils._VERSION,
+ message = "the 'Date' module is deprecated, see https://github.com/lunarmodules/Penlight/issues/285",
+ version_removed = "2.0.0",
+ version_deprecated = "1.9.2",
+}
+
+
+local Date = class()
+Date.Format = class()
+
+--- Date constructor.
+-- @param t this can be either
+--
+-- * `nil` or empty - use current date and time
+-- * number - seconds since epoch (as returned by `os.time`). Resulting time is UTC
+-- * `Date` - make a copy of this date
+-- * table - table containing year, month, etc as for `os.time`. You may leave out year, month or day,
+-- in which case current values will be used.
+-- * year (will be followed by month, day etc)
+--
+-- @param ... true if Universal Coordinated Time, or two to five numbers: month,day,hour,min,sec
+-- @function Date
+function Date:_init(t,...)
+ local time
+ local nargs = select('#',...)
+ if nargs > 2 then
+ local extra = {...}
+ local year = t
+ t = {
+ year = year,
+ month = extra[1],
+ day = extra[2],
+ hour = extra[3],
+ min = extra[4],
+ sec = extra[5]
+ }
+ end
+ if nargs == 1 then
+ self.utc = select(1,...) == true
+ end
+ if t == nil or t == 'utc' then
+ time = os_time()
+ self.utc = t == 'utc'
+ elseif type(t) == 'number' then
+ time = t
+ if self.utc == nil then self.utc = true end
+ elseif type(t) == 'table' then
+ if getmetatable(t) == Date then -- copy ctor
+ time = t.time
+ self.utc = t.utc
+ else
+ if not (t.year and t.month) then
+ local lt = os_date('*t')
+ if not t.year and not t.month and not t.day then
+ t.year = lt.year
+ t.month = lt.month
+ t.day = lt.day
+ else
+ t.year = t.year or lt.year
+ t.month = t.month or (t.day and lt.month or 1)
+ t.day = t.day or 1
+ end
+ end
+ t.day = t.day or 1
+ time = os_time(t)
+ end
+ else
+ error("bad type for Date constructor: "..type(t),2)
+ end
+ self:set(time)
+end
+
+--- set the current time of this Date object.
+-- @int t seconds since epoch
+function Date:set(t)
+ self.time = t
+ if self.utc then
+ self.tab = os_date('!*t',t)
+ else
+ self.tab = os_date('*t',t)
+ end
+end
+
+--- get the time zone offset from UTC.
+-- @int ts seconds ahead of UTC
+function Date.tzone (ts)
+ if ts == nil then
+ ts = os_time()
+ elseif type(ts) == "table" then
+ if getmetatable(ts) == Date then
+ ts = ts.time
+ else
+ ts = Date(ts).time
+ end
+ end
+ local utc = os_date('!*t',ts)
+ local lcl = os_date('*t',ts)
+ lcl.isdst = false
+ return os.difftime(os_time(lcl), os_time(utc))
+end
+
+--- convert this date to UTC.
+function Date:toUTC ()
+ local ndate = Date(self)
+ if not self.utc then
+ ndate.utc = true
+ ndate:set(ndate.time)
+ end
+ return ndate
+end
+
+--- convert this UTC date to local.
+function Date:toLocal ()
+ local ndate = Date(self)
+ if self.utc then
+ ndate.utc = false
+ ndate:set(ndate.time)
+--~ ndate:add { sec = Date.tzone(self) }
+ end
+ return ndate
+end
+
+--- set the year.
+-- @int y Four-digit year
+-- @class function
+-- @name Date:year
+
+--- set the month.
+-- @int m month
+-- @class function
+-- @name Date:month
+
+--- set the day.
+-- @int d day
+-- @class function
+-- @name Date:day
+
+--- set the hour.
+-- @int h hour
+-- @class function
+-- @name Date:hour
+
+--- set the minutes.
+-- @int min minutes
+-- @class function
+-- @name Date:min
+
+--- set the seconds.
+-- @int sec seconds
+-- @class function
+-- @name Date:sec
+
+--- set the day of year.
+-- @class function
+-- @int yday day of year
+-- @name Date:yday
+
+--- get the year.
+-- @int y Four-digit year
+-- @class function
+-- @name Date:year
+
+--- get the month.
+-- @class function
+-- @name Date:month
+
+--- get the day.
+-- @class function
+-- @name Date:day
+
+--- get the hour.
+-- @class function
+-- @name Date:hour
+
+--- get the minutes.
+-- @class function
+-- @name Date:min
+
+--- get the seconds.
+-- @class function
+-- @name Date:sec
+
+--- get the day of year.
+-- @class function
+-- @name Date:yday
+
+
+for _,c in ipairs{'year','month','day','hour','min','sec','yday'} do
+ Date[c] = function(self,val)
+ if val then
+ assert_arg(1,val,"number")
+ self.tab[c] = val
+ self:set(os_time(self.tab))
+ return self
+ else
+ return self.tab[c]
+ end
+ end
+end
+
+--- name of day of week.
+-- @bool full abbreviated if true, full otherwise.
+-- @ret string name
+function Date:weekday_name(full)
+ return os_date(full and '%A' or '%a',self.time)
+end
+
+--- name of month.
+-- @int full abbreviated if true, full otherwise.
+-- @ret string name
+function Date:month_name(full)
+ return os_date(full and '%B' or '%b',self.time)
+end
+
+--- is this day on a weekend?.
+function Date:is_weekend()
+ return self.tab.wday == 1 or self.tab.wday == 7
+end
+
+--- add to a date object.
+-- @param t a table containing one of the following keys and a value:
+-- one of `year`,`month`,`day`,`hour`,`min`,`sec`
+-- @return this date
+function Date:add(t)
+ local old_dst = self.tab.isdst
+ local key,val = next(t)
+ self.tab[key] = self.tab[key] + val
+ self:set(os_time(self.tab))
+ if old_dst ~= self.tab.isdst then
+ self.tab.hour = self.tab.hour - (old_dst and 1 or -1)
+ self:set(os_time(self.tab))
+ end
+ return self
+end
+
+--- last day of the month.
+-- @return int day
+function Date:last_day()
+ local d = 28
+ local m = self.tab.month
+ while self.tab.month == m do
+ d = d + 1
+ self:add{day=1}
+ end
+ self:add{day=-1}
+ return self
+end
+
+--- difference between two Date objects.
+-- @tparam Date other Date object
+-- @treturn Date.Interval object
+function Date:diff(other)
+ local dt = self.time - other.time
+ if dt < 0 then error("date difference is negative!",2) end
+ return Date.Interval(dt)
+end
+
+--- long numerical ISO data format version of this date.
+function Date:__tostring()
+ local fmt = '%Y-%m-%dT%H:%M:%S'
+ if self.utc then
+ fmt = "!"..fmt
+ end
+ local t = os_date(fmt,self.time)
+ if self.utc then
+ return t .. 'Z'
+ else
+ local offs = self:tzone()
+ if offs == 0 then
+ return t .. 'Z'
+ end
+ local sign = offs > 0 and '+' or '-'
+ local h = math.ceil(offs/3600)
+ local m = (offs % 3600)/60
+ if m == 0 then
+ return t .. ('%s%02d'):format(sign,h)
+ else
+ return t .. ('%s%02d:%02d'):format(sign,h,m)
+ end
+ end
+end
+
+--- equality between Date objects.
+function Date:__eq(other)
+ return self.time == other.time
+end
+
+--- ordering between Date objects.
+function Date:__lt(other)
+ return self.time < other.time
+end
+
+--- difference between Date objects.
+-- @function Date:__sub
+Date.__sub = Date.diff
+
+--- add a date and an interval.
+-- @param other either a `Date.Interval` object or a table such as
+-- passed to `Date:add`
+function Date:__add(other)
+ local nd = Date(self)
+ if Date.Interval:class_of(other) then
+ other = {sec=other.time}
+ end
+ nd:add(other)
+ return nd
+end
+
+Date.Interval = class(Date)
+
+---- Date.Interval constructor
+-- @int t an interval in seconds
+-- @function Date.Interval
+function Date.Interval:_init(t)
+ self:set(t)
+end
+
+function Date.Interval:set(t)
+ self.time = t
+ self.tab = os_date('!*t',self.time)
+end
+
+local function ess(n)
+ if n > 1 then return 's '
+ else return ' '
+ end
+end
+
+--- If it's an interval then the format is '2 hours 29 sec' etc.
+function Date.Interval:__tostring()
+ local t, res = self.tab, ''
+ local y,m,d = t.year - 1970, t.month - 1, t.day - 1
+ if y > 0 then res = res .. y .. ' year'..ess(y) end
+ if m > 0 then res = res .. m .. ' month'..ess(m) end
+ if d > 0 then res = res .. d .. ' day'..ess(d) end
+ if y == 0 and m == 0 then
+ local h = t.hour
+ if h > 0 then res = res .. h .. ' hour'..ess(h) end
+ if t.min > 0 then res = res .. t.min .. ' min ' end
+ if t.sec > 0 then res = res .. t.sec .. ' sec ' end
+ end
+ if res == '' then res = 'zero' end
+ return res
+end
+
+------------ Date.Format class: parsing and renderinig dates ------------
+
+-- short field names, explicit os.date names, and a mask for allowed field repeats
+local formats = {
+ d = {'day',{true,true}},
+ y = {'year',{false,true,false,true}},
+ m = {'month',{true,true}},
+ H = {'hour',{true,true}},
+ M = {'min',{true,true}},
+ S = {'sec',{true,true}},
+}
+
+--- Date.Format constructor.
+-- @string fmt. A string where the following fields are significant:
+--
+-- * d day (either d or dd)
+-- * y year (either yy or yyy)
+-- * m month (either m or mm)
+-- * H hour (either H or HH)
+-- * M minute (either M or MM)
+-- * S second (either S or SS)
+--
+-- Alternatively, if fmt is nil then this returns a flexible date parser
+-- that tries various date/time schemes in turn:
+--
+-- * [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601), like `2010-05-10 12:35:23Z` or `2008-10-03T14:30+02`
+-- * times like 15:30 or 8.05pm (assumed to be today's date)
+-- * dates like 28/10/02 (European order!) or 5 Feb 2012
+-- * month name like march or Mar (case-insensitive, first 3 letters); here the
+-- day will be 1 and the year this current year
+--
+-- A date in format 3 can be optionally followed by a time in format 2.
+-- Please see test-date.lua in the tests folder for more examples.
+-- @usage df = Date.Format("yyyy-mm-dd HH:MM:SS")
+-- @class function
+-- @name Date.Format
+function Date.Format:_init(fmt)
+ if not fmt then
+ self.fmt = '%Y-%m-%d %H:%M:%S'
+ self.outf = self.fmt
+ self.plain = true
+ return
+ end
+ local append = table.insert
+ local D,PLUS,OPENP,CLOSEP = '\001','\002','\003','\004'
+ local vars,used = {},{}
+ local patt,outf = {},{}
+ local i = 1
+ while i < #fmt do
+ local ch = fmt:sub(i,i)
+ local df = formats[ch]
+ if df then
+ if used[ch] then error("field appeared twice: "..ch,4) end
+ used[ch] = true
+ -- this field may be repeated
+ local _,inext = fmt:find(ch..'+',i+1)
+ local cnt = not _ and 1 or inext-i+1
+ if not df[2][cnt] then error("wrong number of fields: "..ch,4) end
+ -- single chars mean 'accept more than one digit'
+ local p = cnt==1 and (D..PLUS) or (D):rep(cnt)
+ append(patt,OPENP..p..CLOSEP)
+ append(vars,ch)
+ if ch == 'y' then
+ append(outf,cnt==2 and '%y' or '%Y')
+ else
+ append(outf,'%'..ch)
+ end
+ i = i + cnt
+ else
+ append(patt,ch)
+ append(outf,ch)
+ i = i + 1
+ end
+ end
+ -- escape any magic characters
+ fmt = utils.escape(table.concat(patt))
+ -- fmt = table.concat(patt):gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1')
+ -- replace markers with their magic equivalents
+ fmt = fmt:gsub(D,'%%d'):gsub(PLUS,'+'):gsub(OPENP,'('):gsub(CLOSEP,')')
+ self.fmt = fmt
+ self.outf = table.concat(outf)
+ self.vars = vars
+end
+
+local parse_date
+
+--- parse a string into a Date object.
+-- @string str a date string
+-- @return date object
+function Date.Format:parse(str)
+ assert_string(1,str)
+ if self.plain then
+ return parse_date(str,self.us)
+ end
+ local res = {str:match(self.fmt)}
+ if #res==0 then return nil, 'cannot parse '..str end
+ local tab = {}
+ for i,v in ipairs(self.vars) do
+ local name = formats[v][1] -- e.g. 'y' becomes 'year'
+ tab[name] = tonumber(res[i])
+ end
+ -- os.date() requires these fields; if not present, we assume
+ -- that the time set is for the current day.
+ if not (tab.year and tab.month and tab.day) then
+ local today = Date()
+ tab.year = tab.year or today:year()
+ tab.month = tab.month or today:month()
+ tab.day = tab.day or today:day()
+ end
+ local Y = tab.year
+ if Y < 100 then -- classic Y2K pivot
+ tab.year = Y + (Y < 35 and 2000 or 1999)
+ elseif not Y then
+ tab.year = 1970
+ end
+ return Date(tab)
+end
+
+--- convert a Date object into a string.
+-- @param d a date object, or a time value as returned by @{os.time}
+-- @return string
+function Date.Format:tostring(d)
+ local tm
+ local fmt = self.outf
+ if type(d) == 'number' then
+ tm = d
+ else
+ tm = d.time
+ if d.utc then
+ fmt = '!'..fmt
+ end
+ end
+ return os_date(fmt,tm)
+end
+
+--- force US order in dates like 9/11/2001
+function Date.Format:US_order(yesno)
+ self.us = yesno
+end
+
+--local months = {jan=1,feb=2,mar=3,apr=4,may=5,jun=6,jul=7,aug=8,sep=9,oct=10,nov=11,dec=12}
+local months
+local parse_date_unsafe
+local function create_months()
+ local ld, day1 = parse_date_unsafe '2000-12-31', {day=1}
+ months = {}
+ for i = 1,12 do
+ ld = ld:last_day()
+ ld:add(day1)
+ local mon = ld:month_name():lower()
+ months [mon] = i
+ end
+end
+
+--[[
+Allowed patterns:
+- [day] [monthname] [year] [time]
+- [day]/[month][/year] [time]
+
+]]
+
+local function looks_like_a_month(w)
+ return w:match '^%a+,*$' ~= nil
+end
+local is_number = stringx.isdigit
+local function tonum(s,l1,l2,kind)
+ kind = kind or ''
+ local n = tonumber(s)
+ if not n then error(("%snot a number: '%s'"):format(kind,s)) end
+ if n < l1 or n > l2 then
+ error(("%s out of range: %s is not between %d and %d"):format(kind,s,l1,l2))
+ end
+ return n
+end
+
+local function parse_iso_end(p,ns,sec)
+ -- may be fractional part of seconds
+ local _,nfrac,secfrac = p:find('^%.%d+',ns+1)
+ if secfrac then
+ sec = sec .. secfrac
+ p = p:sub(nfrac+1)
+ else
+ p = p:sub(ns+1)
+ end
+ -- ISO 8601 dates may end in Z (for UTC) or [+-][isotime]
+ -- (we're working with the date as lower case, hence 'z')
+ if p:match 'z$' then -- we're UTC!
+ return sec, {h=0,m=0}
+ end
+ p = p:gsub(':','') -- turn 00:30 to 0030
+ local _,_,sign,offs = p:find('^([%+%-])(%d+)')
+ if not sign then return sec, nil end -- not UTC
+
+ if #offs == 2 then offs = offs .. '00' end -- 01 to 0100
+ local tz = { h = tonumber(offs:sub(1,2)), m = tonumber(offs:sub(3,4)) }
+ if sign == '-' then tz.h = -tz.h; tz.m = -tz.m end
+ return sec, tz
+end
+
+function parse_date_unsafe (s,US)
+ s = s:gsub('T',' ') -- ISO 8601
+ local parts = stringx.split(s:lower())
+ local i,p = 1,parts[1]
+ local function nextp() i = i + 1; p = parts[i] end
+ local year,min,hour,sec,apm
+ local tz
+ local _,nxt,day, month = p:find '^(%d+)/(%d+)'
+ if day then
+ -- swop for US case
+ if US then
+ day, month = month, day
+ end
+ _,_,year = p:find('^/(%d+)',nxt+1)
+ nextp()
+ else -- ISO
+ year,month,day = p:match('^(%d+)%-(%d+)%-(%d+)')
+ if year then
+ nextp()
+ end
+ end
+ if p and not year and is_number(p) then -- has to be date
+ if #p < 4 then
+ day = p
+ nextp()
+ else -- unless it looks like a 24-hour time
+ year = true
+ end
+ end
+ if p and looks_like_a_month(p) then -- date followed by month
+ p = p:sub(1,3)
+ if not months then
+ create_months()
+ end
+ local mon = months[p]
+ if mon then
+ month = mon
+ else error("not a month: " .. p) end
+ nextp()
+ end
+ if p and not year and is_number(p) then
+ year = p
+ nextp()
+ end
+
+ if p then -- time is hh:mm[:ss], hhmm[ss] or H.M[am|pm]
+ _,nxt,hour,min = p:find '^(%d+):(%d+)'
+ local ns
+ if nxt then -- are there seconds?
+ _,ns,sec = p:find ('^:(%d+)',nxt+1)
+ --if ns then
+ sec,tz = parse_iso_end(p,ns or nxt,sec)
+ --end
+ else -- might be h.m
+ _,ns,hour,min = p:find '^(%d+)%.(%d+)'
+ if ns then
+ apm = p:match '[ap]m$'
+ else -- or hhmm[ss]
+ local hourmin
+ _,nxt,hourmin = p:find ('^(%d+)')
+ if nxt then
+ hour = hourmin:sub(1,2)
+ min = hourmin:sub(3,4)
+ sec = hourmin:sub(5,6)
+ if #sec == 0 then sec = nil end
+ sec,tz = parse_iso_end(p,nxt,sec)
+ end
+ end
+ end
+ end
+ local today
+ if year == true then year = nil end
+ if not (year and month and day) then
+ today = Date()
+ end
+ day = day and tonum(day,1,31,'day') or (month and 1 or today:day())
+ month = month and tonum(month,1,12,'month') or today:month()
+ year = year and tonumber(year) or today:year()
+ if year < 100 then -- two-digit year pivot around year < 2035
+ year = year + (year < 35 and 2000 or 1900)
+ end
+ hour = hour and tonum(hour,0,apm and 12 or 24,'hour') or 12
+ if apm == 'pm' then
+ hour = hour + 12
+ end
+ min = min and tonum(min,0,59) or 0
+ sec = sec and tonum(sec,0,60) or 0 --60 used to indicate leap second
+ local res = Date {year = year, month = month, day = day, hour = hour, min = min, sec = sec}
+ if tz then -- ISO 8601 UTC time
+ local corrected = false
+ if tz.h ~= 0 then res:add {hour = -tz.h}; corrected = true end
+ if tz.m ~= 0 then res:add {min = -tz.m}; corrected = true end
+ res.utc = true
+ -- we're in UTC, so let's go local...
+ if corrected then
+ res = res:toLocal()
+ end-- we're UTC!
+ end
+ return res
+end
+
+function parse_date (s)
+ local ok, d = pcall(parse_date_unsafe,s)
+ if not ok then -- error
+ d = d:gsub('.-:%d+: ','')
+ return nil, d
+ else
+ return d
+ end
+end
+
+return Date
+
diff --git a/macros/luatex/generic/penlight/pl/LICENSE-pl.md b/macros/luatex/generic/penlight/pl/LICENSE-pl.md
new file mode 100644
index 0000000000..111b2e109d
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/LICENSE-pl.md
@@ -0,0 +1,25 @@
+
+
+# "Penlight" Lua package license
+
+Copyright (C) 2009-2016 Steve Donovan, David Manura.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/macros/luatex/generic/penlight/pl/List.lua b/macros/luatex/generic/penlight/pl/List.lua
new file mode 100644
index 0000000000..b66c251eaf
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/List.lua
@@ -0,0 +1,566 @@
+--- Python-style list class.
+--
+-- **Please Note**: methods that change the list will return the list.
+-- This is to allow for method chaining, but please note that `ls = ls:sort()`
+-- does not mean that a new copy of the list is made. In-place (mutable) methods
+-- are marked as returning 'the list' in this documentation.
+--
+-- See the Guide for further @{02-arrays.md.Python_style_Lists|discussion}
+--
+-- See <a href="http://www.python.org/doc/current/tut/tut.html">http://www.python.org/doc/current/tut/tut.html</a>, section 5.1
+--
+-- **Note**: The comments before some of the functions are from the Python docs
+-- and contain Python code.
+--
+-- Written for Lua version Nick Trout 4.0; Redone for Lua 5.1, Steve Donovan.
+--
+-- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`
+-- @classmod pl.List
+-- @pragma nostrip
+
+local tinsert,tremove,concat,tsort = table.insert,table.remove,table.concat,table.sort
+local setmetatable, getmetatable,type,tostring,string = setmetatable,getmetatable,type,tostring,string
+local tablex = require 'pl.tablex'
+local filter,imap,imap2,reduce,transform,tremovevalues = tablex.filter,tablex.imap,tablex.imap2,tablex.reduce,tablex.transform,tablex.removevalues
+local tsub = tablex.sub
+local utils = require 'pl.utils'
+local class = require 'pl.class'
+
+local array_tostring,split,assert_arg,function_arg = utils.array_tostring,utils.split,utils.assert_arg,utils.function_arg
+local normalize_slice = tablex._normalize_slice
+
+-- metatable for our list and map objects has already been defined..
+local Multimap = utils.stdmt.MultiMap
+local List = utils.stdmt.List
+
+local iter
+
+class(nil,nil,List)
+
+-- we want the result to be _covariant_, i.e. t must have type of obj if possible
+local function makelist (t,obj)
+ local klass = List
+ if obj then
+ klass = getmetatable(obj)
+ end
+ return setmetatable(t,klass)
+end
+
+local function simple_table(t)
+ return type(t) == 'table' and not getmetatable(t) and #t > 0
+end
+
+function List._create (src)
+ if simple_table(src) then return src end
+end
+
+function List:_init (src)
+ if self == src then return end -- existing table used as self!
+ if src then
+ for v in iter(src) do
+ tinsert(self,v)
+ end
+ end
+end
+
+--- Create a new list. Can optionally pass a table;
+-- passing another instance of List will cause a copy to be created;
+-- this will return a plain table with an appropriate metatable.
+-- we pass anything which isn't a simple table to iterate() to work out
+-- an appropriate iterator
+-- @see List.iterate
+-- @param[opt] t An optional list-like table
+-- @return a new List
+-- @usage ls = List(); ls = List {1,2,3,4}
+-- @function List.new
+
+List.new = List
+
+--- Make a copy of an existing list.
+-- The difference from a plain 'copy constructor' is that this returns
+-- the actual List subtype.
+function List:clone()
+ local ls = makelist({},self)
+ ls:extend(self)
+ return ls
+end
+
+--- Add an item to the end of the list.
+-- @param i An item
+-- @return the list
+function List:append(i)
+ tinsert(self,i)
+ return self
+end
+
+List.push = tinsert
+
+--- Extend the list by appending all the items in the given list.
+-- equivalent to 'a[len(a):] = L'.
+-- @tparam List L Another List
+-- @return the list
+function List:extend(L)
+ assert_arg(1,L,'table')
+ for i = 1,#L do tinsert(self,L[i]) end
+ return self
+end
+
+--- Insert an item at a given position. i is the index of the
+-- element before which to insert.
+-- @int i index of element before whichh to insert
+-- @param x A data item
+-- @return the list
+function List:insert(i, x)
+ assert_arg(1,i,'number')
+ tinsert(self,i,x)
+ return self
+end
+
+--- Insert an item at the begining of the list.
+-- @param x a data item
+-- @return the list
+function List:put (x)
+ return self:insert(1,x)
+end
+
+--- Remove an element given its index.
+-- (equivalent of Python's del s[i])
+-- @int i the index
+-- @return the list
+function List:remove (i)
+ assert_arg(1,i,'number')
+ tremove(self,i)
+ return self
+end
+
+--- Remove the first item from the list whose value is given.
+-- (This is called 'remove' in Python; renamed to avoid confusion
+-- with table.remove)
+-- Return nil if there is no such item.
+-- @param x A data value
+-- @return the list
+function List:remove_value(x)
+ for i=1,#self do
+ if self[i]==x then tremove(self,i) return self end
+ end
+ return self
+ end
+
+--- Remove the item at the given position in the list, and return it.
+-- If no index is specified, a:pop() returns the last item in the list.
+-- The item is also removed from the list.
+-- @int[opt] i An index
+-- @return the item
+function List:pop(i)
+ if not i then i = #self end
+ assert_arg(1,i,'number')
+ return tremove(self,i)
+end
+
+List.get = List.pop
+
+--- Return the index in the list of the first item whose value is given.
+-- Return nil if there is no such item.
+-- @function List:index
+-- @param x A data value
+-- @int[opt=1] idx where to start search
+-- @return the index, or nil if not found.
+
+local tfind = tablex.find
+List.index = tfind
+
+--- Does this list contain the value?
+-- @param x A data value
+-- @return true or false
+function List:contains(x)
+ return tfind(self,x) and true or false
+end
+
+--- Return the number of times value appears in the list.
+-- @param x A data value
+-- @return number of times x appears
+function List:count(x)
+ local cnt=0
+ for i=1,#self do
+ if self[i]==x then cnt=cnt+1 end
+ end
+ return cnt
+end
+
+--- Sort the items of the list, in place.
+-- @func[opt='<'] cmp an optional comparison function
+-- @return the list
+function List:sort(cmp)
+ if cmp then cmp = function_arg(1,cmp) end
+ tsort(self,cmp)
+ return self
+end
+
+--- Return a sorted copy of this list.
+-- @func[opt='<'] cmp an optional comparison function
+-- @return a new list
+function List:sorted(cmp)
+ return List(self):sort(cmp)
+end
+
+--- Reverse the elements of the list, in place.
+-- @return the list
+function List:reverse()
+ local t = self
+ local n = #t
+ for i = 1,n/2 do
+ t[i],t[n] = t[n],t[i]
+ n = n - 1
+ end
+ return self
+end
+
+--- Return the minimum and the maximum value of the list.
+-- @return minimum value
+-- @return maximum value
+function List:minmax()
+ local vmin,vmax = 1e70,-1e70
+ for i = 1,#self do
+ local v = self[i]
+ if v < vmin then vmin = v end
+ if v > vmax then vmax = v end
+ end
+ return vmin,vmax
+end
+
+--- Emulate list slicing. like 'list[first:last]' in Python.
+-- If first or last are negative then they are relative to the end of the list
+-- eg. slice(-2) gives last 2 entries in a list, and
+-- slice(-4,-2) gives from -4th to -2nd
+-- @param first An index
+-- @param last An index
+-- @return a new List
+function List:slice(first,last)
+ return tsub(self,first,last)
+end
+
+--- Empty the list.
+-- @return the list
+function List:clear()
+ for i=1,#self do tremove(self) end
+ return self
+end
+
+local eps = 1.0e-10
+
+--- Emulate Python's range(x) function.
+-- Include it in List table for tidiness
+-- @int start A number
+-- @int[opt] finish A number greater than start; if absent,
+-- then start is 1 and finish is start
+-- @int[opt=1] incr an increment (may be less than 1)
+-- @return a List from start .. finish
+-- @usage List.range(0,3) == List{0,1,2,3}
+-- @usage List.range(4) = List{1,2,3,4}
+-- @usage List.range(5,1,-1) == List{5,4,3,2,1}
+function List.range(start,finish,incr)
+ if not finish then
+ finish = start
+ start = 1
+ end
+ if incr then
+ assert_arg(3,incr,'number')
+ if math.ceil(incr) ~= incr then finish = finish + eps end
+ else
+ incr = 1
+ end
+ assert_arg(1,start,'number')
+ assert_arg(2,finish,'number')
+ local t = List()
+ for i=start,finish,incr do tinsert(t,i) end
+ return t
+end
+
+--- list:len() is the same as #list.
+function List:len()
+ return #self
+end
+
+-- Extended operations --
+
+--- Remove a subrange of elements.
+-- equivalent to 'del s[i1:i2]' in Python.
+-- @int i1 start of range
+-- @int i2 end of range
+-- @return the list
+function List:chop(i1,i2)
+ return tremovevalues(self,i1,i2)
+end
+
+--- Insert a sublist into a list
+-- equivalent to 's[idx:idx] = list' in Python
+-- @int idx index
+-- @tparam List list list to insert
+-- @return the list
+-- @usage l = List{10,20}; l:splice(2,{21,22}); assert(l == List{10,21,22,20})
+function List:splice(idx,list)
+ assert_arg(1,idx,'number')
+ idx = idx - 1
+ local i = 1
+ for v in iter(list) do
+ tinsert(self,i+idx,v)
+ i = i + 1
+ end
+ return self
+end
+
+--- General slice assignment s[i1:i2] = seq.
+-- @int i1 start index
+-- @int i2 end index
+-- @tparam List seq a list
+-- @return the list
+function List:slice_assign(i1,i2,seq)
+ assert_arg(1,i1,'number')
+ assert_arg(1,i2,'number')
+ i1,i2 = normalize_slice(self,i1,i2)
+ if i2 >= i1 then self:chop(i1,i2) end
+ self:splice(i1,seq)
+ return self
+end
+
+--- Concatenation operator.
+-- @within metamethods
+-- @tparam List L another List
+-- @return a new list consisting of the list with the elements of the new list appended
+function List:__concat(L)
+ assert_arg(1,L,'table')
+ local ls = self:clone()
+ ls:extend(L)
+ return ls
+end
+
+--- Equality operator ==. True iff all elements of two lists are equal.
+-- @within metamethods
+-- @tparam List L another List
+-- @return true or false
+function List:__eq(L)
+ if #self ~= #L then return false end
+ for i = 1,#self do
+ if self[i] ~= L[i] then return false end
+ end
+ return true
+end
+
+--- Join the elements of a list using a delimiter.
+-- This method uses tostring on all elements.
+-- @string[opt=''] delim a delimiter string, can be empty.
+-- @return a string
+function List:join (delim)
+ delim = delim or ''
+ assert_arg(1,delim,'string')
+ return concat(array_tostring(self),delim)
+end
+
+--- Join a list of strings. <br>
+-- Uses `table.concat` directly.
+-- @function List:concat
+-- @string[opt=''] delim a delimiter
+-- @return a string
+List.concat = concat
+
+local function tostring_q(val)
+ local s = tostring(val)
+ if type(val) == 'string' then
+ s = '"'..s..'"'
+ end
+ return s
+end
+
+--- How our list should be rendered as a string. Uses join().
+-- @within metamethods
+-- @see List:join
+function List:__tostring()
+ return '{'..self:join(',',tostring_q)..'}'
+end
+
+--- Call the function on each element of the list.
+-- @func fun a function or callable object
+-- @param ... optional values to pass to function
+function List:foreach (fun,...)
+ fun = function_arg(1,fun)
+ for i = 1,#self do
+ fun(self[i],...)
+ end
+end
+
+local function lookup_fun (obj,name)
+ local f = obj[name]
+ if not f then error(type(obj).." does not have method "..name,3) end
+ return f
+end
+
+--- Call the named method on each element of the list.
+-- @string name the method name
+-- @param ... optional values to pass to function
+function List:foreachm (name,...)
+ for i = 1,#self do
+ local obj = self[i]
+ local f = lookup_fun(obj,name)
+ f(obj,...)
+ end
+end
+
+--- Create a list of all elements which match a function.
+-- @func fun a boolean function
+-- @param[opt] arg optional argument to be passed as second argument of the predicate
+-- @return a new filtered list.
+function List:filter (fun,arg)
+ return makelist(filter(self,fun,arg),self)
+end
+
+--- Split a string using a delimiter.
+-- @string s the string
+-- @string[opt] delim the delimiter (default spaces)
+-- @return a List of strings
+-- @see pl.utils.split
+function List.split (s,delim)
+ assert_arg(1,s,'string')
+ return makelist(split(s,delim))
+end
+
+--- Apply a function to all elements.
+-- Any extra arguments will be passed to the function.
+-- @func fun a function of at least one argument
+-- @param ... arbitrary extra arguments.
+-- @return a new list: {f(x) for x in self}
+-- @usage List{'one','two'}:map(string.upper) == {'ONE','TWO'}
+-- @see pl.tablex.imap
+function List:map (fun,...)
+ return makelist(imap(fun,self,...),self)
+end
+
+--- Apply a function to all elements, in-place.
+-- Any extra arguments are passed to the function.
+-- @func fun A function that takes at least one argument
+-- @param ... arbitrary extra arguments.
+-- @return the list.
+function List:transform (fun,...)
+ transform(fun,self,...)
+ return self
+end
+
+--- Apply a function to elements of two lists.
+-- Any extra arguments will be passed to the function
+-- @func fun a function of at least two arguments
+-- @tparam List ls another list
+-- @param ... arbitrary extra arguments.
+-- @return a new list: {f(x,y) for x in self, for x in arg1}
+-- @see pl.tablex.imap2
+function List:map2 (fun,ls,...)
+ return makelist(imap2(fun,self,ls,...),self)
+end
+
+--- apply a named method to all elements.
+-- Any extra arguments will be passed to the method.
+-- @string name name of method
+-- @param ... extra arguments
+-- @return a new list of the results
+-- @see pl.seq.mapmethod
+function List:mapm (name,...)
+ local res = {}
+ for i = 1,#self do
+ local val = self[i]
+ local fn = lookup_fun(val,name)
+ res[i] = fn(val,...)
+ end
+ return makelist(res,self)
+end
+
+local function composite_call (method,f)
+ return function(self,...)
+ return self[method](self,f,...)
+ end
+end
+
+function List.default_map_with(T)
+ return function(self,name)
+ local m
+ if T then
+ local f = lookup_fun(T,name)
+ m = composite_call('map',f)
+ else
+ m = composite_call('mapn',name)
+ end
+ getmetatable(self)[name] = m -- and cache..
+ return m
+ end
+end
+
+List.default_map = List.default_map_with
+
+--- 'reduce' a list using a binary function.
+-- @func fun a function of two arguments
+-- @return result of the function
+-- @see pl.tablex.reduce
+function List:reduce (fun)
+ return reduce(fun,self)
+end
+
+--- Partition a list using a classifier function.
+-- The function may return nil, but this will be converted to the string key '<nil>'.
+-- @func fun a function of at least one argument
+-- @param ... will also be passed to the function
+-- @treturn MultiMap a table where the keys are the returned values, and the values are Lists
+-- of values where the function returned that key.
+-- @see pl.MultiMap
+function List:partition (fun,...)
+ fun = function_arg(1,fun)
+ local res = {}
+ for i = 1,#self do
+ local val = self[i]
+ local klass = fun(val,...)
+ if klass == nil then klass = '<nil>' end
+ if not res[klass] then res[klass] = List() end
+ res[klass]:append(val)
+ end
+ return setmetatable(res,Multimap)
+end
+
+--- return an iterator over all values.
+function List:iter ()
+ return iter(self)
+end
+
+--- Create an iterator over a seqence.
+-- This captures the Python concept of 'sequence'.
+-- For tables, iterates over all values with integer indices.
+-- @param seq a sequence; a string (over characters), a table, a file object (over lines) or an iterator function
+-- @usage for x in iterate {1,10,22,55} do io.write(x,',') end ==> 1,10,22,55
+-- @usage for ch in iterate 'help' do do io.write(ch,' ') end ==> h e l p
+function List.iterate(seq)
+ if type(seq) == 'string' then
+ local idx = 0
+ local n = #seq
+ local sub = string.sub
+ return function ()
+ idx = idx + 1
+ if idx > n then return nil
+ else
+ return sub(seq,idx,idx)
+ end
+ end
+ elseif type(seq) == 'table' then
+ local idx = 0
+ local n = #seq
+ return function()
+ idx = idx + 1
+ if idx > n then return nil
+ else
+ return seq[idx]
+ end
+ end
+ elseif type(seq) == 'function' then
+ return seq
+ elseif type(seq) == 'userdata' and io.type(seq) == 'file' then
+ return seq:lines()
+ end
+end
+iter = List.iterate
+
+return List
+
diff --git a/macros/luatex/generic/penlight/pl/Map.lua b/macros/luatex/generic/penlight/pl/Map.lua
new file mode 100644
index 0000000000..572b22ad0b
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/Map.lua
@@ -0,0 +1,120 @@
+--- A Map class.
+--
+-- > Map = require 'pl.Map'
+-- > m = Map{one=1,two=2}
+-- > m:update {three=3,four=4,two=20}
+-- > = m == M{one=1,two=20,three=3,four=4}
+-- true
+--
+-- Dependencies: `pl.utils`, `pl.class`, `pl.tablex`, `pl.pretty`
+-- @classmod pl.Map
+
+local tablex = require 'pl.tablex'
+local utils = require 'pl.utils'
+local stdmt = utils.stdmt
+local deepcompare = tablex.deepcompare
+
+local pretty_write = require 'pl.pretty' . write
+local Map = stdmt.Map
+local Set = stdmt.Set
+
+local class = require 'pl.class'
+
+-- the Map class ---------------------
+class(nil,nil,Map)
+
+function Map:_init (t)
+ local mt = getmetatable(t)
+ if mt == Set or mt == Map then
+ self:update(t)
+ else
+ return t -- otherwise assumed to be a map-like table
+ end
+end
+
+
+local function makelist(t)
+ return setmetatable(t, require('pl.List'))
+end
+
+--- list of keys.
+Map.keys = tablex.keys
+
+--- list of values.
+Map.values = tablex.values
+
+--- return an iterator over all key-value pairs.
+function Map:iter ()
+ return pairs(self)
+end
+
+--- return a List of all key-value pairs, sorted by the keys.
+function Map:items()
+ local ls = makelist(tablex.pairmap (function (k,v) return makelist {k,v} end, self))
+ ls:sort(function(t1,t2) return t1[1] < t2[1] end)
+ return ls
+end
+
+--- set a value in the map if it doesn't exist yet.
+-- @param key the key
+-- @param default value to set
+-- @return the value stored in the map (existing value, or the new value)
+function Map:setdefault(key, default)
+ local val = self[key]
+ if val ~= nil then
+ return val
+ end
+ self:set(key,default)
+ return default
+end
+
+--- size of map.
+-- note: this is a relatively expensive operation!
+-- @class function
+-- @name Map:len
+Map.len = tablex.size
+
+--- put a value into the map.
+-- This will remove the key if the value is `nil`
+-- @param key the key
+-- @param val the value
+function Map:set (key,val)
+ self[key] = val
+end
+
+--- get a value from the map.
+-- @param key the key
+-- @return the value, or nil if not found.
+function Map:get (key)
+ return rawget(self,key)
+end
+
+local index_by = tablex.index_by
+
+--- get a list of values indexed by a list of keys.
+-- @param keys a list-like table of keys
+-- @return a new list
+function Map:getvalues (keys)
+ return makelist(index_by(self,keys))
+end
+
+--- update the map using key/value pairs from another table.
+-- @tab table
+-- @function Map:update
+Map.update = tablex.update
+
+--- equality between maps.
+-- @within metamethods
+-- @tparam Map m another map.
+function Map:__eq (m)
+ -- note we explicitly ask deepcompare _not_ to use __eq!
+ return deepcompare(self,m,true)
+end
+
+--- string representation of a map.
+-- @within metamethods
+function Map:__tostring ()
+ return pretty_write(self,'')
+end
+
+return Map
diff --git a/macros/luatex/generic/penlight/pl/MultiMap.lua b/macros/luatex/generic/penlight/pl/MultiMap.lua
new file mode 100644
index 0000000000..0abe84de02
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/MultiMap.lua
@@ -0,0 +1,54 @@
+--- MultiMap, a Map which has multiple values per key.
+--
+-- Dependencies: `pl.utils`, `pl.class`, `pl.List`, `pl.Map`
+-- @classmod pl.MultiMap
+
+local utils = require 'pl.utils'
+local class = require 'pl.class'
+local List = require 'pl.List'
+local Map = require 'pl.Map'
+
+-- MultiMap is a standard MT
+local MultiMap = utils.stdmt.MultiMap
+
+class(Map,nil,MultiMap)
+MultiMap._name = 'MultiMap'
+
+function MultiMap:_init (t)
+ if not t then return end
+ self:update(t)
+end
+
+--- update a MultiMap using a table.
+-- @param t either a Multimap or a map-like table.
+-- @return the map
+function MultiMap:update (t)
+ utils.assert_arg(1,t,'table')
+ if Map:class_of(t) then
+ for k,v in pairs(t) do
+ self[k] = List()
+ self[k]:append(v)
+ end
+ else
+ for k,v in pairs(t) do
+ self[k] = List(v)
+ end
+ end
+end
+
+--- add a new value to a key. Setting a nil value removes the key.
+-- @param key the key
+-- @param val the value
+-- @return the map
+function MultiMap:set (key,val)
+ if val == nil then
+ self[key] = nil
+ else
+ if not self[key] then
+ self[key] = List()
+ end
+ self[key]:append(val)
+ end
+end
+
+return MultiMap
diff --git a/macros/luatex/generic/penlight/pl/OrderedMap.lua b/macros/luatex/generic/penlight/pl/OrderedMap.lua
new file mode 100644
index 0000000000..379c44fde0
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/OrderedMap.lua
@@ -0,0 +1,167 @@
+--- OrderedMap, a map which preserves ordering.
+--
+-- Derived from `pl.Map`.
+--
+-- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.List`, `pl.Map`
+-- @classmod pl.OrderedMap
+
+local tablex = require 'pl.tablex'
+local utils = require 'pl.utils'
+local List = require 'pl.List'
+local index_by,tsort,concat = tablex.index_by,table.sort,table.concat
+
+local class = require 'pl.class'
+local Map = require 'pl.Map'
+
+local OrderedMap = class(Map)
+OrderedMap._name = 'OrderedMap'
+
+local rawset = rawset
+
+--- construct an OrderedMap.
+-- Will throw an error if the argument is bad.
+-- @param t optional initialization table, same as for @{OrderedMap:update}
+function OrderedMap:_init (t)
+ rawset(self,'_keys',List())
+ if t then
+ local map,err = self:update(t)
+ if not map then error(err,2) end
+ end
+end
+
+local assert_arg,raise = utils.assert_arg,utils.raise
+
+--- update an OrderedMap using a table.
+-- If the table is itself an OrderedMap, then its entries will be appended.
+-- if it s a table of the form `{{key1=val1},{key2=val2},...}` these will be appended.
+--
+-- Otherwise, it is assumed to be a map-like table, and order of extra entries is arbitrary.
+-- @tab t a table.
+-- @return the map, or nil in case of error
+-- @return the error message
+function OrderedMap:update (t)
+ assert_arg(1,t,'table')
+ if OrderedMap:class_of(t) then
+ for k,v in t:iter() do
+ self:set(k,v)
+ end
+ elseif #t > 0 then -- an array must contain {key=val} tables
+ if type(t[1]) == 'table' then
+ for _,pair in ipairs(t) do
+ local key,value = next(pair)
+ if not key then return raise 'empty pair initialization table' end
+ self:set(key,value)
+ end
+ else
+ return raise 'cannot use an array to initialize an OrderedMap'
+ end
+ else
+ for k,v in pairs(t) do
+ self:set(k,v)
+ end
+ end
+ return self
+end
+
+--- set the key's value. This key will be appended at the end of the map.
+--
+-- If the value is nil, then the key is removed.
+-- @param key the key
+-- @param val the value
+-- @return the map
+function OrderedMap:set (key,val)
+ if rawget(self, key) == nil and val ~= nil then -- new key
+ self._keys:append(key) -- we keep in order
+ rawset(self,key,val) -- don't want to provoke __newindex!
+ else -- existing key-value pair
+ if val == nil then
+ self._keys:remove_value(key)
+ rawset(self,key,nil)
+ else
+ self[key] = val
+ end
+ end
+ return self
+end
+
+OrderedMap.__newindex = OrderedMap.set
+
+--- insert a key/value pair before a given position.
+-- Note: if the map already contains the key, then this effectively
+-- moves the item to the new position by first removing at the old position.
+-- Has no effect if the key does not exist and val is nil
+-- @int pos a position starting at 1
+-- @param key the key
+-- @param val the value; if nil use the old value
+function OrderedMap:insert (pos,key,val)
+ local oldval = self[key]
+ val = val or oldval
+ if oldval then
+ self._keys:remove_value(key)
+ end
+ if val then
+ self._keys:insert(pos,key)
+ rawset(self,key,val)
+ end
+ return self
+end
+
+--- return the keys in order.
+-- (Not a copy!)
+-- @return List
+function OrderedMap:keys ()
+ return self._keys
+end
+
+--- return the values in order.
+-- this is relatively expensive.
+-- @return List
+function OrderedMap:values ()
+ return List(index_by(self,self._keys))
+end
+
+--- sort the keys.
+-- @func cmp a comparison function as for @{table.sort}
+-- @return the map
+function OrderedMap:sort (cmp)
+ tsort(self._keys,cmp)
+ return self
+end
+
+--- iterate over key-value pairs in order.
+function OrderedMap:iter ()
+ local i = 0
+ local keys = self._keys
+ local idx
+ return function()
+ i = i + 1
+ if i > #keys then return nil end
+ idx = keys[i]
+ return idx,self[idx]
+ end
+end
+
+--- iterate over an ordered map (5.2).
+-- @within metamethods
+-- @function OrderedMap:__pairs
+OrderedMap.__pairs = OrderedMap.iter
+
+--- string representation of an ordered map.
+-- @within metamethods
+function OrderedMap:__tostring ()
+ local res = {}
+ for i,v in ipairs(self._keys) do
+ local val = self[v]
+ local vs = tostring(val)
+ if type(val) ~= 'number' then
+ vs = '"'..vs..'"'
+ end
+ res[i] = tostring(v)..'='..vs
+ end
+ return '{'..concat(res,',')..'}'
+end
+
+return OrderedMap
+
+
+
diff --git a/macros/luatex/generic/penlight/pl/README-pl.md b/macros/luatex/generic/penlight/pl/README-pl.md
new file mode 100644
index 0000000000..91699d03ce
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/README-pl.md
@@ -0,0 +1,127 @@
+# Penlight Lua Libraries
+
+[![Travis build status](https://img.shields.io/travis/lunarmodules/Penlight/master?logo=Travis)](https://travis-ci.org/lunarmodules/Penlight)
+[![AppVeyor build status](https://img.shields.io/appveyor/build/Tieske/penlight-ta1gi/master?logo=appveyor)](https://ci.appveyor.com/project/Tieske/penlight-ta1gi/branch/master)
+[![Coveralls code coverage](https://img.shields.io/coveralls/github/lunarmodules/Penlight?logo=coveralls)](https://coveralls.io/github/lunarmodules/Penlight)
+[![Luacheck](https://github.com/lunarmodules/Penlight/workflows/Luacheck/badge.svg)](https://github.com/lunarmodules/Penlight/actions)
+[![SemVer](https://img.shields.io/github/v/tag/lunarmodules/Penlight?color=brightgreen&label=SemVer&logo=semver&sort=semver)](CHANGELOG.md)
+
+## Why a new set of libraries?
+
+Penlight brings together a set of generally useful pure Lua modules,
+focusing on input data handling (such as reading configuration files),
+functional programming (such as map, reduce, placeholder expressions, etc),
+and OS path management. Much of the functionality is inspired by the
+Python standard libraries.
+
+## Module Overview
+
+### Paths, Files and Directories
+
+ * `path`: queries like `isdir`,`isfile`,`exists`, splitting paths like `dirname` and `basename`
+ * `dir`: listing files in directories (`getfiles`,`getallfiles`) and creating/removing directory paths
+ * `file`: `copy`,`move`; read/write contents with `read` and `write`
+
+### Application Support
+
+ * `app`: `require_here` to rebase `require` to work with main script path; simple argument parsing `parse_args`
+ * `lapp`: sophisticated usage-text-driven argument parsing for applications
+ * `config`: flexibly read Unix config files and Windows INI files
+ * `strict`: check for undefined global variables - can use `strict.module` for modules
+ * `utils`,`compat`: Penlight support for unified Lua 5.1/5.2 codebases
+ * `types`: predicates like `is_callable` and `is_integer`; extended `type` function.
+
+### Extra String Operations
+
+ * `utils`: can split a string with a delimiter using `utils.split`
+ * `stringx`: extended string functions covering the Python `string` type
+ * `stringio`: open strings for reading, and creating strings using standard Lua IO methods
+ * `lexer`: lexical scanner for splitting text into tokens; special cases for Lua and C
+ * `text`: indenting and dedenting text, wrapping paragraphs; optionally make `%` work as in Python
+ * `template`: small but powerful template expansion engine
+ * `sip`: Simple Input Patterns - higher-level string patterns for parsing text
+
+### Extra Table Operations
+
+ * `tablex`: copying, comparing and mapping over
+ * `pretty`: pretty-printing Lua tables, and various safe ways to load Lua as data
+ * `List`: implementation of Python 'list' type - slices, concatenation and partitioning
+ * `Map`, `Set`, `OrderedMap`: classes for specialized kinds of tables
+ * `data`: reading tabular data into 2D arrays and efficient queries
+ * `array2d`: operations on 2D arrays
+ * `permute`: generate permutations
+
+### Iterators, OOP and Functional
+
+ * `seq`: working with iterator pipelines; collecting iterators as tables
+ * `class`: a simple reusable class framework
+ * `func`: symbolic manipulation of expressions and lambda expressions
+ * `utils`: `utils.string_lambda` converts short strings like '|x| x^2' into functions
+ * `comprehension`: list comprehensions: `C'x for x=1,4'()=={1,2,3,4}`
+
+## Versioning
+
+Penlight is strictly versioned according to [Semantic Versioning](https://semver.org/).
+
+In scope of the version:
+ * functionality provided by Penlight modules/classes
+ * based on stock Lua PuC-Rio or LuaJIT
+
+Not in scope of the version:
+ * Documentation
+ * Error messages (textual changes)
+ * Deprecation warnings (by default to `stderr`)
+
+### Deprecating functionality
+
+Any version may deprecate functionality. So new deprecation notices may appear
+in major, minor, and patch releases. Final removal of functionality (assuming it
+is a breaking change) will only be done in a major version.
+
+It is strongly suggested to use the deprecation warning mechanism to test usage
+of deprecated functionalities when upgrading. This is done by enabling the
+warning system (in Lua 5.4, or the Penlight compatibility function for earlier
+versions):
+
+```lua
+require "pl.compat"
+warn "@on"
+```
+
+See `pl.utils.raise_deprecation` for more info.
+
+## License
+
+Penlight is distributed under the [MIT license](LICENSE.md).
+
+## Installation
+
+Using [LuaRocks](https://luarocks.org): simply run `luarocks install penlight`.
+
+Manually: copy `lua/pl` directory into your Lua module path. It's typically
+`/usr/local/share/lua/5.x` on a Linux system and `C:\Program Files\Lua\5.x\lua`
+for Lua for Windows.
+
+## Dependencies
+
+The file and directory functions depend on [LuaFileSystem](https://keplerproject.github.io/luafilesystem/),
+which is installed automatically if you are using LuaRocks. Additionally, if you want `dir.copyfile` to work
+elegantly on Windows, then you need [Alien](http://mascarenhas.github.io/alien/). Both libraries are present
+in Lua for Windows.
+
+## Building the Documentation
+
+Requires [ldoc](https://github.com/stevedonovan/LDoc), which is available
+through LuaRocks. Then it's a simple matter of running `ldoc .` from the repo.
+
+## Contributing
+
+Contributions are most welcome, please check the [contribution guidelines](CONTRIBUTING.md).
+
+## Running tests
+
+Execute `lua run.lua tests` to run the tests. Execute `lua run.lua examples` to run examples.
+
+## History
+
+For a complete history of the development of Penlight, please check the [changelog](CHANGELOG.md).
diff --git a/macros/luatex/generic/penlight/pl/Set.lua b/macros/luatex/generic/penlight/pl/Set.lua
new file mode 100644
index 0000000000..ce428f0a0d
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/Set.lua
@@ -0,0 +1,222 @@
+--- A Set class.
+--
+-- > Set = require 'pl.Set'
+-- > = Set{'one','two'} == Set{'two','one'}
+-- true
+-- > fruit = Set{'apple','banana','orange'}
+-- > = fruit['banana']
+-- true
+-- > = fruit['hazelnut']
+-- nil
+-- > colours = Set{'red','orange','green','blue'}
+-- > = fruit,colours
+-- [apple,orange,banana] [blue,green,orange,red]
+-- > = fruit+colours
+-- [blue,green,apple,red,orange,banana]
+-- [orange]
+-- > more_fruits = fruit + 'apricot'
+-- > = fruit*colours
+-- > = more_fruits, fruit
+-- [banana,apricot,apple,orange] [banana,apple,orange]
+--
+-- Dependencies: `pl.utils`, `pl.tablex`, `pl.class`, `pl.Map`, (`pl.List` if __tostring is used)
+-- @classmod pl.Set
+
+local tablex = require 'pl.tablex'
+local utils = require 'pl.utils'
+local array_tostring, concat = utils.array_tostring, table.concat
+local merge,difference = tablex.merge,tablex.difference
+local Map = require 'pl.Map'
+local class = require 'pl.class'
+local stdmt = utils.stdmt
+local Set = stdmt.Set
+
+-- the Set class --------------------
+class(Map,nil,Set)
+
+-- note: Set has _no_ methods!
+Set.__index = nil
+
+local function makeset (t)
+ return setmetatable(t,Set)
+end
+
+--- create a set. <br>
+-- @param t may be a Set, Map or list-like table.
+-- @class function
+-- @name Set
+function Set:_init (t)
+ t = t or {}
+ local mt = getmetatable(t)
+ if mt == Set or mt == Map then
+ for k in pairs(t) do self[k] = true end
+ else
+ for _,v in ipairs(t) do self[v] = true end
+ end
+end
+
+--- string representation of a set.
+-- @within metamethods
+function Set:__tostring ()
+ return '['..concat(array_tostring(Set.values(self)),',')..']'
+end
+
+--- get a list of the values in a set.
+-- @param self a Set
+-- @function Set.values
+-- @return a list
+Set.values = Map.keys
+
+--- map a function over the values of a set.
+-- @param self a Set
+-- @param fn a function
+-- @param ... extra arguments to pass to the function.
+-- @return a new set
+function Set.map (self,fn,...)
+ fn = utils.function_arg(1,fn)
+ local res = {}
+ for k in pairs(self) do
+ res[fn(k,...)] = true
+ end
+ return makeset(res)
+end
+
+--- union of two sets (also +).
+-- @param self a Set
+-- @param set another set
+-- @return a new set
+function Set.union (self,set)
+ return merge(self,set,true)
+end
+
+--- modifies '+' operator to allow addition of non-Set elements
+--- Preserves +/- semantics - does not modify first argument.
+local function setadd(self,other)
+ local mt = getmetatable(other)
+ if mt == Set or mt == Map then
+ return Set.union(self,other)
+ else
+ local new = Set(self)
+ new[other] = true
+ return new
+ end
+end
+
+--- union of sets.
+-- @within metamethods
+-- @function Set.__add
+
+Set.__add = setadd
+
+--- intersection of two sets (also *).
+-- @param self a Set
+-- @param set another set
+-- @return a new set
+-- @usage
+-- > s = Set{10,20,30}
+-- > t = Set{20,30,40}
+-- > = t
+-- [20,30,40]
+-- > = Set.intersection(s,t)
+-- [30,20]
+-- > = s*t
+-- [30,20]
+
+function Set.intersection (self,set)
+ return merge(self,set,false)
+end
+
+--- intersection of sets.
+-- @within metamethods
+-- @function Set.__mul
+Set.__mul = Set.intersection
+
+--- new set with elements in the set that are not in the other (also -).
+-- @param self a Set
+-- @param set another set
+-- @return a new set
+function Set.difference (self,set)
+ return difference(self,set,false)
+end
+
+--- modifies "-" operator to remove non-Set values from set.
+--- Preserves +/- semantics - does not modify first argument.
+local function setminus (self,other)
+ local mt = getmetatable(other)
+ if mt == Set or mt == Map then
+ return Set.difference(self,other)
+ else
+ local new = Set(self)
+ new[other] = nil
+ return new
+ end
+end
+
+--- difference of sets.
+-- @within metamethods
+-- @function Set.__sub
+Set.__sub = setminus
+
+-- a new set with elements in _either_ the set _or_ other but not both (also ^).
+-- @param self a Set
+-- @param set another set
+-- @return a new set
+function Set.symmetric_difference (self,set)
+ return difference(self,set,true)
+end
+
+--- symmetric difference of sets.
+-- @within metamethods
+-- @function Set.__pow
+Set.__pow = Set.symmetric_difference
+
+--- is the first set a subset of the second (also <)?.
+-- @param self a Set
+-- @param set another set
+-- @return true or false
+function Set.issubset (self,set)
+ for k in pairs(self) do
+ if not set[k] then return false end
+ end
+ return true
+end
+
+--- first set subset of second?
+-- @within metamethods
+-- @function Set.__lt
+Set.__lt = Set.issubset
+
+--- is the set empty?.
+-- @param self a Set
+-- @return true or false
+function Set.isempty (self)
+ return next(self) == nil
+end
+
+--- are the sets disjoint? (no elements in common).
+-- Uses naive definition, i.e. that intersection is empty
+-- @param s1 a Set
+-- @param s2 another set
+-- @return true or false
+function Set.isdisjoint (s1,s2)
+ return Set.isempty(Set.intersection(s1,s2))
+end
+
+--- size of this set (also # for 5.2).
+-- @param s a Set
+-- @return size
+-- @function Set.len
+Set.len = tablex.size
+
+--- cardinality of set (5.2).
+-- @within metamethods
+-- @function Set.__len
+Set.__len = Set.len
+
+--- equality between sets.
+-- @within metamethods
+function Set.__eq (s1,s2)
+ return Set.issubset(s1,s2) and Set.issubset(s2,s1)
+end
+
+return Set
diff --git a/macros/luatex/generic/penlight/pl/app.lua b/macros/luatex/generic/penlight/pl/app.lua
new file mode 100644
index 0000000000..a06305da4f
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/app.lua
@@ -0,0 +1,297 @@
+--- Application support functions.
+-- See @{01-introduction.md.Application_Support|the Guide}
+--
+-- Dependencies: `pl.utils`, `pl.path`
+-- @module pl.app
+
+local io,package,require = _G.io, _G.package, _G.require
+local utils = require 'pl.utils'
+local path = require 'pl.path'
+
+local app = {}
+
+--- return the name of the current script running.
+-- The name will be the name as passed on the command line
+-- @return string filename
+function app.script_name()
+ if _G.arg and _G.arg[0] then
+ return _G.arg[0]
+ end
+ return utils.raise("No script name found")
+end
+
+--- prefixes the current script's path to the Lua module path.
+-- Applies to both the source and the binary module paths. It makes it easy for
+-- the main file of a multi-file program to access its modules in the same directory.
+-- `base` allows these modules to be put in a specified subdirectory, to allow for
+-- cleaner deployment and resolve potential conflicts between a script name and its
+-- library directory.
+--
+-- Note: the path is prefixed, so it is searched first when requiring modules.
+-- @string base optional base directory (absolute, or relative path).
+-- @treturn string the current script's path with a trailing slash
+function app.require_here (base)
+ local p = path.dirname(app.script_name())
+ if not path.isabs(p) then
+ p = path.join(path.currentdir(),p)
+ end
+ if p:sub(-1,-1) ~= path.sep then
+ p = p..path.sep
+ end
+ if base then
+ if path.is_windows then
+ base = base:gsub('/','\\')
+ end
+ if path.isabs(base) then
+ p = base .. path.sep
+ else
+ p = p..base..path.sep
+ end
+ end
+ local so_ext = path.is_windows and 'dll' or 'so'
+ local lsep = package.path:find '^;' and '' or ';'
+ local csep = package.cpath:find '^;' and '' or ';'
+ package.path = ('%s?.lua;%s?%sinit.lua%s%s'):format(p,p,path.sep,lsep,package.path)
+ package.cpath = ('%s?.%s%s%s'):format(p,so_ext,csep,package.cpath)
+ return p
+end
+
+--- return a suitable path for files private to this application.
+-- These will look like '~/.SNAME/file', with '~' as with expanduser and
+-- SNAME is the name of the script without .lua extension.
+-- If the directory does not exist, it will be created.
+-- @string file a filename (w/out path)
+-- @return a full pathname, or nil
+-- @return cannot create directory error
+-- @usage
+-- -- when run from a script called 'testapp' (on Windows):
+-- local app = require 'pl.app'
+-- print(app.appfile 'test.txt')
+-- -- C:\Documents and Settings\steve\.testapp\test.txt
+function app.appfile(file)
+ local sfullname, err = app.script_name()
+ if not sfullname then return utils.raise(err) end
+ local sname = path.basename(sfullname)
+ local name = path.splitext(sname)
+ local dir = path.join(path.expanduser('~'),'.'..name)
+ if not path.isdir(dir) then
+ local ret = path.mkdir(dir)
+ if not ret then return utils.raise('cannot create '..dir) end
+ end
+ return path.join(dir,file)
+end
+
+--- return string indicating operating system.
+-- @return 'Windows','OSX' or whatever uname returns (e.g. 'Linux')
+function app.platform()
+ if path.is_windows then
+ return 'Windows'
+ else
+ local f = io.popen('uname')
+ local res = f:read()
+ if res == 'Darwin' then res = 'OSX' end
+ f:close()
+ return res
+ end
+end
+
+--- return the full command-line used to invoke this script.
+-- It will not include the scriptname itself, see `app.script_name`.
+-- @return command-line
+-- @return name of Lua program used
+-- @usage
+-- -- execute: lua -lluacov -e 'print(_VERSION)' myscript.lua
+--
+-- -- myscript.lua
+-- print(require("pl.app").lua()) --> "lua -lluacov -e 'print(_VERSION)'", "lua"
+function app.lua()
+ local args = _G.arg
+ if not args then
+ return utils.raise "not in a main program"
+ end
+
+ local cmd = {}
+ local i = -1
+ while true do
+ table.insert(cmd, 1, args[i])
+ if not args[i-1] then
+ return utils.quote_arg(cmd), args[i]
+ end
+ i = i - 1
+ end
+end
+
+--- parse command-line arguments into flags and parameters.
+-- Understands GNU-style command-line flags; short (`-f`) and long (`--flag`).
+--
+-- These may be given a value with either '=' or ':' (`-k:2`,`--alpha=3.2`,`-n2`),
+-- a number value can be given without a space. If the flag is marked
+-- as having a value, then a space-separated value is also accepted (`-i hello`),
+-- see the `flags_with_values` argument).
+--
+-- Multiple short args can be combined like so: ( `-abcd`).
+--
+-- When specifying the `flags_valid` parameter, its contents can also contain
+-- aliasses, to convert short/long flags to the same output name. See the
+-- example below.
+--
+-- Note: if a flag is repeated, the last value wins.
+-- @tparam {string} args an array of strings (default is the global `arg`)
+-- @tab flags_with_values any flags that take values, either list or hash
+-- table e.g. `{ out=true }` or `{ "out" }`.
+-- @tab flags_valid (optional) flags that are valid, either list or hashtable.
+-- If not given, everything
+-- will be accepted(everything in `flags_with_values` will automatically be allowed)
+-- @return a table of flags (flag=value pairs)
+-- @return an array of parameters
+-- @raise if args is nil, then the global `args` must be available!
+-- @usage
+-- -- Simple form:
+-- local flags, params = app.parse_args(nil,
+-- { "hello", "world" }, -- list of flags taking values
+-- { "l", "a", "b"}) -- list of allowed flags (value ones will be added)
+--
+-- -- More complex example using aliasses:
+-- local valid = {
+-- long = "l", -- if 'l' is specified, it is reported as 'long'
+-- new = { "n", "old" }, -- here both 'n' and 'old' will go into 'new'
+-- }
+-- local values = {
+-- "value", -- will automatically be added to the allowed set of flags
+-- "new", -- will mark 'n' and 'old' as requiring a value as well
+-- }
+-- local flags, params = app.parse_args(nil, values, valid)
+--
+-- -- command: myapp.lua -l --old:hello --value world param1 param2
+-- -- will yield:
+-- flags = {
+-- long = true, -- input from 'l'
+-- new = "hello", -- input from 'old'
+-- value = "world", -- allowed because it was in 'values', note: space separated!
+-- }
+-- params = {
+-- [1] = "param1"
+-- [2] = "param2"
+-- }
+function app.parse_args (args,flags_with_values, flags_valid)
+ if not args then
+ args = _G.arg
+ if not args then utils.raise "Not in a main program: 'arg' not found" end
+ end
+
+ local with_values = {}
+ for k,v in pairs(flags_with_values or {}) do
+ if type(k) == "number" then
+ k = v
+ end
+ with_values[k] = true
+ end
+
+ local valid
+ if not flags_valid then
+ -- if no allowed flags provided, we create a table that always returns
+ -- the keyname, no matter what you look up
+ valid = setmetatable({},{ __index = function(_, key) return key end })
+ else
+ valid = {}
+ for k,aliasses in pairs(flags_valid) do
+ if type(k) == "number" then -- array/list entry
+ k = aliasses
+ end
+ if type(aliasses) == "string" then -- single alias
+ aliasses = { aliasses }
+ end
+ if type(aliasses) == "table" then -- list of aliasses
+ -- it's the alternate name, so add the proper mappings
+ for i, alias in ipairs(aliasses) do
+ valid[alias] = k
+ end
+ end
+ valid[k] = k
+ end
+ do
+ local new_with_values = {} -- needed to prevent "invalid key to 'next'" error
+ for k,v in pairs(with_values) do
+ if not valid[k] then
+ valid[k] = k -- add the with_value entry as a valid one
+ new_with_values[k] = true
+ else
+ new_with_values[valid[k]] = true --set, but by its alias
+ end
+ end
+ with_values = new_with_values
+ end
+ end
+
+ -- now check that all flags with values are reported as such under all
+ -- of their aliasses
+ for k, main_alias in pairs(valid) do
+ if with_values[main_alias] then
+ with_values[k] = true
+ end
+ end
+
+ local _args = {}
+ local flags = {}
+ local i = 1
+ while i <= #args do
+ local a = args[i]
+ local v = a:match('^-(.+)')
+ local is_long
+ if not v then
+ -- we have a parameter
+ _args[#_args+1] = a
+ else
+ -- it's a flag
+ if v:find '^-' then
+ is_long = true
+ v = v:sub(2)
+ end
+ if with_values[v] then
+ if i == #args or args[i+1]:find '^-' then
+ return utils.raise ("no value for '"..v.."'")
+ end
+ flags[valid[v]] = args[i+1]
+ i = i + 1
+ else
+ -- a value can also be indicated with = or :
+ local var,val = utils.splitv (v,'[=:]', false, 2)
+ var = var or v
+ val = val or true
+ if not is_long then
+ if #var > 1 then
+ if var:find '.%d+' then -- short flag, number value
+ val = var:sub(2)
+ var = var:sub(1,1)
+ else -- multiple short flags
+ for i = 1,#var do
+ local f = var:sub(i,i)
+ if not valid[f] then
+ return utils.raise("unknown flag '"..f.."'")
+ else
+ f = valid[f]
+ end
+ flags[f] = true
+ end
+ val = nil -- prevents use of var as a flag below
+ end
+ else -- single short flag (can have value, defaults to true)
+ val = val or true
+ end
+ end
+ if val then
+ if not valid[var] then
+ return utils.raise("unknown flag '"..var.."'")
+ else
+ var = valid[var]
+ end
+ flags[var] = val
+ end
+ end
+ end
+ i = i + 1
+ end
+ return flags,_args
+end
+
+return app
diff --git a/macros/luatex/generic/penlight/pl/array2d.lua b/macros/luatex/generic/penlight/pl/array2d.lua
new file mode 100644
index 0000000000..2bc13b9cd4
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/array2d.lua
@@ -0,0 +1,579 @@
+--- Operations on two-dimensional arrays.
+-- See @{02-arrays.md.Operations_on_two_dimensional_tables|The Guide}
+--
+-- The size of the arrays is determined by using the length operator `#` hence
+-- the module is not `nil` safe, and the usual precautions apply.
+--
+-- Note: all functions taking `i1,j1,i2,j2` as arguments will normalize the
+-- arguments using `default_range`.
+--
+-- Dependencies: `pl.utils`, `pl.tablex`, `pl.types`
+-- @module pl.array2d
+
+local tonumber,tostring,io,ipairs,string,table =
+ _G.tonumber,_G.tostring,_G.io,_G.ipairs,_G.string,_G.table
+local setmetatable,getmetatable = setmetatable,getmetatable
+
+local tablex = require 'pl.tablex'
+local utils = require 'pl.utils'
+local types = require 'pl.types'
+local imap,tmap,reduce,keys,tmap2,tset,index_by = tablex.imap,tablex.map,tablex.reduce,tablex.keys,tablex.map2,tablex.set,tablex.index_by
+local remove = table.remove
+local splitv,fprintf,assert_arg = utils.splitv,utils.fprintf,utils.assert_arg
+local byte = string.byte
+local stdout = io.stdout
+local min = math.min
+
+
+local array2d = {}
+
+local function obj (int,out)
+ local mt = getmetatable(int)
+ if mt then
+ setmetatable(out,mt)
+ end
+ return out
+end
+
+local function makelist (res)
+ return setmetatable(res, require('pl.List'))
+end
+
+--- return the row and column size.
+-- Size is calculated using the Lua length operator #, so usual precautions
+-- regarding `nil` values apply.
+-- @array2d a a 2d array
+-- @treturn int number of rows (`#a`)
+-- @treturn int number of cols (`#a[1]`)
+function array2d.size (a)
+ assert_arg(1,a,'table')
+ return #a,#a[1]
+end
+
+do
+ local function index (t,k)
+ return t[k]
+ end
+
+ --- extract a column from the 2D array.
+ -- @array2d a 2d array
+ -- @param j column index
+ -- @return 1d array
+ function array2d.column (a,j)
+ assert_arg(1,a,'table')
+ return makelist(imap(index,a,j))
+ end
+end
+local column = array2d.column
+
+--- extract a row from the 2D array.
+-- Added in line with `column`, for read-only purposes directly
+-- accessing a[i] is more performant.
+-- @array2d a 2d array
+-- @param i row index
+-- @return 1d array (copy of the row)
+function array2d.row(a,i)
+ assert_arg(1,a,'table')
+ local row = a[i]
+ local r = {}
+ for n,v in ipairs(row) do
+ r[n] = v
+ end
+ return makelist(r)
+end
+
+--- map a function over a 2D array
+-- @func f a function of at least one argument
+-- @array2d a 2d array
+-- @param arg an optional extra argument to be passed to the function.
+-- @return 2d array
+function array2d.map (f,a,arg)
+ assert_arg(2,a,'table')
+ f = utils.function_arg(1,f)
+ return obj(a,imap(function(row) return imap(f,row,arg) end, a))
+end
+
+--- reduce the rows using a function.
+-- @func f a binary function
+-- @array2d a 2d array
+-- @return 1d array
+-- @see pl.tablex.reduce
+function array2d.reduce_rows (f,a)
+ assert_arg(1,a,'table')
+ return tmap(function(row) return reduce(f,row) end, a)
+end
+
+--- reduce the columns using a function.
+-- @func f a binary function
+-- @array2d a 2d array
+-- @return 1d array
+-- @see pl.tablex.reduce
+function array2d.reduce_cols (f,a)
+ assert_arg(1,a,'table')
+ return tmap(function(c) return reduce(f,column(a,c)) end, keys(a[1]))
+end
+
+--- reduce a 2D array into a scalar, using two operations.
+-- @func opc operation to reduce the final result
+-- @func opr operation to reduce the rows
+-- @param a 2D array
+function array2d.reduce2 (opc,opr,a)
+ assert_arg(3,a,'table')
+ local tmp = array2d.reduce_rows(opr,a)
+ return reduce(opc,tmp)
+end
+
+--- map a function over two arrays.
+-- They can be both or either 2D arrays
+-- @func f function of at least two arguments
+-- @int ad order of first array (`1` if `a` is a list/array, `2` if it is a 2d array)
+-- @int bd order of second array (`1` if `b` is a list/array, `2` if it is a 2d array)
+-- @tab a 1d or 2d array
+-- @tab b 1d or 2d array
+-- @param arg optional extra argument to pass to function
+-- @return 2D array, unless both arrays are 1D
+function array2d.map2 (f,ad,bd,a,b,arg)
+ assert_arg(1,a,'table')
+ assert_arg(2,b,'table')
+ f = utils.function_arg(1,f)
+ if ad == 1 and bd == 2 then
+ return imap(function(row)
+ return tmap2(f,a,row,arg)
+ end, b)
+ elseif ad == 2 and bd == 1 then
+ return imap(function(row)
+ return tmap2(f,row,b,arg)
+ end, a)
+ elseif ad == 1 and bd == 1 then
+ return tmap2(f,a,b)
+ elseif ad == 2 and bd == 2 then
+ return tmap2(function(rowa,rowb)
+ return tmap2(f,rowa,rowb,arg)
+ end, a,b)
+ end
+end
+
+--- cartesian product of two 1d arrays.
+-- @func f a function of 2 arguments
+-- @array t1 a 1d table
+-- @array t2 a 1d table
+-- @return 2d table
+-- @usage product('..',{1,2},{'a','b'}) == {{'1a','2a'},{'1b','2b'}}
+function array2d.product (f,t1,t2)
+ f = utils.function_arg(1,f)
+ assert_arg(2,t1,'table')
+ assert_arg(3,t2,'table')
+ local res = {}
+ for i,v in ipairs(t2) do
+ res[i] = tmap(f,t1,v)
+ end
+ return res
+end
+
+--- flatten a 2D array.
+-- (this goes over columns first.)
+-- @array2d t 2d table
+-- @return a 1d table
+-- @usage flatten {{1,2},{3,4},{5,6}} == {1,2,3,4,5,6}
+function array2d.flatten (t)
+ local res = {}
+ local k = 1
+ local rows, cols = array2d.size(t)
+ for r = 1, rows do
+ local row = t[r]
+ for c = 1, cols do
+ res[k] = row[c]
+ k = k + 1
+ end
+ end
+ return makelist(res)
+end
+
+--- reshape a 2D array. Reshape the aray by specifying a new nr of rows.
+-- @array2d t 2d array
+-- @int nrows new number of rows
+-- @bool co use column-order (Fortran-style) (default false)
+-- @return a new 2d array
+function array2d.reshape (t,nrows,co)
+ local nr,nc = array2d.size(t)
+ local ncols = nr*nc / nrows
+ local res = {}
+ local ir,ic = 1,1
+ for i = 1,nrows do
+ local row = {}
+ for j = 1,ncols do
+ row[j] = t[ir][ic]
+ if not co then
+ ic = ic + 1
+ if ic > nc then
+ ir = ir + 1
+ ic = 1
+ end
+ else
+ ir = ir + 1
+ if ir > nr then
+ ic = ic + 1
+ ir = 1
+ end
+ end
+ end
+ res[i] = row
+ end
+ return obj(t,res)
+end
+
+--- transpose a 2D array.
+-- @array2d t 2d array
+-- @return a new 2d array
+function array2d.transpose(t)
+ assert_arg(1,t,'table')
+ local _, c = array2d.size(t)
+ return array2d.reshape(t,c,true)
+end
+
+--- swap two rows of an array.
+-- @array2d t a 2d array
+-- @int i1 a row index
+-- @int i2 a row index
+-- @return t (same, modified 2d array)
+function array2d.swap_rows (t,i1,i2)
+ assert_arg(1,t,'table')
+ t[i1],t[i2] = t[i2],t[i1]
+ return t
+end
+
+--- swap two columns of an array.
+-- @array2d t a 2d array
+-- @int j1 a column index
+-- @int j2 a column index
+-- @return t (same, modified 2d array)
+function array2d.swap_cols (t,j1,j2)
+ assert_arg(1,t,'table')
+ for _, row in ipairs(t) do
+ row[j1],row[j2] = row[j2],row[j1]
+ end
+ return t
+end
+
+--- extract the specified rows.
+-- @array2d t 2d array
+-- @tparam {int} ridx a table of row indices
+-- @return a new 2d array with the extracted rows
+function array2d.extract_rows (t,ridx)
+ return obj(t,index_by(t,ridx))
+end
+
+--- extract the specified columns.
+-- @array2d t 2d array
+-- @tparam {int} cidx a table of column indices
+-- @return a new 2d array with the extracted colums
+function array2d.extract_cols (t,cidx)
+ assert_arg(1,t,'table')
+ local res = {}
+ for i = 1,#t do
+ res[i] = index_by(t[i],cidx)
+ end
+ return obj(t,res)
+end
+
+--- remove a row from an array.
+-- @function array2d.remove_row
+-- @array2d t a 2d array
+-- @int i a row index
+array2d.remove_row = remove
+
+--- remove a column from an array.
+-- @array2d t a 2d array
+-- @int j a column index
+function array2d.remove_col (t,j)
+ assert_arg(1,t,'table')
+ for i = 1,#t do
+ remove(t[i],j)
+ end
+end
+
+do
+ local function _parse (s)
+ local r, c = s:match 'R(%d+)C(%d+)'
+ if r then
+ r,c = tonumber(r),tonumber(c)
+ return r,c
+ end
+ c,r = s:match '(%a+)(%d+)'
+ if c then
+ local cv = 0
+ for i = 1, #c do
+ cv = cv * 26 + byte(c:sub(i,i)) - byte 'A' + 1
+ end
+ return tonumber(r), cv
+ end
+ 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')
+ -- @string s a range (case insensitive).
+ -- @treturn int start row
+ -- @treturn int start col
+ -- @treturn int end row
+ -- @treturn int end col
+ function array2d.parse_range (s)
+ assert_arg(1,s,'string')
+ s = s:upper()
+ if s:find ':' then
+ local start,finish = splitv(s,':')
+ local i1,j1 = _parse(start)
+ local i2,j2 = _parse(finish)
+ return i1,j1,i2,j2
+ else -- single value
+ local i,j = _parse(s)
+ return i,j
+ end
+ 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))
+end
+
+local default_range do
+ local function norm_value(v, max)
+ if not v then return v end
+ if v < 0 then
+ v = max + v + 1
+ end
+ if v < 1 then v = 1 end
+ if v > max then v = max end
+ return v
+ end
+
+ --- normalizes coordinates to valid positive entries and defaults.
+ -- 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
+ function array2d.default_range (t,i1,j1,i2,j2)
+ local nr, nc = array2d.size(t)
+ i1 = norm_value(i1 or 1, nr)
+ j1 = norm_value(j1 or 1, nc)
+ i2 = norm_value(i2 or nr, nr)
+ j2 = norm_value(j2 or nc, nc)
+ return i1,j1,i2,j2
+ end
+ default_range = array2d.default_range
+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)
+-- @return an array, 2D in general but 1D in special cases.
+function array2d.slice (t,i1,j1,i2,j2)
+ assert_arg(1,t,'table')
+ i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
+ local res = {}
+ for i = i1,i2 do
+ local val
+ local row = t[i]
+ if j1 == j2 then
+ val = row[j1]
+ else
+ val = {}
+ for j = j1,j2 do
+ val[#val+1] = row[j]
+ end
+ end
+ res[#res+1] = val
+ end
+ if i1 == i2 then res = res[1] end
+ return obj(t,res)
+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)
+-- @see tablex.set
+function array2d.set (t,value,i1,j1,i2,j2)
+ i1,j1,i2,j2 = default_range(t,i1,j1,i2,j2)
+ local i = i1
+ if types.is_callable(value) then
+ local old_f = value
+ value = function(j)
+ return old_f(i,j)
+ end
+ end
+ while i <= i2 do
+ tset(t[i],value,j1,j2)
+ i = i + 1
+ end
+end
+
+--- write a 2D array to a file.
+-- @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)
+function array2d.write (t,f,fmt,i1,j1,i2,j2)
+ assert_arg(1,t,'table')
+ f = f or stdout
+ local rowop
+ if fmt then
+ rowop = function(row,j) fprintf(f,fmt,row[j]) end
+ else
+ rowop = function(row,j) f:write(tostring(row[j]),' ') end
+ end
+ local function newline()
+ f:write '\n'
+ end
+ array2d.forall(t,rowop,newline,i1,j1,i2,j2)
+end
+
+--- perform an operation for all values in a 2D array.
+-- @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)
+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)
+ for i = i1,i2 do
+ local row = t[i]
+ for j = j1,j2 do
+ row_op(row,j)
+ end
+ if end_row_op then end_row_op(i) end
+ end
+end
+
+---- move a block from the destination to the source.
+-- @array2d dest a 2D array
+-- @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)
+function array2d.move (dest,di,dj,src,i1,j1,i2,j2)
+ assert_arg(1,dest,'table')
+ assert_arg(4,src,'table')
+ i1,j1,i2,j2 = default_range(src,i1,j1,i2,j2)
+ local nr,nc = array2d.size(dest)
+ i2, j2 = min(nr,i2), min(nc,j2)
+ --i1, j1 = max(1,i1), max(1,j1)
+ dj = dj - 1
+ for i = i1,i2 do
+ local drow, srow = dest[i+di-1], src[i]
+ for j = j1,j2 do
+ drow[j+dj] = srow[j]
+ end
+ end
+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)
+-- @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')
+ i1,j1,i2,j2 = default_range(a,i1,j1,i2,j2)
+ local i,j = i1,j1-1
+ local row = a[i]
+ return function()
+ j = j + 1
+ if j > j2 then
+ j = j1
+ i = i + 1
+ row = a[i]
+ if i > i2 then
+ return nil
+ end
+ end
+ if indices then
+ return i,j,row[j]
+ else
+ return row[j]
+ end
+ end
+end
+
+--- iterate over all columns.
+-- @array2d a a 2D array
+-- @return column, column-index
+function array2d.columns(a)
+ assert_arg(1,a,'table')
+ local n = #a[1]
+ local i = 0
+ return function()
+ i = i + 1
+ if i > n then return nil end
+ return column(a,i), i
+ end
+end
+
+--- iterate over all rows.
+-- Returns a copy of the row, for read-only purrposes directly iterating
+-- is more performant; `ipairs(a)`
+-- @array2d a a 2D array
+-- @return row, row-index
+function array2d.rows(a)
+ assert_arg(1,a,'table')
+ local n = #a
+ local i = 0
+ return function()
+ i = i + 1
+ if i > n then return nil end
+ return array2d.row(a,i), i
+ end
+end
+
+--- new array of specified dimensions
+-- @int rows number of rows
+-- @int cols number of cols
+-- @param val initial value; if it's a function then use `val(i,j)`
+-- @return new 2d array
+function array2d.new(rows,cols,val)
+ local res = {}
+ local fun = types.is_callable(val)
+ for i = 1,rows do
+ local row = {}
+ if fun then
+ for j = 1,cols do row[j] = val(i,j) end
+ else
+ for j = 1,cols do row[j] = val end
+ end
+ res[i] = row
+ end
+ return res
+end
+
+return array2d
diff --git a/macros/luatex/generic/penlight/pl/class.lua b/macros/luatex/generic/penlight/pl/class.lua
new file mode 100644
index 0000000000..49246ee32b
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/class.lua
@@ -0,0 +1,265 @@
+--- Provides a reuseable and convenient framework for creating classes in Lua.
+-- Two possible notations:
+--
+-- B = class(A)
+-- class.B(A)
+--
+-- The latter form creates a named class within the current environment. Note
+-- that this implicitly brings in `pl.utils` as a dependency.
+--
+-- See the Guide for further @{01-introduction.md.Simplifying_Object_Oriented_Programming_in_Lua|discussion}
+-- @module pl.class
+
+local error, getmetatable, io, pairs, rawget, rawset, setmetatable, tostring, type =
+ _G.error, _G.getmetatable, _G.io, _G.pairs, _G.rawget, _G.rawset, _G.setmetatable, _G.tostring, _G.type
+local compat
+
+-- this trickery is necessary to prevent the inheritance of 'super' and
+-- the resulting recursive call problems.
+local function call_ctor (c,obj,...)
+ local init = rawget(c,'_init')
+ local parent_with_init = rawget(c,'_parent_with_init')
+
+ if parent_with_init then
+ if not init then -- inheriting an init
+ init = rawget(parent_with_init, '_init')
+ parent_with_init = rawget(parent_with_init, '_parent_with_init')
+ end
+ if parent_with_init then -- super() points to one above whereever _init came from
+ rawset(obj,'super',function(obj,...)
+ call_ctor(parent_with_init,obj,...)
+ end)
+ end
+ else
+ -- Without this, calling super() where none exists will sometimes loop and stack overflow
+ rawset(obj,'super',nil)
+ end
+
+ local res = init(obj,...)
+ if parent_with_init then -- If this execution of call_ctor set a super, unset it
+ rawset(obj,'super',nil)
+ end
+
+ return res
+end
+
+--- initializes an __instance__ upon creation.
+-- @function class:_init
+-- @param ... parameters passed to the constructor
+-- @usage local Cat = class()
+-- function Cat:_init(name)
+-- --self:super(name) -- call the ancestor initializer if needed
+-- self.name = name
+-- end
+--
+-- local pussycat = Cat("pussycat")
+-- print(pussycat.name) --> pussycat
+
+--- checks whether an __instance__ is derived from some class.
+-- Works the other way around as `class_of`. It has two ways of using;
+-- 1) call with a class to check against, 2) call without params.
+-- @function instance:is_a
+-- @param some_class class to check against, or `nil` to return the class
+-- @return `true` if `instance` is derived from `some_class`, or if `some_class == nil` then
+-- it returns the class table of the instance
+-- @usage local pussycat = Lion() -- assuming Lion derives from Cat
+-- if pussycat:is_a(Cat) then
+-- -- it's true, it is a Lion, but also a Cat
+-- end
+--
+-- if pussycat:is_a() == Lion then
+-- -- It's true
+-- end
+local function is_a(self,klass)
+ if klass == nil then
+ -- no class provided, so return the class this instance is derived from
+ return getmetatable(self)
+ end
+ local m = getmetatable(self)
+ if not m then return false end --*can't be an object!
+ while m do
+ if m == klass then return true end
+ m = rawget(m,'_base')
+ end
+ return false
+end
+
+--- checks whether an __instance__ is derived from some class.
+-- Works the other way around as `is_a`.
+-- @function some_class:class_of
+-- @param some_instance instance to check against
+-- @return `true` if `some_instance` is derived from `some_class`
+-- @usage local pussycat = Lion() -- assuming Lion derives from Cat
+-- if Cat:class_of(pussycat) then
+-- -- it's true
+-- end
+local function class_of(klass,obj)
+ if type(klass) ~= 'table' or not rawget(klass,'is_a') then return false end
+ return klass.is_a(obj,klass)
+end
+
+--- cast an object to another class.
+-- It is not clever (or safe!) so use carefully.
+-- @param some_instance the object to be changed
+-- @function some_class:cast
+local function cast (klass, obj)
+ return setmetatable(obj,klass)
+end
+
+
+local function _class_tostring (obj)
+ local mt = obj._class
+ local name = rawget(mt,'_name')
+ setmetatable(obj,nil)
+ local str = tostring(obj)
+ setmetatable(obj,mt)
+ if name then str = name ..str:gsub('table','') end
+ return str
+end
+
+local function tupdate(td,ts,dont_override)
+ for k,v in pairs(ts) do
+ if not dont_override or td[k] == nil then
+ td[k] = v
+ end
+ end
+end
+
+local function _class(base,c_arg,c)
+ -- the class `c` will be the metatable for all its objects,
+ -- and they will look up their methods in it.
+ local mt = {} -- a metatable for the class to support __call and _handler
+ -- can define class by passing it a plain table of methods
+ local plain = type(base) == 'table' and not getmetatable(base)
+ if plain then
+ c = base
+ base = c._base
+ else
+ c = c or {}
+ end
+
+ if type(base) == 'table' then
+ -- our new class is a shallow copy of the base class!
+ -- but be careful not to wipe out any methods we have been given at this point!
+ tupdate(c,base,plain)
+ c._base = base
+ -- inherit the 'not found' handler, if present
+ if rawget(c,'_handler') then mt.__index = c._handler end
+ elseif base ~= nil then
+ error("must derive from a table type",3)
+ end
+
+ c.__index = c
+ setmetatable(c,mt)
+ if not plain then
+ if base and rawget(base,'_init') then c._parent_with_init = base end -- For super and inherited init
+ c._init = nil
+ end
+
+ if base and rawget(base,'_class_init') then
+ base._class_init(c,c_arg)
+ end
+
+ -- expose a ctor which can be called by <classname>(<args>)
+ mt.__call = function(class_tbl,...)
+ local obj
+ if rawget(c,'_create') then obj = c._create(...) end
+ if not obj then obj = {} end
+ setmetatable(obj,c)
+
+ if rawget(c,'_init') or rawget(c,'_parent_with_init') then -- constructor exists
+ local res = call_ctor(c,obj,...)
+ if res then -- _if_ a ctor returns a value, it becomes the object...
+ obj = res
+ setmetatable(obj,c)
+ end
+ end
+
+ if base and rawget(base,'_post_init') then
+ base._post_init(obj)
+ end
+
+ return obj
+ end
+ -- Call Class.catch to set a handler for methods/properties not found in the class!
+ c.catch = function(self, handler)
+ if type(self) == "function" then
+ -- called using . instead of :
+ handler = self
+ end
+ c._handler = handler
+ mt.__index = handler
+ end
+ c.is_a = is_a
+ c.class_of = class_of
+ c.cast = cast
+ c._class = c
+
+ if not rawget(c,'__tostring') then
+ c.__tostring = _class_tostring
+ end
+
+ return c
+end
+
+--- create a new class, derived from a given base class.
+-- Supporting two class creation syntaxes:
+-- either `Name = class(base)` or `class.Name(base)`.
+-- The first form returns the class directly and does not set its `_name`.
+-- The second form creates a variable `Name` in the current environment set
+-- to the class, and also sets `_name`.
+-- @function class
+-- @param base optional base class
+-- @param c_arg optional parameter to class constructor
+-- @param c optional table to be used as class
+local class
+class = setmetatable({},{
+ __call = function(fun,...)
+ return _class(...)
+ end,
+ __index = function(tbl,key)
+ if key == 'class' then
+ io.stderr:write('require("pl.class").class is deprecated. Use require("pl.class")\n')
+ return class
+ end
+ compat = compat or require 'pl.compat'
+ local env = compat.getfenv(2)
+ return function(...)
+ local c = _class(...)
+ c._name = key
+ rawset(env,key,c)
+ return c
+ end
+ end
+})
+
+class.properties = class()
+
+function class.properties._class_init(klass)
+ klass.__index = function(t,key)
+ -- normal class lookup!
+ local v = klass[key]
+ if v then return v end
+ -- is it a getter?
+ v = rawget(klass,'get_'..key)
+ if v then
+ return v(t)
+ end
+ -- is it a field?
+ return rawget(t,'_'..key)
+ end
+ klass.__newindex = function (t,key,value)
+ -- if there's a setter, use that, otherwise directly set table
+ local p = 'set_'..key
+ local setter = klass[p]
+ if setter then
+ setter(t,value)
+ else
+ rawset(t,key,value)
+ end
+ end
+end
+
+
+return class
+
diff --git a/macros/luatex/generic/penlight/pl/compat.lua b/macros/luatex/generic/penlight/pl/compat.lua
new file mode 100644
index 0000000000..1707ef6849
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/compat.lua
@@ -0,0 +1,250 @@
+----------------
+--- Lua 5.1/5.2/5.3 compatibility.
+-- Injects `table.pack`, `table.unpack`, and `package.searchpath` in the global
+-- environment, to make sure they are available for Lua 5.1 and LuaJIT.
+--
+-- All other functions are exported as usual in the returned module table.
+--
+-- NOTE: everything in this module is also available in `pl.utils`.
+-- @module pl.compat
+local compat = {}
+
+--- boolean flag this is Lua 5.1 (or LuaJIT).
+-- @field lua51
+compat.lua51 = _VERSION == 'Lua 5.1'
+
+--- boolean flag this is LuaJIT.
+-- @field jit
+compat.jit = (tostring(assert):match('builtin') ~= nil)
+
+--- boolean flag this is LuaJIT with 5.2 compatibility compiled in.
+-- @field jit52
+if compat.jit then
+ -- 'goto' is a keyword when 52 compatibility is enabled in LuaJit
+ compat.jit52 = not loadstring("local goto = 1")
+end
+
+--- the directory separator character for the current platform.
+-- @field dir_separator
+compat.dir_separator = _G.package.config:sub(1,1)
+
+--- boolean flag this is a Windows platform.
+-- @field is_windows
+compat.is_windows = compat.dir_separator == '\\'
+
+--- execute a shell command, in a compatible and platform independent way.
+-- This is a compatibility function that returns the same for Lua 5.1 and
+-- Lua 5.2+.
+--
+-- NOTE: Windows systems can use signed 32bit integer exitcodes. Posix systems
+-- only use exitcodes 0-255, anything else is undefined.
+--
+-- NOTE2: In Lua 5.2 and 5.3 a Windows exitcode of -1 would not properly be
+-- returned, this function will return it properly for all versions.
+-- @param cmd a shell command
+-- @return true if successful
+-- @return actual return code
+function compat.execute(cmd)
+ local res1,res2,res3 = os.execute(cmd)
+ if res2 == "No error" and res3 == 0 and compat.is_windows then
+ -- os.execute bug in Lua 5.2/5.3 not reporting -1 properly on Windows
+ -- this was fixed in 5.4
+ res3 = -1
+ end
+ if compat.lua51 and not compat.jit52 then
+ if compat.is_windows then
+ return res1==0,res1
+ else
+ res1 = res1 > 255 and res1 / 256 or res1
+ return res1==0,res1
+ end
+ else
+ if compat.is_windows then
+ return res3==0,res3
+ else
+ return not not res1,res3
+ end
+ end
+end
+
+----------------
+-- Load Lua code as a text or binary chunk (in a Lua 5.2 compatible way).
+-- @param ld code string or loader
+-- @param[opt] source name of chunk for errors
+-- @param[opt] mode 'b', 't' or 'bt'
+-- @param[opt] env environment to load the chunk in
+-- @function compat.load
+
+---------------
+-- Get environment of a function (in a Lua 5.1 compatible way).
+-- Not 100% compatible, so with Lua 5.2 it may return nil for a function with no
+-- global references!
+-- Based on code by [Sergey Rozhenko](http://lua-users.org/lists/lua-l/2010-06/msg00313.html)
+-- @param f a function or a call stack reference
+-- @function compat.getfenv
+
+---------------
+-- Set environment of a function (in a Lua 5.1 compatible way).
+-- @param f a function or a call stack reference
+-- @param env a table that becomes the new environment of `f`
+-- @function compat.setfenv
+
+if compat.lua51 then -- define Lua 5.2 style load()
+ if not compat.jit then -- but LuaJIT's load _is_ compatible
+ local lua51_load = load
+ function compat.load(str,src,mode,env)
+ local chunk,err
+ if type(str) == 'string' then
+ if str:byte(1) == 27 and not (mode or 'bt'):find 'b' then
+ return nil,"attempt to load a binary chunk"
+ end
+ chunk,err = loadstring(str,src)
+ else
+ chunk,err = lua51_load(str,src)
+ end
+ if chunk and env then setfenv(chunk,env) end
+ return chunk,err
+ end
+ else
+ compat.load = load
+ end
+ compat.setfenv, compat.getfenv = setfenv, getfenv
+else
+ compat.load = load
+ -- setfenv/getfenv replacements for Lua 5.2
+ -- by Sergey Rozhenko
+ -- http://lua-users.org/lists/lua-l/2010-06/msg00313.html
+ -- Roberto Ierusalimschy notes that it is possible for getfenv to return nil
+ -- in the case of a function with no globals:
+ -- http://lua-users.org/lists/lua-l/2010-06/msg00315.html
+ function compat.setfenv(f, t)
+ f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
+ local name
+ local up = 0
+ repeat
+ up = up + 1
+ name = debug.getupvalue(f, up)
+ until name == '_ENV' or name == nil
+ if name then
+ debug.upvaluejoin(f, up, function() return name end, 1) -- use unique upvalue
+ debug.setupvalue(f, up, t)
+ end
+ if f ~= 0 then return f end
+ end
+
+ function compat.getfenv(f)
+ local f = f or 0
+ f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func)
+ local name, val
+ local up = 0
+ repeat
+ up = up + 1
+ name, val = debug.getupvalue(f, up)
+ until name == '_ENV' or name == nil
+ return val
+ end
+end
+
+
+--- Global exported functions (for Lua 5.1 & LuaJIT)
+-- @section lua52
+
+--- pack an argument list into a table.
+-- @param ... any arguments
+-- @return a table with field n set to the length
+-- @function table.pack
+if not table.pack then
+ function table.pack (...) -- luacheck: ignore
+ return {n=select('#',...); ...}
+ end
+end
+
+--- unpack a table and return the elements.
+--
+-- NOTE: this version does NOT honor the n field, and hence it is not nil-safe.
+-- 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
+-- @return multiple return values from the table
+-- @function table.unpack
+-- @see utils.unpack
+if not table.unpack then
+ table.unpack = unpack -- luacheck: ignore
+end
+
+--- return the full path where a file name would be matched.
+-- This function was introduced in Lua 5.2, so this compatibility version
+-- will be injected in Lua 5.1 engines.
+-- @string name file name, possibly dotted
+-- @string path a path-template in the same form as package.path or package.cpath
+-- @string[opt] sep template separate character to be replaced by path separator. Default: "."
+-- @string[opt] rep the path separator to use, defaults to system separator. Default; "/" on Unixes, "\" on Windows.
+-- @see path.package_path
+-- @function package.searchpath
+-- @return on success: path of the file
+-- @return on failure: nil, error string listing paths tried
+if not package.searchpath then
+ function package.searchpath (name,path,sep,rep) -- luacheck: ignore
+ if type(name) ~= "string" then
+ error(("bad argument #1 to 'searchpath' (string expected, got %s)"):format(type(path)), 2)
+ end
+ if type(path) ~= "string" then
+ error(("bad argument #2 to 'searchpath' (string expected, got %s)"):format(type(path)), 2)
+ end
+ if sep ~= nil and type(sep) ~= "string" then
+ error(("bad argument #3 to 'searchpath' (string expected, got %s)"):format(type(path)), 2)
+ end
+ if rep ~= nil and type(rep) ~= "string" then
+ error(("bad argument #4 to 'searchpath' (string expected, got %s)"):format(type(path)), 2)
+ end
+ sep = sep or "."
+ rep = rep or compat.dir_separator
+ do
+ local s, e = name:find(sep, nil, true)
+ while s do
+ name = name:sub(1, s-1) .. rep .. name:sub(e+1, -1)
+ s, e = name:find(sep, s + #rep + 1, true)
+ end
+ end
+ local tried = {}
+ for m in path:gmatch('[^;]+') do
+ local nm = m:gsub('?', name)
+ tried[#tried+1] = nm
+ local f = io.open(nm,'r')
+ if f then f:close(); return nm end
+ end
+ return nil, "\tno file '" .. table.concat(tried, "'\n\tno file '") .. "'"
+ end
+end
+
+--- Global exported functions (for Lua < 5.4)
+-- @section lua54
+
+--- raise a warning message.
+-- This functions mimics the `warn` function added in Lua 5.4.
+-- @function warn
+-- @param ... any arguments
+if not warn then -- luacheck: ignore
+ local enabled = false
+ function warn(arg1, ...) -- luacheck: ignore
+ if type(arg1) == "string" and arg1:sub(1, 1) == "@" then
+ -- control message
+ if arg1 == "@on" then
+ enabled = true
+ return
+ end
+ if arg1 == "@off" then
+ enabled = false
+ return
+ end
+ return -- ignore unknown control messages
+ end
+ if enabled then
+ io.stderr:write("Lua warning: ", arg1, ...)
+ io.stderr:write("\n")
+ end
+ end
+end
+
+return compat
diff --git a/macros/luatex/generic/penlight/pl/comprehension.lua b/macros/luatex/generic/penlight/pl/comprehension.lua
new file mode 100644
index 0000000000..39be7c5260
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/comprehension.lua
@@ -0,0 +1,285 @@
+--- List comprehensions implemented in Lua.
+--
+-- See the [wiki page](http://lua-users.org/wiki/ListComprehensions)
+--
+-- local C= require 'pl.comprehension' . new()
+--
+-- C ('x for x=1,10') ()
+-- ==> {1,2,3,4,5,6,7,8,9,10}
+-- C 'x^2 for x=1,4' ()
+-- ==> {1,4,9,16}
+-- C '{x,x^2} for x=1,4' ()
+-- ==> {{1,1},{2,4},{3,9},{4,16}}
+-- C '2*x for x' {1,2,3}
+-- ==> {2,4,6}
+-- dbl = C '2*x for x'
+-- dbl {10,20,30}
+-- ==> {20,40,60}
+-- C 'x for x if x % 2 == 0' {1,2,3,4,5}
+-- ==> {2,4}
+-- C '{x,y} for x = 1,2 for y = 1,2' ()
+-- ==> {{1,1},{1,2},{2,1},{2,2}}
+-- C '{x,y} for x for y' ({1,2},{10,20})
+-- ==> {{1,10},{1,20},{2,10},{2,20}}
+-- assert(C 'sum(x^2 for x)' {2,3,4} == 2^2+3^2+4^2)
+--
+-- (c) 2008 David Manura. Licensed under the same terms as Lua (MIT license).
+--
+-- Dependencies: `pl.utils`, `pl.luabalanced`
+--
+-- See @{07-functional.md.List_Comprehensions|the Guide}
+-- @module pl.comprehension
+
+local utils = require 'pl.utils'
+
+local status,lb = pcall(require, "pl.luabalanced")
+if not status then
+ lb = require 'luabalanced'
+end
+
+local math_max = math.max
+local table_concat = table.concat
+
+-- fold operations
+-- http://en.wikipedia.org/wiki/Fold_(higher-order_function)
+local ops = {
+ list = {init=' {} ', accum=' __result[#__result+1] = (%s) '},
+ table = {init=' {} ', accum=' local __k, __v = %s __result[__k] = __v '},
+ sum = {init=' 0 ', accum=' __result = __result + (%s) '},
+ min = {init=' nil ', accum=' local __tmp = %s ' ..
+ ' if __result then if __tmp < __result then ' ..
+ '__result = __tmp end else __result = __tmp end '},
+ max = {init=' nil ', accum=' local __tmp = %s ' ..
+ ' if __result then if __tmp > __result then ' ..
+ '__result = __tmp end else __result = __tmp end '},
+}
+
+
+-- Parses comprehension string expr.
+-- Returns output expression list <out> string, array of for types
+-- ('=', 'in' or nil) <fortypes>, array of input variable name
+-- strings <invarlists>, array of input variable value strings
+-- <invallists>, array of predicate expression strings <preds>,
+-- operation name string <opname>, and number of placeholder
+-- parameters <max_param>.
+--
+-- The is equivalent to the mathematical set-builder notation:
+--
+-- <opname> { <out> | <invarlist> in <invallist> , <preds> }
+--
+-- @usage "x^2 for x" -- array values
+-- @usage "x^2 for x=1,10,2" -- numeric for
+-- @usage "k^v for k,v in pairs(_1)" -- iterator for
+-- @usage "(x+y)^2 for x for y if x > y" -- nested
+--
+local function parse_comprehension(expr)
+ local pos = 1
+
+ -- extract opname (if exists)
+ local opname
+ local tok, post = expr:match('^%s*([%a_][%w_]*)%s*%(()', pos)
+ local pose = #expr + 1
+ if tok then
+ local tok2, posb = lb.match_bracketed(expr, post-1)
+ assert(tok2, 'syntax error')
+ if expr:match('^%s*$', posb) then
+ opname = tok
+ pose = posb - 1
+ pos = post
+ end
+ end
+ opname = opname or "list"
+
+ -- extract out expression list
+ local out; out, pos = lb.match_explist(expr, pos)
+ assert(out, "syntax error: missing expression list")
+ out = table_concat(out, ', ')
+
+ -- extract "for" clauses
+ local fortypes = {}
+ local invarlists = {}
+ local invallists = {}
+ while 1 do
+ local post = expr:match('^%s*for%s+()', pos)
+ if not post then break end
+ pos = post
+
+ -- extract input vars
+ local iv; iv, pos = lb.match_namelist(expr, pos)
+ assert(#iv > 0, 'syntax error: zero variables')
+ for _,ident in ipairs(iv) do
+ assert(not ident:match'^__',
+ "identifier " .. ident .. " may not contain __ prefix")
+ end
+ invarlists[#invarlists+1] = iv
+
+ -- extract '=' or 'in' (optional)
+ local fortype, post = expr:match('^(=)%s*()', pos)
+ if not fortype then fortype, post = expr:match('^(in)%s+()', pos) end
+ if fortype then
+ pos = post
+ -- extract input value range
+ local il; il, pos = lb.match_explist(expr, pos)
+ assert(#il > 0, 'syntax error: zero expressions')
+ assert(fortype ~= '=' or #il == 2 or #il == 3,
+ 'syntax error: numeric for requires 2 or three expressions')
+ fortypes[#invarlists] = fortype
+ invallists[#invarlists] = il
+ else
+ fortypes[#invarlists] = false
+ invallists[#invarlists] = false
+ end
+ end
+ assert(#invarlists > 0, 'syntax error: missing "for" clause')
+
+ -- extract "if" clauses
+ local preds = {}
+ while 1 do
+ local post = expr:match('^%s*if%s+()', pos)
+ if not post then break end
+ pos = post
+ local pred; pred, pos = lb.match_expression(expr, pos)
+ assert(pred, 'syntax error: predicated expression not found')
+ preds[#preds+1] = pred
+ end
+
+ -- extract number of parameter variables (name matching "_%d+")
+ local stmp = ''; lb.gsub(expr, function(u, sin) -- strip comments/strings
+ if u == 'e' then stmp = stmp .. ' ' .. sin .. ' ' end
+ end)
+ local max_param = 0; stmp:gsub('[%a_][%w_]*', function(s)
+ local s = s:match('^_(%d+)$')
+ if s then max_param = math_max(max_param, tonumber(s)) end
+ end)
+
+ if pos ~= pose then
+ assert(false, "syntax error: unrecognized " .. expr:sub(pos))
+ end
+
+ --DEBUG:
+ --print('----\n', string.format("%q", expr), string.format("%q", out), opname)
+ --for k,v in ipairs(invarlists) do print(k,v, invallists[k]) end
+ --for k,v in ipairs(preds) do print(k,v) end
+
+ return out, fortypes, invarlists, invallists, preds, opname, max_param
+end
+
+
+-- Create Lua code string representing comprehension.
+-- Arguments are in the form returned by parse_comprehension.
+local function code_comprehension(
+ out, fortypes, invarlists, invallists, preds, opname, max_param
+)
+ local op = assert(ops[opname])
+ local code = op.accum:gsub('%%s', out)
+
+ for i=#preds,1,-1 do local pred = preds[i]
+ code = ' if ' .. pred .. ' then ' .. code .. ' end '
+ end
+ for i=#invarlists,1,-1 do
+ if not fortypes[i] then
+ local arrayname = '__in' .. i
+ local idx = '__idx' .. i
+ code =
+ ' for ' .. idx .. ' = 1, #' .. arrayname .. ' do ' ..
+ ' local ' .. invarlists[i][1] .. ' = ' .. arrayname .. '['..idx..'] ' ..
+ code .. ' end '
+ else
+ code =
+ ' for ' ..
+ table_concat(invarlists[i], ', ') ..
+ ' ' .. fortypes[i] .. ' ' ..
+ table_concat(invallists[i], ', ') ..
+ ' do ' .. code .. ' end '
+ end
+ end
+ code = ' local __result = ( ' .. op.init .. ' ) ' .. code
+ return code
+end
+
+
+-- Convert code string represented by code_comprehension
+-- into Lua function. Also must pass ninputs = #invarlists,
+-- max_param, and invallists (from parse_comprehension).
+-- Uses environment env.
+local function wrap_comprehension(code, ninputs, max_param, invallists, env)
+ assert(ninputs > 0)
+ local ts = {}
+ for i=1,max_param do
+ ts[#ts+1] = '_' .. i
+ end
+ for i=1,ninputs do
+ if not invallists[i] then
+ local name = '__in' .. i
+ ts[#ts+1] = name
+ end
+ end
+ if #ts > 0 then
+ code = ' local ' .. table_concat(ts, ', ') .. ' = ... ' .. code
+ end
+ code = code .. ' return __result '
+ --print('DEBUG:', code)
+ local f, err = utils.load(code,'tmp','t',env)
+ if not f then assert(false, err .. ' with generated code ' .. code) end
+ return f
+end
+
+
+-- Build Lua function from comprehension string.
+-- Uses environment env.
+local function build_comprehension(expr, env)
+ local out, fortypes, invarlists, invallists, preds, opname, max_param
+ = parse_comprehension(expr)
+ local code = code_comprehension(
+ out, fortypes, invarlists, invallists, preds, opname, max_param)
+ local f = wrap_comprehension(code, #invarlists, max_param, invallists, env)
+ return f
+end
+
+
+-- Creates new comprehension cache.
+-- Any list comprehension function created are set to the environment
+-- env (defaults to caller of new).
+local function new(env)
+ -- Note: using a single global comprehension cache would have had
+ -- security implications (e.g. retrieving cached functions created
+ -- in other environments).
+ -- The cache lookup function could have instead been written to retrieve
+ -- the caller's environment, lookup up the cache private to that
+ -- environment, and then looked up the function in that cache.
+ -- That would avoid the need for this <new> call to
+ -- explicitly manage caches; however, that might also have an undue
+ -- performance penalty.
+
+ if not env then
+ env = utils.getfenv(2)
+ end
+
+ local mt = {}
+ local cache = setmetatable({}, mt)
+
+ -- Index operator builds, caches, and returns Lua function
+ -- corresponding to comprehension expression string.
+ --
+ -- Example: f = comprehension['x^2 for x']
+ --
+ function mt:__index(expr)
+ local f = build_comprehension(expr, env)
+ self[expr] = f -- cache
+ return f
+ end
+
+ -- Convenience syntax.
+ -- Allows comprehension 'x^2 for x' instead of comprehension['x^2 for x'].
+ mt.__call = mt.__index
+
+ cache.new = new
+
+ return cache
+end
+
+
+local comprehension = {}
+comprehension.new = new
+
+return comprehension
diff --git a/macros/luatex/generic/penlight/pl/config.lua b/macros/luatex/generic/penlight/pl/config.lua
new file mode 100644
index 0000000000..2e6db0cb63
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/config.lua
@@ -0,0 +1,207 @@
+--- Reads configuration files into a Lua table.
+-- Understands INI files, classic Unix config files, and simple
+-- delimited columns of values. See @{06-data.md.Reading_Configuration_Files|the Guide}
+--
+-- # test.config
+-- # Read timeout in seconds
+-- read.timeout=10
+-- # Write timeout in seconds
+-- write.timeout=5
+-- #acceptable ports
+-- ports = 1002,1003,1004
+--
+-- -- readconfig.lua
+-- local config = require 'config'
+-- local t = config.read 'test.config'
+-- print(pretty.write(t))
+--
+-- ### output #####
+-- {
+-- ports = {
+-- 1002,
+-- 1003,
+-- 1004
+-- },
+-- write_timeout = 5,
+-- read_timeout = 10
+-- }
+--
+-- @module pl.config
+
+local type,tonumber,ipairs,io, table = _G.type,_G.tonumber,_G.ipairs,_G.io,_G.table
+
+local function split(s,re)
+ local res = {}
+ local t_insert = table.insert
+ re = '[^'..re..']+'
+ for k in s:gmatch(re) do t_insert(res,k) end
+ return res
+end
+
+local function strip(s)
+ return s:gsub('^%s+',''):gsub('%s+$','')
+end
+
+local function strip_quotes (s)
+ return s:gsub("['\"](.*)['\"]",'%1')
+end
+
+local config = {}
+
+--- like io.lines(), but allows for lines to be continued with '\'.
+-- @param file a file-like object (anything where read() returns the next line) or a filename.
+-- Defaults to stardard input.
+-- @return an iterator over the lines, or nil
+-- @return error 'not a file-like object' or 'file is nil'
+function config.lines(file)
+ local f,openf,err
+ local line = ''
+ if type(file) == 'string' then
+ f,err = io.open(file,'r')
+ if not f then return nil,err end
+ openf = true
+ else
+ f = file or io.stdin
+ if not file.read then return nil, 'not a file-like object' end
+ end
+ if not f then return nil, 'file is nil' end
+ return function()
+ local l = f:read()
+ while l do
+ -- only for non-blank lines that don't begin with either ';' or '#'
+ if l:match '%S' and not l:match '^%s*[;#]' then
+ -- does the line end with '\'?
+ local i = l:find '\\%s*$'
+ if i then -- if so,
+ line = line..l:sub(1,i-1)
+ elseif line == '' then
+ return l
+ else
+ l = line..l
+ line = ''
+ return l
+ end
+ end
+ l = f:read()
+ end
+ if openf then f:close() end
+ end
+end
+
+--- read a configuration file into a table
+-- @param file either a file-like object or a string, which must be a filename
+-- @tab[opt] cnfg a configuration table that may contain these fields:
+--
+-- * `smart` try to deduce what kind of config file we have (default false)
+-- * `variabilize` make names into valid Lua identifiers (default true)
+-- * `convert_numbers` try to convert values into numbers (default true)
+-- * `trim_space` ensure that there is no starting or trailing whitespace with values (default true)
+-- * `trim_quotes` remove quotes from strings (default false)
+-- * `list_delim` delimiter to use when separating columns (default ',')
+-- * `keysep` separator between key and value pairs (default '=')
+--
+-- @return a table containing items, or `nil`
+-- @return error message (same as @{config.lines}
+function config.read(file,cnfg)
+ local auto
+
+ local iter,err = config.lines(file)
+ if not iter then return nil,err end
+ local line = iter()
+ cnfg = cnfg or {}
+ if cnfg.smart then
+ auto = true
+ if line:match '^[^=]+=' then
+ cnfg.keysep = '='
+ elseif line:match '^[^:]+:' then
+ cnfg.keysep = ':'
+ cnfg.list_delim = ':'
+ elseif line:match '^%S+%s+' then
+ cnfg.keysep = ' '
+ -- more than two columns assume that it's a space-delimited list
+ -- cf /etc/fstab with /etc/ssh/ssh_config
+ if line:match '^%S+%s+%S+%s+%S+' then
+ cnfg.list_delim = ' '
+ end
+ cnfg.variabilize = false
+ end
+ end
+
+
+ local function check_cnfg (var,def)
+ local val = cnfg[var]
+ if val == nil then return def else return val end
+ end
+
+ local initial_digits = '^[%d%+%-]'
+ local t = {}
+ local top_t = t
+ local variabilize = check_cnfg ('variabilize',true)
+ local list_delim = check_cnfg('list_delim',',')
+ local convert_numbers = check_cnfg('convert_numbers',true)
+ local convert_boolean = check_cnfg('convert_boolean',false)
+ local trim_space = check_cnfg('trim_space',true)
+ local trim_quotes = check_cnfg('trim_quotes',false)
+ local ignore_assign = check_cnfg('ignore_assign',false)
+ local keysep = check_cnfg('keysep','=')
+ local keypat = keysep == ' ' and '%s+' or '%s*'..keysep..'%s*'
+ if list_delim == ' ' then list_delim = '%s+' end
+
+ local function process_name(key)
+ if variabilize then
+ key = key:gsub('[^%w]','_')
+ end
+ return key
+ end
+
+ local function process_value(value)
+ if list_delim and value:find(list_delim) then
+ value = split(value,list_delim)
+ for i,v in ipairs(value) do
+ value[i] = process_value(v)
+ end
+ elseif convert_numbers and value:find(initial_digits) then
+ local val = tonumber(value)
+ if not val and value:match ' kB$' then
+ value = value:gsub(' kB','')
+ val = tonumber(value)
+ end
+ if val then value = val end
+ elseif convert_boolean and value == 'true' then
+ return true
+ elseif convert_boolean and value == 'false' then
+ return false
+ end
+ if type(value) == 'string' then
+ if trim_space then value = strip(value) end
+ if not trim_quotes and auto and value:match '^"' then
+ trim_quotes = true
+ end
+ if trim_quotes then value = strip_quotes(value) end
+ end
+ return value
+ end
+
+ while line do
+ if line:find('^%[') then -- section!
+ local section = process_name(line:match('%[([^%]]+)%]'))
+ t = top_t
+ t[section] = {}
+ t = t[section]
+ else
+ line = line:gsub('^%s*','')
+ local i1,i2 = line:find(keypat)
+ if i1 and not ignore_assign then -- key,value assignment
+ local key = process_name(line:sub(1,i1-1))
+ local value = process_value(line:sub(i2+1))
+ t[key] = value
+ else -- a plain list of values...
+ t[#t+1] = process_value(line)
+ end
+ end
+ line = iter()
+ end
+ return top_t
+end
+
+return config
diff --git a/macros/luatex/generic/penlight/pl/data.lua b/macros/luatex/generic/penlight/pl/data.lua
new file mode 100644
index 0000000000..a565ebc9a1
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/data.lua
@@ -0,0 +1,654 @@
+--- Reading and querying simple tabular data.
+--
+-- data.read 'test.txt'
+-- ==> {{10,20},{2,5},{40,50},fieldnames={'x','y'},delim=','}
+--
+-- Provides a way of creating basic SQL-like queries.
+--
+-- require 'pl'
+-- local d = data.read('xyz.txt')
+-- local q = d:select('x,y,z where x > 3 and z < 2 sort by y')
+-- for x,y,z in q do
+-- print(x,y,z)
+-- end
+--
+-- See @{06-data.md.Reading_Columnar_Data|the Guide}
+--
+-- Dependencies: `pl.utils`, `pl.array2d` (fallback methods)
+-- @module pl.data
+
+local utils = require 'pl.utils'
+local _DEBUG = rawget(_G,'_DEBUG')
+
+local patterns,function_arg,usplit,array_tostring = utils.patterns,utils.function_arg,utils.split,utils.array_tostring
+local append,concat = table.insert,table.concat
+local gsub = string.gsub
+local io = io
+local _G,print,type,tonumber,ipairs,setmetatable = _G,print,type,tonumber,ipairs,setmetatable
+
+
+local data = {}
+
+local parse_select
+
+local function rstrip(s)
+ return (s:gsub('%s+$',''))
+end
+
+local function strip (s)
+ return (rstrip(s):gsub('^%s*',''))
+end
+
+-- This gives `l` the standard List metatable,
+-- pulling in the List module.
+local function makelist(l)
+ return setmetatable(l, require('pl.List'))
+end
+
+local function map(fun,t)
+ local res = {}
+ for i = 1,#t do
+ res[i] = fun(t[i])
+ end
+ return res
+end
+
+local function split(line,delim,csv,n)
+ local massage
+ -- CSV fields may be double-quoted and may contain commas!
+ if csv and line:match '"' then
+ line = line:gsub('"([^"]+)"',function(str)
+ local s,cnt = str:gsub(',','\001')
+ if cnt > 0 then massage = true end
+ return s
+ end)
+ if massage then
+ massage = function(s) return (s:gsub('\001',',')) end
+ end
+ end
+ local res = (usplit(line,delim,false,n))
+ if csv then
+ -- restore CSV commas-in-fields
+ if massage then res = map(massage,res) end
+ -- in CSV mode trailiing commas are significant!
+ if line:match ',$' then append(res,'') end
+ end
+ return makelist(res)
+end
+
+local function find(t,v)
+ for i = 1,#t do
+ if v == t[i] then return i end
+ end
+end
+
+local DataMT = {
+ column_by_name = function(self,name)
+ if type(name) == 'number' then
+ name = '$'..name
+ end
+ local arr = {}
+ for res in data.query(self,name) do
+ append(arr,res)
+ end
+ return makelist(arr)
+ end,
+
+ copy_select = function(self,condn)
+ condn = parse_select(condn,self)
+ local iter = data.query(self,condn)
+ local res = {}
+ local row = makelist{iter()}
+ while #row > 0 do
+ append(res,row)
+ row = makelist{iter()}
+ end
+ res.delim = self.delim
+ return data.new(res,split(condn.fields,','))
+ end,
+
+ column_names = function(self)
+ return self.fieldnames
+ end,
+}
+
+local array2d
+
+DataMT.__index = function(self,name)
+ local f = DataMT[name]
+ if f then return f end
+ if not array2d then
+ array2d = require 'pl.array2d'
+ end
+ return array2d[name]
+end
+
+--- return a particular column as a list of values (method).
+-- @param name either name of column, or numerical index.
+-- @function Data.column_by_name
+
+--- return a query iterator on this data (method).
+-- @string condn the query expression
+-- @function Data.select
+-- @see data.query
+
+--- return a row iterator on this data (method).
+-- @string condn the query expression
+-- @function Data.select_row
+
+--- return a new data object based on this query (method).
+-- @string condn the query expression
+-- @function Data.copy_select
+
+--- return the field names of this data object (method).
+-- @function Data.column_names
+
+--- write out a row (method).
+-- @param f file-like object
+-- @function Data.write_row
+
+--- write data out to file (method).
+-- @param f file-like object
+-- @function Data.write
+
+
+-- [guessing delimiter] We check for comma, tab and spaces in that order.
+-- [issue] any other delimiters to be checked?
+local delims = {',', '\t', ' ', ';'}
+
+local function guess_delim (line)
+ if line=='' then return ' ' end
+ for _,delim in ipairs(delims) do
+ if line:find(delim) then
+ return delim == ' ' and '%s+' or delim
+ end
+ end
+ return ' '
+end
+
+-- [file parameter] If it's a string, we try open as a filename. If nil, then
+-- either stdin or stdout depending on the mode. Otherwise, check if this is
+-- a file-like object (implements read or write depending)
+local function open_file (f,mode)
+ local opened, err
+ local reading = mode == 'r'
+ if type(f) == 'string' then
+ if f == 'stdin' then
+ f = io.stdin
+ elseif f == 'stdout' then
+ f = io.stdout
+ else
+ f,err = io.open(f,mode)
+ if not f then return nil,err end
+ opened = true
+ end
+ end
+ if f and ((reading and not f.read) or (not reading and not f.write)) then
+ return nil, "not a file-like object"
+ end
+ return f,nil,opened
+end
+
+--- read a delimited file in a Lua table.
+-- By default, attempts to treat first line as separated list of fieldnames.
+-- @param file a filename or a file-like object
+-- @tab cnfg parsing options
+-- @string cnfg.delim a string pattern to split fields
+-- @array cnfg.fieldnames (i.e. don't read from first line)
+-- @bool cnfg.no_convert (default is to try conversion on first data line)
+-- @tab cnfg.convert table of custom conversion functions with column keys
+-- @int cnfg.numfields indices of columns known to be numbers
+-- @bool cnfg.last_field_collect only split as many fields as fieldnames.
+-- @int cnfg.thousands_dot thousands separator in Excel CSV is '.'
+-- @bool cnfg.csv fields may be double-quoted and contain commas;
+-- Also, empty fields are considered to be equivalent to zero.
+-- @return `data` object, or `nil`
+-- @return error message. May be a file error, 'not a file-like object'
+-- or a conversion error
+function data.read(file,cnfg)
+ local count,line
+ local D = {}
+ if not cnfg then cnfg = {} end
+ local f,err,opened = open_file(file,'r')
+ if not f then return nil, err end
+ local thousands_dot = cnfg.thousands_dot
+ local csv = cnfg.csv
+ if csv then cnfg.delim = ',' end
+
+ -- note that using dot as the thousands separator (@thousands_dot)
+ -- requires a special conversion function! For CSV, _empty fields_ are
+ -- considered to default to numerial zeroes.
+ local tonumber = tonumber
+ local function try_number(x)
+ if thousands_dot then x = x:gsub('%.(...)','%1') end
+ if csv and x == '' then x = '0' end
+ local v = tonumber(x)
+ if v == nil then return nil,"not a number" end
+ return v
+ end
+
+ count = 1
+ line = f:read()
+ if not line then return nil, "empty file" end
+
+ -- first question: what is the delimiter?
+ D.delim = cnfg.delim and cnfg.delim or guess_delim(line)
+ local delim = D.delim
+
+ local conversion
+ local numfields = {}
+ local function append_conversion (idx,conv)
+ conversion = conversion or {}
+ append(numfields,idx)
+ append(conversion,conv)
+ end
+ if cnfg.numfields then
+ for _,n in ipairs(cnfg.numfields) do append_conversion(n,try_number) end
+ end
+
+ -- some space-delimited data starts with a space. This should not be a column,
+ -- although it certainly would be for comma-separated, etc.
+ local stripper
+ if delim == '%s+' and line:find(delim) == 1 then
+ stripper = function(s) return s:gsub('^%s+','') end
+ line = stripper(line)
+ end
+ -- first line will usually be field names. Unless fieldnames are specified,
+ -- we check if it contains purely numerical values for the case of reading
+ -- plain data files.
+ if not cnfg.fieldnames then
+ local fields,nums
+ fields = split(line,delim,csv)
+ if not cnfg.convert then
+ nums = map(tonumber,fields)
+ if #nums == #fields then -- they're ALL numbers!
+ append(D,nums) -- add the first converted row
+ -- and specify conversions for subsequent rows
+ for i = 1,#nums do append_conversion(i,try_number) end
+ else -- we'll try to check numbers just now..
+ nums = nil
+ end
+ else -- [explicit column conversions] (any deduced number conversions will be added)
+ for idx,conv in pairs(cnfg.convert) do append_conversion(idx,conv) end
+ end
+ if nums == nil then
+ cnfg.fieldnames = fields
+ end
+ line = f:read()
+ count = count + 1
+ if stripper then line = stripper(line) end
+ elseif type(cnfg.fieldnames) == 'string' then
+ cnfg.fieldnames = split(cnfg.fieldnames,delim,csv)
+ end
+ local nfields
+ -- at this point, the column headers have been read in. If the first
+ -- row consisted of numbers, it has already been added to the dataset.
+ if cnfg.fieldnames then
+ D.fieldnames = cnfg.fieldnames
+ -- [collecting end field] If @last_field_collect then we'll
+ -- only split as many fields as there are fieldnames
+ if cnfg.last_field_collect then
+ nfields = #D.fieldnames
+ end
+ -- [implicit column conversion] unless @no_convert, we need the numerical field indices
+ -- of the first data row. These can also be specified explicitly by @numfields.
+ if not cnfg.no_convert then
+ local fields = split(line,D.delim,csv,nfields)
+ for i = 1,#fields do
+ if not find(numfields,i) and try_number(fields[i]) then
+ append_conversion(i,try_number)
+ end
+ end
+ end
+ end
+ -- keep going until finished
+ while line do
+ if not line:find ('^%s*$') then -- [blank lines] ignore them!
+ if stripper then line = stripper(line) end
+ local fields = split(line,delim,csv,nfields)
+ if conversion then -- there were field conversions...
+ for k = 1,#numfields do
+ local i,conv = numfields[k],conversion[k]
+ local val,err = conv(fields[i])
+ if val == nil then
+ return nil, err..": "..fields[i].." at line "..count
+ else
+ fields[i] = val
+ end
+ end
+ end
+ append(D,fields)
+ end
+ line = f:read()
+ count = count + 1
+ end
+ if opened then f:close() end
+ if delim == '%s+' then D.delim = ' ' end
+ if not D.fieldnames then D.fieldnames = {} end
+ return data.new(D)
+end
+
+local function write_row (data,f,row,delim)
+ data.temp = array_tostring(row,data.temp)
+ f:write(concat(data.temp,delim),'\n')
+end
+
+function DataMT:write_row(f,row)
+ write_row(self,f,row,self.delim)
+end
+
+--- write 2D data to a file.
+-- Does not assume that the data has actually been
+-- generated with `new` or `read`.
+-- @param data 2D array
+-- @param file filename or file-like object
+-- @tparam[opt] {string} fieldnames list of fields (optional)
+-- @string[opt='\t'] delim delimiter (default tab)
+-- @return true or nil, error
+function data.write (data,file,fieldnames,delim)
+ local f,err,opened = open_file(file,'w')
+ if not f then return nil, err end
+ if not fieldnames then
+ fieldnames = data.fieldnames
+ end
+ delim = delim or '\t'
+ if fieldnames and #fieldnames > 0 then
+ f:write(concat(fieldnames,delim),'\n')
+ end
+ for i = 1,#data do
+ write_row(data,f,data[i],delim)
+ end
+ if opened then f:close() end
+ return true
+end
+
+
+function DataMT:write(file)
+ data.write(self,file,self.fieldnames,self.delim)
+end
+
+local function massage_fieldnames (fields,copy)
+ -- fieldnames must be valid Lua identifiers; ignore any surrounding padding
+ -- but keep the original fieldnames...
+ for i = 1,#fields do
+ local f = strip(fields[i])
+ copy[i] = f
+ fields[i] = f:gsub('%W','_')
+ end
+end
+
+--- create a new dataset from a table of rows.
+-- Can specify the fieldnames, else the table must have a field called
+-- 'fieldnames', which is either a string of delimiter-separated names,
+-- or a table of names. <br>
+-- If the table does not have a field called 'delim', then an attempt will be
+-- made to guess it from the fieldnames string, defaults otherwise to tab.
+-- @param d the table.
+-- @tparam[opt] {string} fieldnames optional fieldnames
+-- @return the table.
+function data.new (d,fieldnames)
+ d.fieldnames = d.fieldnames or fieldnames or ''
+ if not d.delim and type(d.fieldnames) == 'string' then
+ d.delim = guess_delim(d.fieldnames)
+ d.fieldnames = split(d.fieldnames,d.delim)
+ end
+ d.fieldnames = makelist(d.fieldnames)
+ d.original_fieldnames = {}
+ massage_fieldnames(d.fieldnames,d.original_fieldnames)
+ setmetatable(d,DataMT)
+ -- a query with just the fieldname will return a sequence
+ -- of values, which seq.copy turns into a table.
+ return d
+end
+
+local sorted_query = [[
+return function (t)
+ local i = 0
+ local v
+ local ls = {}
+ for i,v in ipairs(t) do
+ if CONDITION then
+ ls[#ls+1] = v
+ end
+ end
+ table.sort(ls,function(v1,v2)
+ return SORT_EXPR
+ end)
+ local n = #ls
+ return function()
+ i = i + 1
+ v = ls[i]
+ if i > n then return end
+ return FIELDLIST
+ end
+end
+]]
+
+-- question: is this optimized case actually worth the extra code?
+local simple_query = [[
+return function (t)
+ local n = #t
+ local i = 0
+ local v
+ return function()
+ repeat
+ i = i + 1
+ v = t[i]
+ until i > n or CONDITION
+ if i > n then return end
+ return FIELDLIST
+ end
+end
+]]
+
+local function is_string (s)
+ return type(s) == 'string'
+end
+
+local field_error
+
+local function fieldnames_as_string (data)
+ return concat(data.fieldnames,',')
+end
+
+local function massage_fields(data,f)
+ local idx
+ if f:find '^%d+$' then
+ idx = tonumber(f)
+ else
+ idx = find(data.fieldnames,f)
+ end
+ if idx then
+ return 'v['..idx..']'
+ else
+ field_error = f..' not found in '..fieldnames_as_string(data)
+ return f
+ end
+end
+
+
+local function process_select (data,parms)
+ --- preparing fields ----
+ field_error = nil
+ local fields = parms.fields
+ local numfields = fields:find '%$' or #data.fieldnames == 0
+ if fields:find '^%s*%*%s*' then
+ if not numfields then
+ fields = fieldnames_as_string(data)
+ else
+ local ncol = #data[1]
+ fields = {}
+ for i = 1,ncol do append(fields,'$'..i) end
+ fields = concat(fields,',')
+ end
+ end
+ local idpat = patterns.IDEN
+ if numfields then
+ idpat = '%$(%d+)'
+ else
+ -- massage field names to replace non-identifier chars
+ fields = rstrip(fields):gsub('[^,%w]','_')
+ end
+ local massage_fields = utils.bind1(massage_fields,data)
+ local ret = gsub(fields,idpat,massage_fields)
+ if field_error then return nil,field_error end
+ parms.fields = fields
+ parms.proc_fields = ret
+ parms.where = parms.where or 'true'
+ if is_string(parms.where) then
+ parms.where = gsub(parms.where,idpat,massage_fields)
+ field_error = nil
+ end
+ return true
+end
+
+
+parse_select = function(s,data)
+ local endp
+ local parms = {}
+ local w1,w2 = s:find('where ')
+ local s1,s2 = s:find('sort by ')
+ if w1 then -- where clause!
+ endp = (s1 or 0)-1
+ parms.where = s:sub(w2+1,endp)
+ end
+ if s1 then -- sort by clause (must be last!)
+ parms.sort_by = s:sub(s2+1)
+ end
+ endp = (w1 or s1 or 0)-1
+ parms.fields = s:sub(1,endp)
+ local status,err = process_select(data,parms)
+ if not status then return nil,err
+ else return parms end
+end
+
+--- create a query iterator from a select string.
+-- Select string has this format: <br>
+-- FIELDLIST [ where LUA-CONDN [ sort by FIELD] ]<br>
+-- FIELDLIST is a comma-separated list of valid fields, or '*'. <br> <br>
+-- The condition can also be a table, with fields 'fields' (comma-sep string or
+-- table), 'sort_by' (string) and 'where' (Lua expression string or function)
+-- @param data table produced by read
+-- @param condn select string or table
+-- @param context a list of tables to be searched when resolving functions
+-- @param return_row if true, wrap the results in a row table
+-- @return an iterator over the specified fields, or nil
+-- @return an error message
+function data.query(data,condn,context,return_row)
+ local err
+ if is_string(condn) then
+ condn,err = parse_select(condn,data)
+ if not condn then return nil,err end
+ elseif type(condn) == 'table' then
+ if type(condn.fields) == 'table' then
+ condn.fields = concat(condn.fields,',')
+ end
+ if not condn.proc_fields then
+ local status,err = process_select(data,condn)
+ if not status then return nil,err end
+ end
+ else
+ return nil, "condition must be a string or a table"
+ end
+ local query
+ if condn.sort_by then -- use sorted_query
+ query = sorted_query
+ else
+ query = simple_query
+ end
+ local fields = condn.proc_fields or condn.fields
+ if return_row then
+ fields = '{'..fields..'}'
+ end
+ query = query:gsub('FIELDLIST',fields)
+ if is_string(condn.where) then
+ query = query:gsub('CONDITION',condn.where)
+ condn.where = nil
+ else
+ query = query:gsub('CONDITION','_condn(v)')
+ condn.where = function_arg(0,condn.where,'condition.where must be callable')
+ end
+ if condn.sort_by then
+ local expr,sort_var,sort_dir
+ local sort_by = condn.sort_by
+ local i1,i2 = sort_by:find('%s+')
+ if i1 then
+ sort_var,sort_dir = sort_by:sub(1,i1-1),sort_by:sub(i2+1)
+ else
+ sort_var = sort_by
+ sort_dir = 'asc'
+ end
+ if sort_var:match '^%$' then sort_var = sort_var:sub(2) end
+ sort_var = massage_fields(data,sort_var)
+ if field_error then return nil,field_error end
+ if sort_dir == 'asc' then
+ sort_dir = '<'
+ else
+ sort_dir = '>'
+ end
+ expr = ('%s %s %s'):format(sort_var:gsub('v','v1'),sort_dir,sort_var:gsub('v','v2'))
+ query = query:gsub('SORT_EXPR',expr)
+ end
+ if condn.where then
+ query = 'return function(_condn) '..query..' end'
+ end
+ if _DEBUG then print(query) end
+
+ local fn,err = utils.load(query,'tmp')
+ if not fn then return nil,err end
+ fn = fn() -- get the function
+ if condn.where then
+ fn = fn(condn.where)
+ end
+ local qfun = fn(data)
+ if context then
+ -- [specifying context for condition] @context is a list of tables which are
+ -- 'injected'into the condition's custom context
+ append(context,_G)
+ local lookup = {}
+ utils.setfenv(qfun,lookup)
+ setmetatable(lookup,{
+ __index = function(tbl,key)
+ -- _G.print(tbl,key)
+ for k,t in ipairs(context) do
+ if t[key] then return t[key] end
+ end
+ end
+ })
+ end
+ return qfun
+end
+
+
+DataMT.select = data.query
+DataMT.select_row = function(d,condn,context)
+ return data.query(d,condn,context,true)
+end
+
+--- Filter input using a query.
+-- @string Q a query string
+-- @param infile filename or file-like object
+-- @param outfile filename or file-like object
+-- @bool dont_fail true if you want to return an error, not just fail
+function data.filter (Q,infile,outfile,dont_fail)
+ local d = data.read(infile or 'stdin')
+ local out = open_file(outfile or 'stdout')
+ local iter,err = d:select(Q)
+ local delim = d.delim
+ if not iter then
+ err = 'error: '..err
+ if dont_fail then
+ return nil,err
+ else
+ utils.quit(1,err)
+ end
+ end
+ while true do
+ local res = {iter()}
+ if #res == 0 then break end
+ out:write(concat(res,delim),'\n')
+ end
+end
+
+return data
+
diff --git a/macros/luatex/generic/penlight/pl/dir.lua b/macros/luatex/generic/penlight/pl/dir.lua
new file mode 100644
index 0000000000..b19c8fc440
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/dir.lua
@@ -0,0 +1,524 @@
+--- Listing files in directories and creating/removing directory paths.
+--
+-- Dependencies: `pl.utils`, `pl.path`
+--
+-- Soft Dependencies: `alien`, `ffi` (either are used on Windows for copying/moving files)
+-- @module pl.dir
+
+local utils = require 'pl.utils'
+local path = require 'pl.path'
+local is_windows = path.is_windows
+local ldir = path.dir
+local mkdir = path.mkdir
+local rmdir = path.rmdir
+local sub = string.sub
+local os,pcall,ipairs,pairs,require,setmetatable = os,pcall,ipairs,pairs,require,setmetatable
+local remove = os.remove
+local append = table.insert
+local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise
+
+local exists, isdir = path.exists, path.isdir
+local sep = path.sep
+
+local dir = {}
+
+local function makelist(l)
+ return setmetatable(l, require('pl.List'))
+end
+
+local function assert_dir (n,val)
+ assert_arg(n,val,'string',path.isdir,'not a directory',4)
+end
+
+local function filemask(mask)
+ mask = utils.escape(path.normcase(mask))
+ return '^'..mask:gsub('%%%*','.*'):gsub('%%%?','.')..'$'
+end
+
+--- Test whether a file name matches a shell pattern.
+-- Both parameters are case-normalized if operating system is
+-- case-insensitive.
+-- @string filename A file name.
+-- @string pattern A shell pattern. The only special characters are
+-- `'*'` and `'?'`: `'*'` matches any sequence of characters and
+-- `'?'` matches any single character.
+-- @treturn bool
+-- @raise dir and mask must be strings
+function dir.fnmatch(filename,pattern)
+ assert_string(1,filename)
+ assert_string(2,pattern)
+ return path.normcase(filename):find(filemask(pattern)) ~= nil
+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.
+-- @treturn List(string) List of matching file names.
+-- @raise dir and mask must be strings
+function dir.filter(filenames,pattern)
+ assert_arg(1,filenames,'table')
+ assert_string(2,pattern)
+ local res = {}
+ local mask = filemask(pattern)
+ for i,f in ipairs(filenames) do
+ if path.normcase(f):find(mask) then append(res,f) end
+ end
+ return makelist(res)
+end
+
+local function _listfiles(dirname,filemode,match)
+ local res = {}
+ local check = utils.choose(filemode,path.isfile,path.isdir)
+ if not dirname then dirname = '.' end
+ for f in ldir(dirname) do
+ if f ~= '.' and f ~= '..' then
+ local p = path.join(dirname,f)
+ if check(p) and (not match or match(f)) then
+ append(res,p)
+ end
+ end
+ end
+ return makelist(res)
+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.
+-- @treturn {string} list of files
+-- @raise dirname and mask must be strings
+function dir.getfiles(dirname,mask)
+ assert_dir(1,dirname)
+ if mask then assert_string(2,mask) end
+ local match
+ if mask then
+ mask = filemask(mask)
+ match = function(f)
+ return path.normcase(f):find(mask)
+ end
+ end
+ return _listfiles(dirname,true,match)
+end
+
+--- return a list of all subdirectories of the directory.
+-- @string dirname A directory
+-- @treturn {string} a list of directories
+-- @raise dir must be a a valid directory
+function dir.getdirectories(dirname)
+ assert_dir(1,dirname)
+ return _listfiles(dirname,false)
+end
+
+local alien,ffi,ffi_checked,CopyFile,MoveFile,GetLastError,win32_errors,cmd_tmpfile
+
+local function execute_command(cmd,parms)
+ if not cmd_tmpfile then cmd_tmpfile = path.tmpname () end
+ local err = path.is_windows and ' > ' or ' 2> '
+ cmd = cmd..' '..parms..err..utils.quote_arg(cmd_tmpfile)
+ local ret = utils.execute(cmd)
+ if not ret then
+ local err = (utils.readfile(cmd_tmpfile):gsub('\n(.*)',''))
+ remove(cmd_tmpfile)
+ return false,err
+ else
+ remove(cmd_tmpfile)
+ return true
+ end
+end
+
+local function find_ffi_copyfile ()
+ if not ffi_checked then
+ ffi_checked = true
+ local res
+ res,alien = pcall(require,'alien')
+ if not res then
+ alien = nil
+ res, ffi = pcall(require,'ffi')
+ end
+ if not res then
+ ffi = nil
+ return
+ end
+ else
+ return
+ end
+ if alien then
+ -- register the Win32 CopyFile and MoveFile functions
+ local kernel = alien.load('kernel32.dll')
+ CopyFile = kernel.CopyFileA
+ CopyFile:types{'string','string','int',ret='int',abi='stdcall'}
+ MoveFile = kernel.MoveFileA
+ MoveFile:types{'string','string',ret='int',abi='stdcall'}
+ GetLastError = kernel.GetLastError
+ GetLastError:types{ret ='int', abi='stdcall'}
+ elseif ffi then
+ ffi.cdef [[
+ int CopyFileA(const char *src, const char *dest, int iovr);
+ int MoveFileA(const char *src, const char *dest);
+ int GetLastError();
+ ]]
+ CopyFile = ffi.C.CopyFileA
+ MoveFile = ffi.C.MoveFileA
+ GetLastError = ffi.C.GetLastError
+ end
+ win32_errors = {
+ ERROR_FILE_NOT_FOUND = 2,
+ ERROR_PATH_NOT_FOUND = 3,
+ ERROR_ACCESS_DENIED = 5,
+ ERROR_WRITE_PROTECT = 19,
+ ERROR_BAD_UNIT = 20,
+ ERROR_NOT_READY = 21,
+ ERROR_WRITE_FAULT = 29,
+ ERROR_READ_FAULT = 30,
+ ERROR_SHARING_VIOLATION = 32,
+ ERROR_LOCK_VIOLATION = 33,
+ ERROR_HANDLE_DISK_FULL = 39,
+ ERROR_BAD_NETPATH = 53,
+ ERROR_NETWORK_BUSY = 54,
+ ERROR_DEV_NOT_EXIST = 55,
+ ERROR_FILE_EXISTS = 80,
+ ERROR_OPEN_FAILED = 110,
+ ERROR_INVALID_NAME = 123,
+ ERROR_BAD_PATHNAME = 161,
+ ERROR_ALREADY_EXISTS = 183,
+ }
+end
+
+local function two_arguments (f1,f2)
+ return utils.quote_arg(f1)..' '..utils.quote_arg(f2)
+end
+
+local function file_op (is_copy,src,dest,flag)
+ if flag == 1 and path.exists(dest) then
+ return false,"cannot overwrite destination"
+ end
+ if is_windows then
+ -- if we haven't tried to load Alien/LuaJIT FFI before, then do so
+ find_ffi_copyfile()
+ -- fallback if there's no Alien, just use DOS commands *shudder*
+ -- 'rename' involves a copy and then deleting the source.
+ if not CopyFile then
+ if path.is_windows then
+ src = src:gsub("/","\\")
+ dest = dest:gsub("/","\\")
+ end
+ local res, err = execute_command('copy',two_arguments(src,dest))
+ if not res then return false,err end
+ if not is_copy then
+ return execute_command('del',utils.quote_arg(src))
+ end
+ return true
+ else
+ if path.isdir(dest) then
+ dest = path.join(dest,path.basename(src))
+ end
+ local ret
+ if is_copy then ret = CopyFile(src,dest,flag)
+ else ret = MoveFile(src,dest) end
+ if ret == 0 then
+ local err = GetLastError()
+ for name,value in pairs(win32_errors) do
+ if value == err then return false,name end
+ end
+ return false,"Error #"..err
+ else return true
+ end
+ end
+ else -- for Unix, just use cp for now
+ return execute_command(is_copy and 'cp' or 'mv',
+ two_arguments(src,dest))
+ end
+end
+
+--- copy a file.
+-- @string src source file
+-- @string dest destination file or directory
+-- @bool flag true if you want to force the copy (default)
+-- @treturn bool operation succeeded
+-- @raise src and dest must be strings
+function dir.copyfile (src,dest,flag)
+ assert_string(1,src)
+ assert_string(2,dest)
+ flag = flag==nil or flag
+ return file_op(true,src,dest,flag and 0 or 1)
+end
+
+--- move a file.
+-- @string src source file
+-- @string dest destination file or directory
+-- @treturn bool operation succeeded
+-- @raise src and dest must be strings
+function dir.movefile (src,dest)
+ assert_string(1,src)
+ assert_string(2,dest)
+ return file_op(false,src,dest,0)
+end
+
+local function _dirfiles(dirname,attrib)
+ local dirs = {}
+ local files = {}
+ for f in ldir(dirname) do
+ if f ~= '.' and f ~= '..' then
+ local p = path.join(dirname,f)
+ local mode = attrib(p,'mode')
+ if mode=='directory' then
+ append(dirs,f)
+ else
+ append(files,f)
+ end
+ end
+ end
+ return makelist(dirs), makelist(files)
+end
+
+
+--- return an iterator which walks through a directory tree starting at root.
+-- The iterator returns (root,dirs,files)
+-- Note that dirs and files are lists of names (i.e. you must say path.join(root,d)
+-- to get the actual full path)
+-- If bottom_up is false (or not present), then the entries at the current level are returned
+-- before we go deeper. This means that you can modify the returned list of directories before
+-- continuing.
+-- This is a clone of os.walk from the Python libraries.
+-- @string root A starting directory
+-- @bool bottom_up False if we start listing entries immediately.
+-- @bool follow_links follow symbolic links
+-- @return an iterator returning root,dirs,files
+-- @raise root must be a directory
+function dir.walk(root,bottom_up,follow_links)
+ assert_dir(1,root)
+ local attrib
+ if path.is_windows or not follow_links then
+ attrib = path.attrib
+ else
+ attrib = path.link_attrib
+ end
+
+ local to_scan = { root }
+ local to_return = {}
+ local iter = function()
+ while #to_scan > 0 do
+ local current_root = table.remove(to_scan)
+ local dirs,files = _dirfiles(current_root, attrib)
+ for _, d in ipairs(dirs) do
+ table.insert(to_scan, current_root..path.sep..d)
+ end
+ if not bottom_up then
+ return current_root, dirs, files
+ else
+ table.insert(to_return, { current_root, dirs, files })
+ end
+ end
+ if #to_return > 0 then
+ return utils.unpack(table.remove(to_return))
+ end
+ end
+
+ return iter
+end
+
+--- remove a whole directory tree.
+-- Symlinks in the tree will be deleted without following them.
+-- @string fullpath A directory path (must be an actual directory, not a symlink)
+-- @return true or nil
+-- @return error if failed
+-- @raise fullpath must be a string
+function dir.rmtree(fullpath)
+ assert_dir(1,fullpath)
+ if path.islink(fullpath) then return false,'will not follow symlink' end
+ for root,dirs,files in dir.walk(fullpath,true) do
+ if path.islink(root) then
+ -- sub dir is a link, remove link, do not follow
+ if is_windows then
+ -- Windows requires using "rmdir". Deleting the link like a file
+ -- will instead delete all files from the target directory!!
+ local res, err = rmdir(root)
+ if not res then return nil,err .. ": " .. root end
+ else
+ local res, err = remove(root)
+ if not res then return nil,err .. ": " .. root end
+ end
+ else
+ for i,f in ipairs(files) do
+ local res, err = remove(path.join(root,f))
+ if not res then return nil,err .. ": " .. path.join(root,f) end
+ end
+ local res, err = rmdir(root)
+ if not res then return nil,err .. ": " .. root end
+ end
+ end
+ return true
+end
+
+
+do
+ local dirpat
+ if path.is_windows then
+ dirpat = '(.+)\\[^\\]+$'
+ else
+ dirpat = '(.+)/[^/]+$'
+ end
+
+ local _makepath
+ function _makepath(p)
+ -- windows root drive case
+ if p:find '^%a:[\\]*$' then
+ return true
+ end
+ if not path.isdir(p) then
+ local subp = p:match(dirpat)
+ if subp then
+ local ok, err = _makepath(subp)
+ if not ok then return nil, err end
+ end
+ return mkdir(p)
+ else
+ return true
+ end
+ end
+
+ --- create a directory path.
+ -- This will create subdirectories as necessary!
+ -- @string p A directory path
+ -- @return true on success, nil + errormsg on failure
+ -- @raise failure to create
+ function dir.makepath (p)
+ assert_string(1,p)
+ if path.is_windows then
+ p = p:gsub("/", "\\")
+ end
+ return _makepath(path.abspath(p))
+ end
+end
+
+--- clone a directory tree. Will always try to create a new directory structure
+-- if necessary.
+-- @string path1 the base path of the source tree
+-- @string path2 the new base path for the destination
+-- @func file_fun an optional function to apply on all files
+-- @bool verbose an optional boolean to control the verbosity of the output.
+-- It can also be a logging function that behaves like print()
+-- @return true, or nil
+-- @return error message, or list of failed directory creations
+-- @return list of failed file operations
+-- @raise path1 and path2 must be strings
+-- @usage clonetree('.','../backup',copyfile)
+function dir.clonetree (path1,path2,file_fun,verbose)
+ assert_string(1,path1)
+ assert_string(2,path2)
+ if verbose == true then verbose = print end
+ local abspath,normcase,isdir,join = path.abspath,path.normcase,path.isdir,path.join
+ local faildirs,failfiles = {},{}
+ if not isdir(path1) then return raise 'source is not a valid directory' end
+ path1 = abspath(normcase(path1))
+ path2 = abspath(normcase(path2))
+ if verbose then verbose('normalized:',path1,path2) end
+ -- particularly NB that the new path isn't fully contained in the old path
+ if path1 == path2 then return raise "paths are the same" end
+ local _,i2 = path2:find(path1,1,true)
+ if i2 == #path1 and path2:sub(i2+1,i2+1) == path.sep then
+ return raise 'destination is a subdirectory of the source'
+ end
+ local cp = path.common_prefix (path1,path2)
+ local idx = #cp
+ if idx == 0 then -- no common path, but watch out for Windows paths!
+ if path1:sub(2,2) == ':' then idx = 3 end
+ end
+ for root,dirs,files in dir.walk(path1) do
+ local opath = path2..root:sub(idx)
+ if verbose then verbose('paths:',opath,root) end
+ if not isdir(opath) then
+ local ret = dir.makepath(opath)
+ if not ret then append(faildirs,opath) end
+ if verbose then verbose('creating:',opath,ret) end
+ end
+ if file_fun then
+ for i,f in ipairs(files) do
+ local p1 = join(root,f)
+ local p2 = join(opath,f)
+ local ret = file_fun(p1,p2)
+ if not ret then append(failfiles,p2) end
+ if verbose then
+ verbose('files:',p1,p2,ret)
+ end
+ end
+ end
+ end
+ return true,faildirs,failfiles
+end
+
+
+-- each entry of the stack is an array with three items:
+-- 1. the name of the directory
+-- 2. the lfs iterator function
+-- 3. the lfs iterator userdata
+local function treeiter(iterstack)
+ local diriter = iterstack[#iterstack]
+ if not diriter then
+ return -- done
+ end
+
+ local dirname = diriter[1]
+ local entry = diriter[2](diriter[3])
+ if not entry then
+ table.remove(iterstack)
+ return treeiter(iterstack) -- tail-call to try next
+ end
+
+ if entry ~= "." and entry ~= ".." then
+ entry = dirname .. sep .. entry
+ if exists(entry) then -- Just in case a symlink is broken.
+ local is_dir = isdir(entry)
+ if is_dir then
+ table.insert(iterstack, { entry, ldir(entry) })
+ end
+ return entry, is_dir
+ end
+ end
+
+ return treeiter(iterstack) -- tail-call to try next
+end
+
+
+--- return an iterator over all entries in a directory tree
+-- @string d a directory
+-- @return an iterator giving pathname and mode (true for dir, false otherwise)
+-- @raise d must be a non-empty string
+function dir.dirtree( d )
+ assert( d and d ~= "", "directory parameter is missing or empty" )
+
+ local last = sub ( d, -1 )
+ if last == sep or last == '/' then
+ d = sub( d, 1, -2 )
+ end
+
+ local iterstack = { {d, ldir(d)} }
+
+ return treeiter, iterstack
+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_.
+-- @raise start_path must be a directory
+function dir.getallfiles( start_path, shell_pattern )
+ assert_dir(1,start_path)
+ shell_pattern = shell_pattern or "*"
+
+ local files = {}
+ local normcase = path.normcase
+ for filename, mode in dir.dirtree( start_path ) do
+ if not mode then
+ local mask = filemask( shell_pattern )
+ if normcase(filename):find( mask ) then
+ files[#files + 1] = filename
+ end
+ end
+ end
+
+ return makelist(files)
+end
+
+return dir
diff --git a/macros/luatex/generic/penlight/pl/file.lua b/macros/luatex/generic/penlight/pl/file.lua
new file mode 100644
index 0000000000..b8058c412c
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/file.lua
@@ -0,0 +1,55 @@
+--- File manipulation functions: reading, writing, moving and copying.
+--
+-- This module wraps a number of functions from other modules into a
+-- file related module for convenience.
+--
+-- Dependencies: `pl.utils`, `pl.dir`, `pl.path`
+-- @module pl.file
+local os = os
+local utils = require 'pl.utils'
+local dir = require 'pl.dir'
+local path = require 'pl.path'
+
+local file = {}
+
+--- return the contents of a file as a string.
+-- This function is a copy of `utils.readfile`.
+-- @function file.read
+file.read = utils.readfile
+
+--- write a string to a file.
+-- This function is a copy of `utils.writefile`.
+-- @function file.write
+file.write = utils.writefile
+
+--- copy a file.
+-- This function is a copy of `dir.copyfile`.
+-- @function file.copy
+file.copy = dir.copyfile
+
+--- move a file.
+-- This function is a copy of `dir.movefile`.
+-- @function file.move
+file.move = dir.movefile
+
+--- Return the time of last access as the number of seconds since the epoch.
+-- This function is a copy of `path.getatime`.
+-- @function file.access_time
+file.access_time = path.getatime
+
+---Return when the file was created.
+-- This function is a copy of `path.getctime`.
+-- @function file.creation_time
+file.creation_time = path.getctime
+
+--- Return the time of last modification.
+-- This function is a copy of `path.getmtime`.
+-- @function file.modified_time
+file.modified_time = path.getmtime
+
+--- Delete a file.
+-- This function is a copy of `os.remove`.
+-- @function file.delete
+file.delete = os.remove
+
+return file
diff --git a/macros/luatex/generic/penlight/pl/func.lua b/macros/luatex/generic/penlight/pl/func.lua
new file mode 100644
index 0000000000..daac0fce3c
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/func.lua
@@ -0,0 +1,393 @@
+--- Functional helpers like composition, binding and placeholder expressions.
+-- Placeholder expressions are useful for short anonymous functions, and were
+-- inspired by the Boost Lambda library.
+--
+-- > utils.import 'pl.func'
+-- > ls = List{10,20,30}
+-- > = ls:map(_1+1)
+-- {11,21,31}
+--
+-- They can also be used to _bind_ particular arguments of a function.
+--
+-- > p = bind(print,'start>',_0)
+-- > p(10,20,30)
+-- > start> 10 20 30
+--
+-- See @{07-functional.md.Creating_Functions_from_Functions|the Guide}
+--
+-- Dependencies: `pl.utils`, `pl.tablex`
+-- @module pl.func
+local type,setmetatable,getmetatable,rawset = type,setmetatable,getmetatable,rawset
+local concat,append = table.concat,table.insert
+local tostring = tostring
+local utils = require 'pl.utils'
+local pairs,rawget,unpack,pack = pairs,rawget,utils.unpack,utils.pack
+local tablex = require 'pl.tablex'
+local map = tablex.map
+local _DEBUG = rawget(_G,'_DEBUG')
+local assert_arg = utils.assert_arg
+
+local func = {}
+
+-- metatable for Placeholder Expressions (PE)
+local _PEMT = {}
+
+local function P (t)
+ setmetatable(t,_PEMT)
+ return t
+end
+
+func.PE = P
+
+local function isPE (obj)
+ return getmetatable(obj) == _PEMT
+end
+
+func.isPE = isPE
+
+-- construct a placeholder variable (e.g _1 and _2)
+local function PH (idx)
+ return P {op='X',repr='_'..idx, index=idx}
+end
+
+-- construct a constant placeholder variable (e.g _C1 and _C2)
+local function CPH (idx)
+ return P {op='X',repr='_C'..idx, index=idx}
+end
+
+func._1,func._2,func._3,func._4,func._5 = PH(1),PH(2),PH(3),PH(4),PH(5)
+func._0 = P{op='X',repr='...',index=0}
+
+function func.Var (name)
+ local ls = utils.split(name,'[%s,]+')
+ local res = {}
+ for i = 1, #ls do
+ append(res,P{op='X',repr=ls[i],index=0})
+ end
+ return unpack(res)
+end
+
+function func._ (value)
+ return P{op='X',repr=value,index='wrap'}
+end
+
+local repr
+
+func.Nil = func.Var 'nil'
+
+function _PEMT.__index(obj,key)
+ return P{op='[]',obj,key}
+end
+
+function _PEMT.__call(fun,...)
+ return P{op='()',fun,...}
+end
+
+function _PEMT.__tostring (e)
+ return repr(e)
+end
+
+function _PEMT.__unm(arg)
+ return P{op='unm',arg}
+end
+
+function func.Not (arg)
+ return P{op='not',arg}
+end
+
+function func.Len (arg)
+ return P{op='#',arg}
+end
+
+
+local function binreg(context,t)
+ for name,op in pairs(t) do
+ rawset(context,name,function(x,y)
+ return P{op=op,x,y}
+ end)
+ end
+end
+
+local function import_name (name,fun,context)
+ rawset(context,name,function(...)
+ return P{op='()',fun,...}
+ end)
+end
+
+local imported_functions = {}
+
+local function is_global_table (n)
+ return type(_G[n]) == 'table'
+end
+
+--- wrap a table of functions. This makes them available for use in
+-- placeholder expressions.
+-- @string tname a table name
+-- @tab context context to put results, defaults to environment of caller
+function func.import(tname,context)
+ assert_arg(1,tname,'string',is_global_table,'arg# 1: not a name of a global table')
+ local t = _G[tname]
+ context = context or _G
+ for name,fun in pairs(t) do
+ import_name(name,fun,context)
+ imported_functions[fun] = name
+ end
+end
+
+--- register a function for use in placeholder expressions.
+-- @func fun a function
+-- @string[opt] name an optional name
+-- @return a placeholder functiond
+function func.register (fun,name)
+ assert_arg(1,fun,'function')
+ if name then
+ assert_arg(2,name,'string')
+ imported_functions[fun] = name
+ end
+ return function(...)
+ return P{op='()',fun,...}
+ end
+end
+
+function func.lookup_imported_name (fun)
+ return imported_functions[fun]
+end
+
+local function _arg(...) return ... end
+
+function func.Args (...)
+ return P{op='()',_arg,...}
+end
+
+-- binary operators with their precedences (see Lua manual)
+-- precedences might be incremented by one before use depending on
+-- left- or right-associativity, space them out
+local binary_operators = {
+ ['or'] = 0,
+ ['and'] = 2,
+ ['=='] = 4, ['~='] = 4, ['<'] = 4, ['>'] = 4, ['<='] = 4, ['>='] = 4,
+ ['..'] = 6,
+ ['+'] = 8, ['-'] = 8,
+ ['*'] = 10, ['/'] = 10, ['%'] = 10,
+ ['^'] = 14
+}
+
+-- unary operators with their precedences
+local unary_operators = {
+ ['not'] = 12, ['#'] = 12, ['unm'] = 12
+}
+
+-- comparisons (as prefix functions)
+binreg (func,{And='and',Or='or',Eq='==',Lt='<',Gt='>',Le='<=',Ge='>='})
+
+-- standard binary operators (as metamethods)
+binreg (_PEMT,{__add='+',__sub='-',__mul='*',__div='/',__mod='%',__pow='^',__concat='..'})
+
+binreg (_PEMT,{__eq='=='})
+
+--- all elements of a table except the first.
+-- @tab ls a list-like table.
+function func.tail (ls)
+ assert_arg(1,ls,'table')
+ local res = {}
+ for i = 2,#ls do
+ append(res,ls[i])
+ end
+ return res
+end
+
+--- create a string representation of a placeholder expression.
+-- @param e a placeholder expression
+-- @param lastpred not used
+function repr (e,lastpred)
+ local tail = func.tail
+ if isPE(e) then
+ local pred = binary_operators[e.op] or unary_operators[e.op]
+ if pred then
+ -- binary or unary operator
+ local s
+ if binary_operators[e.op] then
+ local left_pred = pred
+ local right_pred = pred
+ if e.op == '..' or e.op == '^' then
+ left_pred = left_pred + 1
+ else
+ right_pred = right_pred + 1
+ end
+ local left_arg = repr(e[1], left_pred)
+ local right_arg = repr(e[2], right_pred)
+ s = left_arg..' '..e.op..' '..right_arg
+ else
+ local op = e.op == 'unm' and '-' or e.op
+ s = op..' '..repr(e[1], pred)
+ end
+ if lastpred and lastpred > pred then
+ s = '('..s..')'
+ end
+ return s
+ else -- either postfix, or a placeholder
+ local ls = map(repr,e)
+ if e.op == '[]' then
+ return ls[1]..'['..ls[2]..']'
+ elseif e.op == '()' then
+ local fn
+ if ls[1] ~= nil then -- was _args, undeclared!
+ fn = ls[1]
+ else
+ fn = ''
+ end
+ return fn..'('..concat(tail(ls),',')..')'
+ else
+ return e.repr
+ end
+ end
+ elseif type(e) == 'string' then
+ return '"'..e..'"'
+ elseif type(e) == 'function' then
+ local name = func.lookup_imported_name(e)
+ if name then return name else return tostring(e) end
+ else
+ return tostring(e) --should not really get here!
+ end
+end
+func.repr = repr
+
+-- collect all the non-PE values in this PE into vlist, and replace each occurence
+-- with a constant PH (_C1, etc). Return the maximum placeholder index found.
+local collect_values
+function collect_values (e,vlist)
+ if isPE(e) then
+ if e.op ~= 'X' then
+ local m = 0
+ for i = 1,#e do
+ local subx = e[i]
+ local pe = isPE(subx)
+ if pe then
+ if subx.op == 'X' and subx.index == 'wrap' then
+ subx = subx.repr
+ pe = false
+ else
+ m = math.max(m,collect_values(subx,vlist))
+ end
+ end
+ if not pe then
+ append(vlist,subx)
+ e[i] = CPH(#vlist)
+ end
+ end
+ return m
+ else -- was a placeholder, it has an index...
+ return e.index
+ end
+ else -- plain value has no placeholder dependence
+ return 0
+ end
+end
+func.collect_values = collect_values
+
+--- instantiate a PE into an actual function. First we find the largest placeholder used,
+-- e.g. _2; from this a list of the formal parameters can be build. Then we collect and replace
+-- any non-PE values from the PE, and build up a constant binding list.
+-- Finally, the expression can be compiled, and e.__PE_function is set.
+-- @param e a placeholder expression
+-- @return a function
+function func.instantiate (e)
+ local consts,values,parms = {},{},{}
+ local rep, err, fun
+ local n = func.collect_values(e,values)
+ for i = 1,#values do
+ append(consts,'_C'..i)
+ if _DEBUG then print(i,values[i]) end
+ end
+ for i =1,n do
+ append(parms,'_'..i)
+ end
+ consts = concat(consts,',')
+ parms = concat(parms,',')
+ rep = repr(e)
+ local fstr = ('return function(%s) return function(%s) return %s end end'):format(consts,parms,rep)
+ if _DEBUG then print(fstr) end
+ fun,err = utils.load(fstr,'fun')
+ if not fun then return nil,err end
+ fun = fun() -- get wrapper
+ fun = fun(unpack(values)) -- call wrapper (values could be empty)
+ e.__PE_function = fun
+ return fun
+end
+
+--- instantiate a PE unless it has already been done.
+-- @param e a placeholder expression
+-- @return the function
+function func.I(e)
+ if rawget(e,'__PE_function') then
+ return e.__PE_function
+ else return func.instantiate(e)
+ end
+end
+
+utils.add_function_factory(_PEMT,func.I)
+
+--- bind the first parameter of the function to a value.
+-- @function func.bind1
+-- @func fn a function of one or more arguments
+-- @param p a value
+-- @return a function of one less argument
+-- @usage (bind1(math.max,10))(20) == math.max(10,20)
+func.bind1 = utils.bind1
+func.curry = func.bind1
+
+--- create a function which chains two functions.
+-- @func f a function of at least one argument
+-- @func g a function of at least one argument
+-- @return a function
+-- @usage printf = compose(io.write,string.format)
+function func.compose (f,g)
+ return function(...) return f(g(...)) end
+end
+
+--- bind the arguments of a function to given values.
+-- `bind(fn,v,_2)` is equivalent to `bind1(fn,v)`.
+-- @func fn a function of at least one argument
+-- @param ... values or placeholder variables
+-- @return a function
+-- @usage (bind(f,_1,a))(b) == f(a,b)
+-- @usage (bind(f,_2,_1))(a,b) == f(b,a)
+function func.bind(fn,...)
+ local args = pack(...)
+ local holders,parms,bvalues,values = {},{},{'fn'},{}
+ local nv,maxplace,varargs = 1,0,false
+ for i = 1,args.n do
+ local a = args[i]
+ if isPE(a) and a.op == 'X' then
+ append(holders,a.repr)
+ maxplace = math.max(maxplace,a.index)
+ if a.index == 0 then varargs = true end
+ else
+ local v = '_v'..nv
+ append(bvalues,v)
+ append(holders,v)
+ append(values,a)
+ nv = nv + 1
+ end
+ end
+ for np = 1,maxplace do
+ append(parms,'_'..np)
+ end
+ if varargs then append(parms,'...') end
+ bvalues = concat(bvalues,',')
+ parms = concat(parms,',')
+ holders = concat(holders,',')
+ local fstr = ([[
+return function (%s)
+ return function(%s) return fn(%s) end
+end
+]]):format(bvalues,parms,holders)
+ if _DEBUG then print(fstr) end
+ local res = utils.load(fstr)
+ res = res()
+ return res(fn,unpack(values))
+end
+
+return func
+
+
diff --git a/macros/luatex/generic/penlight/pl/import_into.lua b/macros/luatex/generic/penlight/pl/import_into.lua
new file mode 100644
index 0000000000..6dd274130a
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/import_into.lua
@@ -0,0 +1,91 @@
+--------------
+-- PL loader, for loading all PL libraries, only on demand.
+-- Whenever a module is implicitly accesssed, the table will have the module automatically injected.
+-- (e.g. `_ENV.tablex`)
+-- then that module is dynamically loaded. The submodules are all brought into
+-- the table that is provided as the argument, or returned in a new table.
+-- If a table is provided, that table's metatable is clobbered, but the values are not.
+-- This module returns a single function, which is passed the environment.
+-- If this is `true`, then return a 'shadow table' as the module
+-- See @{01-introduction.md.To_Inject_or_not_to_Inject_|the Guide}
+
+-- @module pl.import_into
+
+return function(env)
+ local mod
+ if env == true then
+ mod = {}
+ env = {}
+ end
+ local env = env or {}
+
+ local modules = {
+ utils = true,path=true,dir=true,tablex=true,stringio=true,sip=true,
+ input=true,seq=true,lexer=true,stringx=true,
+ config=true,pretty=true,data=true,func=true,text=true,
+ operator=true,lapp=true,array2d=true,
+ comprehension=true,xml=true,types=true,
+ test = true, app = true, file = true, class = true,
+ luabalanced = true, permute = true, template = true,
+ url = true, compat = true,
+ -- classes --
+ List = true, Map = true, Set = true,
+ OrderedMap = true, MultiMap = true, Date = true,
+ }
+ rawset(env,'utils',require 'pl.utils')
+
+ for name,klass in pairs(env.utils.stdmt) do
+ klass.__index = function(t,key)
+ return require ('pl.'..name)[key]
+ end;
+ end
+
+ -- ensure that we play nice with libraries that also attach a metatable
+ -- to the global table; always forward to a custom __index if we don't
+ -- match
+
+ local _hook,_prev_index
+ local gmt = {}
+ local prevenvmt = getmetatable(env)
+ if prevenvmt then
+ _prev_index = prevenvmt.__index
+ if prevenvmt.__newindex then
+ gmt.__index = prevenvmt.__newindex
+ end
+ end
+
+ function gmt.hook(handler)
+ _hook = handler
+ end
+
+ function gmt.__index(t,name)
+ local found = modules[name]
+ -- either true, or the name of the module containing this class.
+ -- either way, we load the required module and make it globally available.
+ if found then
+ -- e..g pretty.dump causes pl.pretty to become available as 'pretty'
+ rawset(env,name,require('pl.'..name))
+ return env[name]
+ else
+ local res
+ if _hook then
+ res = _hook(t,name)
+ if res then return res end
+ end
+ if _prev_index then
+ return _prev_index(t,name)
+ end
+ end
+ end
+
+ if mod then
+ function gmt.__newindex(t,name,value)
+ mod[name] = value
+ rawset(t,name,value)
+ end
+ end
+
+ setmetatable(env,gmt)
+
+ return env,mod or env
+end
diff --git a/macros/luatex/generic/penlight/pl/init.lua b/macros/luatex/generic/penlight/pl/init.lua
new file mode 100644
index 0000000000..c27a890b56
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/init.lua
@@ -0,0 +1,11 @@
+--------------
+-- Entry point for loading all PL libraries only on demand, into the global space.
+-- Requiring 'pl' means that whenever a module is implicitly accesssed
+-- (e.g. `utils.split`)
+-- then that module is dynamically loaded. The submodules are all brought into
+-- the global space.
+--Updated to use @{pl.import_into}
+-- @module pl
+require'pl.import_into'(_G)
+
+if rawget(_G,'PENLIGHT_STRICT') then require 'pl.strict' end
diff --git a/macros/luatex/generic/penlight/pl/input.lua b/macros/luatex/generic/penlight/pl/input.lua
new file mode 100644
index 0000000000..f81de87c4d
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/input.lua
@@ -0,0 +1,171 @@
+--- Iterators for extracting words or numbers from an input source.
+--
+-- require 'pl'
+-- local total,n = seq.sum(input.numbers())
+-- print('average',total/n)
+--
+-- _source_ is defined as a string or a file-like object (i.e. has a read() method which returns the next line)
+--
+-- See @{06-data.md.Reading_Unstructured_Text_Data|here}
+--
+-- Dependencies: `pl.utils`
+-- @module pl.input
+local strfind = string.find
+local strsub = string.sub
+local strmatch = string.match
+local utils = require 'pl.utils'
+local unpack = utils.unpack
+local pairs,type,tonumber = pairs,type,tonumber
+local patterns = utils.patterns
+local io = io
+
+local input = {}
+
+--- create an iterator over all tokens.
+-- based on allwords from PiL, 7.1
+-- @func getter any function that returns a line of text
+-- @string pattern
+-- @string[opt] fn Optionally can pass a function to process each token as it's found.
+-- @return an iterator
+function input.alltokens (getter,pattern,fn)
+ local line = getter() -- current line
+ local pos = 1 -- current position in the line
+ return function () -- iterator function
+ while line do -- repeat while there are lines
+ local s, e = strfind(line, pattern, pos)
+ if s then -- found a word?
+ pos = e + 1 -- next position is after this token
+ local res = strsub(line, s, e) -- return the token
+ if fn then res = fn(res) end
+ return res
+ else
+ line = getter() -- token not found; try next line
+ pos = 1 -- restart from first position
+ end
+ end
+ return nil -- no more lines: end of traversal
+ end
+end
+local alltokens = input.alltokens
+
+-- question: shd this _split_ a string containing line feeds?
+
+--- create a function which grabs the next value from a source. If the source is a string, then the getter
+-- will return the string and thereafter return nil. If not specified then the source is assumed to be stdin.
+-- @param f a string or a file-like object (i.e. has a read() method which returns the next line)
+-- @return a getter function
+function input.create_getter(f)
+ if f then
+ if type(f) == 'string' then
+ local ls = utils.split(f,'\n')
+ local i,n = 0,#ls
+ return function()
+ i = i + 1
+ if i > n then return nil end
+ return ls[i]
+ end
+ else
+ -- anything that supports the read() method!
+ if not f.read then error('not a file-like object') end
+ return function() return f:read() end
+ end
+ else
+ return io.read -- i.e. just read from stdin
+ end
+end
+
+--- generate a sequence of numbers from a source.
+-- @param f A source
+-- @return An iterator
+function input.numbers(f)
+ return alltokens(input.create_getter(f),
+ '('..patterns.FLOAT..')',tonumber)
+end
+
+--- generate a sequence of words from a source.
+-- @param f A source
+-- @return An iterator
+function input.words(f)
+ return alltokens(input.create_getter(f),"%w+")
+end
+
+local function apply_tonumber (no_fail,...)
+ local args = {...}
+ for i = 1,#args do
+ local n = tonumber(args[i])
+ if n == nil then
+ if not no_fail then return nil,args[i] end
+ else
+ args[i] = n
+ end
+ end
+ return args
+end
+
+--- parse an input source into fields.
+-- By default, will fail if it cannot convert a field to a number.
+-- @param ids a list of field indices, or a maximum field index
+-- @string delim delimiter to parse fields (default space)
+-- @param f a source @see create_getter
+-- @tab opts option table, `{no_fail=true}`
+-- @return an iterator with the field values
+-- @usage for x,y in fields {2,3} do print(x,y) end -- 2nd and 3rd fields from stdin
+function input.fields (ids,delim,f,opts)
+ local sep
+ local s
+ local getter = input.create_getter(f)
+ local no_fail = opts and opts.no_fail
+ local no_convert = opts and opts.no_convert
+ if not delim or delim == ' ' then
+ delim = '%s'
+ sep = '%s+'
+ s = '%s*'
+ else
+ sep = delim
+ s = ''
+ end
+ local max_id = 0
+ if type(ids) == 'table' then
+ for i,id in pairs(ids) do
+ if id > max_id then max_id = id end
+ end
+ else
+ max_id = ids
+ ids = {}
+ for i = 1,max_id do ids[#ids+1] = i end
+ end
+ local pat = '[^'..delim..']*'
+ local k = 1
+ for i = 1,max_id do
+ if ids[k] == i then
+ k = k + 1
+ s = s..'('..pat..')'
+ else
+ s = s..pat
+ end
+ if i < max_id then
+ s = s..sep
+ end
+ end
+ local linecount = 1
+ return function()
+ local line,results,err
+ repeat
+ line = getter()
+ linecount = linecount + 1
+ if not line then return nil end
+ if no_convert then
+ results = {strmatch(line,s)}
+ else
+ results,err = apply_tonumber(no_fail,strmatch(line,s))
+ if not results then
+ utils.quit("line "..(linecount-1)..": cannot convert '"..err.."' to number")
+ end
+ end
+ until #results > 0
+ return unpack(results)
+ end
+end
+
+return input
+
diff --git a/macros/luatex/generic/penlight/pl/lapp.lua b/macros/luatex/generic/penlight/pl/lapp.lua
new file mode 100644
index 0000000000..56d1d9d72f
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/lapp.lua
@@ -0,0 +1,451 @@
+--- Simple command-line parsing using human-readable specification.
+-- Supports GNU-style parameters.
+--
+-- lapp = require 'pl.lapp'
+-- local args = lapp [[
+-- Does some calculations
+-- -o,--offset (default 0.0) Offset to add to scaled number
+-- -s,--scale (number) Scaling factor
+-- <number> (number) Number to be scaled
+-- ]]
+--
+-- print(args.offset + args.scale * args.number)
+--
+-- Lines beginning with `'-'` are flags; there may be a short and a long name;
+-- lines beginning with `'<var>'` are arguments. Anything in parens after
+-- the flag/argument is either a default, a type name or a range constraint.
+--
+-- See @{08-additional.md.Command_line_Programs_with_Lapp|the Guide}
+--
+-- Dependencies: `pl.sip`
+-- @module pl.lapp
+
+local status,sip = pcall(require,'pl.sip')
+if not status then
+ sip = require 'sip'
+end
+local match = sip.match_at_start
+local append,tinsert = table.insert,table.insert
+
+sip.custom_pattern('X','(%a[%w_%-]*)')
+
+local function lines(s) return s:gmatch('([^\n]*)\n') end
+local function lstrip(str) return str:gsub('^%s+','') end
+local function strip(str) return lstrip(str):gsub('%s+$','') end
+local function at(s,k) return s:sub(k,k) end
+
+local lapp = {}
+
+local open_files,parms,aliases,parmlist,usage,script
+
+lapp.callback = false -- keep Strict happy
+
+local filetypes = {
+ stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'},
+ stderr = {io.stderr,'file-out'}
+}
+
+--- controls whether to dump usage on error.
+-- Defaults to true
+lapp.show_usage_error = true
+
+--- quit this script immediately.
+-- @string msg optional message
+-- @bool no_usage suppress 'usage' display
+function lapp.quit(msg,no_usage)
+ if no_usage == 'throw' then
+ error(msg)
+ end
+ if msg then
+ io.stderr:write(msg..'\n\n')
+ end
+ if not no_usage then
+ io.stderr:write(usage)
+ end
+ os.exit(1)
+end
+
+--- print an error to stderr and quit.
+-- @string msg a message
+-- @bool no_usage suppress 'usage' display
+function lapp.error(msg,no_usage)
+ if not lapp.show_usage_error then
+ no_usage = true
+ elseif lapp.show_usage_error == 'throw' then
+ no_usage = 'throw'
+ end
+ lapp.quit(script..': '..msg,no_usage)
+end
+
+--- open a file.
+-- This will quit on error, and keep a list of file objects for later cleanup.
+-- @string file filename
+-- @string[opt] opt same as second parameter of `io.open`
+function lapp.open (file,opt)
+ local val,err = io.open(file,opt)
+ if not val then lapp.error(err,true) end
+ append(open_files,val)
+ return val
+end
+
+--- quit if the condition is false.
+-- @bool condn a condition
+-- @string msg message text
+function lapp.assert(condn,msg)
+ if not condn then
+ lapp.error(msg)
+ end
+end
+
+local function range_check(x,min,max,parm)
+ lapp.assert(min <= x and max >= x,parm..' out of range')
+end
+
+local function xtonumber(s)
+ local val = tonumber(s)
+ if not val then lapp.error("unable to convert to number: "..s) end
+ return val
+end
+
+local types = {}
+
+local builtin_types = {string=true,number=true,['file-in']='file',['file-out']='file',boolean=true}
+
+local function convert_parameter(ps,val)
+ if ps.converter then
+ val = ps.converter(val)
+ end
+ if ps.type == 'number' then
+ val = xtonumber(val)
+ elseif builtin_types[ps.type] == 'file' then
+ val = lapp.open(val,(ps.type == 'file-in' and 'r') or 'w' )
+ elseif ps.type == 'boolean' then
+ return val
+ end
+ if ps.constraint then
+ ps.constraint(val)
+ end
+ return val
+end
+
+--- add a new type to Lapp. These appear in parens after the value like
+-- a range constraint, e.g. '<ival> (integer) Process PID'
+-- @string name name of type
+-- @param converter either a function to convert values, or a Lua type name.
+-- @func[opt] constraint optional function to verify values, should use lapp.error
+-- if failed.
+function lapp.add_type (name,converter,constraint)
+ types[name] = {converter=converter,constraint=constraint}
+end
+
+local function force_short(short)
+ lapp.assert(#short==1,short..": short parameters should be one character")
+end
+
+-- deducing type of variable from default value;
+local function process_default (sval,vtype)
+ local val, success
+ if not vtype or vtype == 'number' then
+ val = tonumber(sval)
+ end
+ if val then -- we have a number!
+ return val,'number'
+ elseif filetypes[sval] then
+ local ft = filetypes[sval]
+ return ft[1],ft[2]
+ else
+ if sval == 'true' and not vtype then
+ return true, 'boolean'
+ end
+ if sval:match '^["\']' then sval = sval:sub(2,-2) end
+
+ local ps = types[vtype] or {}
+ ps.type = vtype
+
+ local show_usage_error = lapp.show_usage_error
+ lapp.show_usage_error = "throw"
+ success, val = pcall(convert_parameter, ps, sval)
+ lapp.show_usage_error = show_usage_error
+ if success then
+ return val, vtype or 'string'
+ end
+
+ return sval,vtype or 'string'
+ end
+end
+
+--- process a Lapp options string.
+-- Usually called as `lapp()`.
+-- @string str the options text
+-- @tparam {string} args a table of arguments (default is `_G.arg`)
+-- @return a table with parameter-value pairs
+function lapp.process_options_string(str,args)
+ local results = {}
+ local varargs
+ local arg = args or _G.arg
+ open_files = {}
+ parms = {}
+ aliases = {}
+ parmlist = {}
+
+ local function check_varargs(s)
+ local res,cnt = s:gsub('^%.%.%.%s*','')
+ return res, (cnt > 0)
+ end
+
+ local function set_result(ps,parm,val)
+ parm = type(parm) == "string" and parm:gsub("%W", "_") or parm -- so foo-bar becomes foo_bar in Lua
+ if not ps.varargs then
+ results[parm] = val
+ else
+ if not results[parm] then
+ results[parm] = { val }
+ else
+ append(results[parm],val)
+ end
+ end
+ end
+
+ usage = str
+
+ for _,a in ipairs(arg) do
+ if a == "-h" or a == "--help" then
+ return lapp.quit()
+ end
+ end
+
+
+ for line in lines(str) do
+ local res = {}
+ local optparm,defval,vtype,constraint,rest
+ line = lstrip(line)
+ local function check(str)
+ return match(str,line,res)
+ end
+
+ -- flags: either '-<short>', '-<short>,--<long>' or '--<long>'
+ if check '-$v{short}, --$o{long} $' or check '-$v{short} $' or check '--$o{long} $' then
+ if res.long then
+ optparm = res.long:gsub('[^%w%-]','_') -- I'm not sure the $o pattern will let anything else through?
+ if #res.rest == 1 then optparm = optparm .. res.rest end
+ if res.short then aliases[res.short] = optparm end
+ else
+ optparm = res.short
+ end
+ if res.short and not lapp.slack then force_short(res.short) end
+ res.rest, varargs = check_varargs(res.rest)
+ elseif check '$<{name} $' then -- is it <parameter_name>?
+ -- so <input file...> becomes input_file ...
+ optparm,rest = res.name:match '([^%.]+)(.*)'
+ optparm = optparm:gsub('%A','_')
+ varargs = rest == '...'
+ append(parmlist,optparm)
+ end
+ -- this is not a pure doc line and specifies the flag/parameter type
+ if res.rest then
+ line = res.rest
+ res = {}
+ local optional
+ -- do we have ([optional] [<type>] [default <val>])?
+ if match('$({def} $',line,res) or match('$({def}',line,res) then
+ local typespec = strip(res.def)
+ local ftype, rest = typespec:match('^(%S+)(.*)$')
+ rest = strip(rest)
+ if ftype == 'optional' then
+ ftype, rest = rest:match('^(%S+)(.*)$')
+ rest = strip(rest)
+ optional = true
+ end
+ local default
+ if ftype == 'default' then
+ default = true
+ if rest == '' then lapp.error("value must follow default") end
+ else -- a type specification
+ if match('$f{min}..$f{max}',ftype,res) then
+ -- a numerical range like 1..10
+ local min,max = res.min,res.max
+ vtype = 'number'
+ constraint = function(x)
+ range_check(x,min,max,optparm)
+ end
+ elseif not ftype:match '|' then -- plain type
+ vtype = ftype
+ else
+ -- 'enum' type is a string which must belong to
+ -- one of several distinct values
+ local enums = ftype
+ local enump = '|' .. enums .. '|'
+ vtype = 'string'
+ constraint = function(s)
+ lapp.assert(enump:match('|'..s..'|'),
+ "value '"..s.."' not in "..enums
+ )
+ end
+ end
+ end
+ res.rest = rest
+ typespec = res.rest
+ -- optional 'default value' clause. Type is inferred as
+ -- 'string' or 'number' if there's no explicit type
+ if default or match('default $r{rest}',typespec,res) then
+ defval,vtype = process_default(res.rest,vtype)
+ end
+ else -- must be a plain flag, no extra parameter required
+ defval = false
+ vtype = 'boolean'
+ end
+ local ps = {
+ type = vtype,
+ defval = defval,
+ required = defval == nil and not optional,
+ comment = res.rest or optparm,
+ constraint = constraint,
+ varargs = varargs
+ }
+ varargs = nil
+ if types[vtype] then
+ local converter = types[vtype].converter
+ if type(converter) == 'string' then
+ ps.type = converter
+ else
+ ps.converter = converter
+ end
+ ps.constraint = types[vtype].constraint
+ elseif not builtin_types[vtype] and vtype then
+ lapp.error(vtype.." is unknown type")
+ end
+ parms[optparm] = ps
+ end
+ end
+ -- cool, we have our parms, let's parse the command line args
+ local iparm = 1
+ local iextra = 1
+ local i = 1
+ local parm,ps,val
+ local end_of_flags = false
+
+ local function check_parm (parm)
+ local eqi = parm:find '[=:]'
+ if eqi then
+ tinsert(arg,i+1,parm:sub(eqi+1))
+ parm = parm:sub(1,eqi-1)
+ end
+ return parm,eqi
+ end
+
+ local function is_flag (parm)
+ return parms[aliases[parm] or parm]
+ end
+
+ while i <= #arg do
+ local theArg = arg[i]
+ local res = {}
+ -- after '--' we don't parse args and they end up in
+ -- the array part of the result (args[1] etc)
+ if theArg == '--' then
+ end_of_flags = true
+ iparm = #parmlist + 1
+ i = i + 1
+ theArg = arg[i]
+ if not theArg then
+ break
+ end
+ end
+ -- look for a flag, -<short flags> or --<long flag>
+ if not end_of_flags and (match('--$S{long}',theArg,res) or match('-$S{short}',theArg,res)) then
+ if res.long then -- long option
+ parm = check_parm(res.long)
+ elseif #res.short == 1 or is_flag(res.short) then
+ parm = res.short
+ else
+ local parmstr,eq = check_parm(res.short)
+ if not eq then
+ parm = at(parmstr,1)
+ local flag = is_flag(parm)
+ if flag and flag.type ~= 'boolean' then
+ --if isdigit(at(parmstr,2)) then
+ -- a short option followed by a digit is an exception (for AW;))
+ -- push ahead into the arg array
+ tinsert(arg,i+1,parmstr:sub(2))
+ else
+ -- push multiple flags into the arg array!
+ for k = 2,#parmstr do
+ tinsert(arg,i+k-1,'-'..at(parmstr,k))
+ end
+ end
+ else
+ parm = parmstr
+ end
+ end
+ if aliases[parm] then parm = aliases[parm] end
+ if not parms[parm] and (parm == 'h' or parm == 'help') then
+ lapp.quit()
+ end
+ else -- a parameter
+ parm = parmlist[iparm]
+ if not parm then
+ -- extra unnamed parameters are indexed starting at 1
+ parm = iextra
+ ps = { type = 'string' }
+ parms[parm] = ps
+ iextra = iextra + 1
+ else
+ ps = parms[parm]
+ end
+ if not ps.varargs then
+ iparm = iparm + 1
+ end
+ val = theArg
+ end
+ ps = parms[parm]
+ if not ps then lapp.error("unrecognized parameter: "..parm) end
+ if ps.type ~= 'boolean' then -- we need a value! This should follow
+ if not val then
+ i = i + 1
+ val = arg[i]
+ theArg = val
+ end
+ lapp.assert(val,parm.." was expecting a value")
+ else -- toggle boolean flags (usually false -> true)
+ val = not ps.defval
+ end
+ ps.used = true
+ val = convert_parameter(ps,val)
+ set_result(ps,parm,val)
+ if builtin_types[ps.type] == 'file' then
+ set_result(ps,parm..'_name',theArg)
+ end
+ if lapp.callback then
+ lapp.callback(parm,theArg,res)
+ end
+ i = i + 1
+ val = nil
+ end
+ -- check unused parms, set defaults and check if any required parameters were missed
+ for parm,ps in pairs(parms) do
+ if not ps.used then
+ if ps.required then lapp.error("missing required parameter: "..parm) end
+ set_result(ps,parm,ps.defval)
+ end
+ end
+ return results
+end
+
+if arg then
+ script = arg[0]
+ script = script or rawget(_G,"LAPP_SCRIPT") or "unknown"
+ -- strip dir and extension to get current script name
+ script = script:gsub('.+[\\/]',''):gsub('%.%a+$','')
+else
+ script = "inter"
+end
+
+
+setmetatable(lapp, {
+ __call = function(tbl,str,args) return lapp.process_options_string(str,args) end,
+})
+
+
+return lapp
+
+
diff --git a/macros/luatex/generic/penlight/pl/lexer.lua b/macros/luatex/generic/penlight/pl/lexer.lua
new file mode 100644
index 0000000000..9219716507
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/lexer.lua
@@ -0,0 +1,515 @@
+--- Lexical scanner for creating a sequence of tokens from text.
+-- `lexer.scan(s)` returns an iterator over all tokens found in the
+-- string `s`. This iterator returns two values, a token type string
+-- (such as 'string' for quoted string, 'iden' for identifier) and the value of the
+-- token.
+--
+-- Versions specialized for Lua and C are available; these also handle block comments
+-- and classify keywords as 'keyword' tokens. For example:
+--
+-- > s = 'for i=1,n do'
+-- > for t,v in lexer.lua(s) do print(t,v) end
+-- keyword for
+-- iden i
+-- = =
+-- number 1
+-- , ,
+-- iden n
+-- keyword do
+--
+-- See the Guide for further @{06-data.md.Lexical_Scanning|discussion}
+-- @module pl.lexer
+
+local strfind = string.find
+local strsub = string.sub
+local append = table.insert
+
+
+local function assert_arg(idx,val,tp)
+ if type(val) ~= tp then
+ error("argument "..idx.." must be "..tp, 2)
+ end
+end
+
+local lexer = {}
+
+local NUMBER1 = '^[%+%-]?%d+%.?%d*[eE][%+%-]?%d+'
+local NUMBER1a = '^[%+%-]?%d*%.%d+[eE][%+%-]?%d+'
+local NUMBER2 = '^[%+%-]?%d+%.?%d*'
+local NUMBER2a = '^[%+%-]?%d*%.%d+'
+local NUMBER3 = '^0x[%da-fA-F]+'
+local NUMBER4 = '^%d+%.?%d*[eE][%+%-]?%d+'
+local NUMBER4a = '^%d*%.%d+[eE][%+%-]?%d+'
+local NUMBER5 = '^%d+%.?%d*'
+local NUMBER5a = '^%d*%.%d+'
+local IDEN = '^[%a_][%w_]*'
+local WSPACE = '^%s+'
+local STRING1 = "^(['\"])%1" -- empty string
+local STRING2 = [[^(['"])(\*)%2%1]]
+local STRING3 = [[^(['"]).-[^\](\*)%2%1]]
+local CHAR1 = "^''"
+local CHAR2 = [[^'(\*)%1']]
+local CHAR3 = [[^'.-[^\](\*)%1']]
+local PREPRO = '^#.-[^\\]\n'
+
+local plain_matches,lua_matches,cpp_matches,lua_keyword,cpp_keyword
+
+local function tdump(tok)
+ return tok,tok
+end
+
+local function ndump(tok,options)
+ if options and options.number then
+ tok = tonumber(tok)
+ end
+ return "number",tok
+end
+
+-- regular strings, single or double quotes; usually we want them
+-- without the quotes
+local function sdump(tok,options)
+ if options and options.string then
+ tok = tok:sub(2,-2)
+ end
+ return "string",tok
+end
+
+-- long Lua strings need extra work to get rid of the quotes
+local function sdump_l(tok,options,findres)
+ if options and options.string then
+ local quotelen = 3
+ if findres[3] then
+ quotelen = quotelen + findres[3]:len()
+ end
+ tok = tok:sub(quotelen, -quotelen)
+ if tok:sub(1, 1) == "\n" then
+ tok = tok:sub(2)
+ end
+ end
+ return "string",tok
+end
+
+local function chdump(tok,options)
+ if options and options.string then
+ tok = tok:sub(2,-2)
+ end
+ return "char",tok
+end
+
+local function cdump(tok)
+ return "comment",tok
+end
+
+local function wsdump (tok)
+ return "space",tok
+end
+
+local function pdump (tok)
+ return "prepro",tok
+end
+
+local function plain_vdump(tok)
+ return "iden",tok
+end
+
+local function lua_vdump(tok)
+ if lua_keyword[tok] then
+ return "keyword",tok
+ else
+ return "iden",tok
+ end
+end
+
+local function cpp_vdump(tok)
+ if cpp_keyword[tok] then
+ return "keyword",tok
+ else
+ return "iden",tok
+ end
+end
+
+--- create a plain token iterator from a string or file-like object.
+-- @tparam string|file s a string or a file-like object with `:read()` method returning lines.
+-- @tab matches an optional match table - array of token descriptions.
+-- A token is described by a `{pattern, action}` pair, where `pattern` should match
+-- token body and `action` is a function called when a token of described type is found.
+-- @tab[opt] filter a table of token types to exclude, by default `{space=true}`
+-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
+-- which means convert numbers and strip string quotes.
+function lexer.scan(s,matches,filter,options)
+ local file = type(s) ~= 'string' and s
+ filter = filter or {space=true}
+ options = options or {number=true,string=true}
+ if filter then
+ if filter.space then filter[wsdump] = true end
+ if filter.comments then
+ filter[cdump] = true
+ end
+ end
+ if not matches then
+ if not plain_matches then
+ plain_matches = {
+ {WSPACE,wsdump},
+ {NUMBER3,ndump},
+ {IDEN,plain_vdump},
+ {NUMBER1,ndump},
+ {NUMBER1a,ndump},
+ {NUMBER2,ndump},
+ {NUMBER2a,ndump},
+ {STRING1,sdump},
+ {STRING2,sdump},
+ {STRING3,sdump},
+ {'^.',tdump}
+ }
+ end
+ matches = plain_matches
+ end
+
+ local line_nr = 0
+ local next_line = file and file:read()
+ local sz = file and 0 or #s
+ local idx = 1
+
+ local tlist_i
+ local tlist
+
+ local first_hit = true
+
+ local function iter(res)
+ local tp = type(res)
+
+ if tlist then -- returning the inserted token list
+ local cur = tlist[tlist_i]
+ if cur then
+ tlist_i = tlist_i + 1
+ return cur[1], cur[2]
+ else
+ tlist = nil
+ end
+ end
+
+ if tp == 'string' then -- search up to some special pattern
+ local i1,i2 = strfind(s,res,idx)
+ if i1 then
+ local tok = strsub(s,i1,i2)
+ idx = i2 + 1
+ return '', tok
+ else
+ idx = sz + 1
+ return '', ''
+ end
+
+ elseif tp == 'table' then -- insert a token list
+ tlist_i = 1
+ tlist = res
+ return '', ''
+
+ elseif tp ~= 'nil' then -- return position
+ return line_nr, idx
+
+ else -- look for next token
+ if first_hit then
+ if not file then line_nr = 1 end
+ first_hit = false
+ end
+
+ if idx > sz then
+ if file then
+ if not next_line then
+ return -- past the end of file, done
+ end
+ s = next_line
+ line_nr = line_nr + 1
+ next_line = file:read()
+ if next_line then
+ s = s .. '\n'
+ end
+ idx, sz = 1, #s
+ else
+ return -- past the end of input, done
+ end
+ end
+
+ for _,m in ipairs(matches) do
+ local pat = m[1]
+ local fun = m[2]
+ local findres = {strfind(s,pat,idx)}
+ local i1, i2 = findres[1], findres[2]
+ if i1 then
+ local tok = strsub(s,i1,i2)
+ idx = i2 + 1
+ local ret1, ret2
+ if not (filter and filter[fun]) then
+ lexer.finished = idx > sz
+ ret1, ret2 = fun(tok, options, findres)
+ end
+ if not file and tok:find("\n") then
+ -- Update line number.
+ local _, newlines = tok:gsub("\n", {})
+ line_nr = line_nr + newlines
+ end
+ if ret1 then
+ return ret1, ret2 -- found a match
+ else
+ return iter() -- tail-call to try again
+ end
+ end
+ end
+ end
+ end
+
+ return iter
+end
+
+local function isstring (s)
+ return type(s) == 'string'
+end
+
+--- insert tokens into a stream.
+-- @param tok a token stream
+-- @param a1 a string is the type, a table is a token list and
+-- a function is assumed to be a token-like iterator (returns type & value)
+-- @string a2 a string is the value
+function lexer.insert (tok,a1,a2)
+ if not a1 then return end
+ local ts
+ if isstring(a1) and isstring(a2) then
+ ts = {{a1,a2}}
+ elseif type(a1) == 'function' then
+ ts = {}
+ for t,v in a1() do
+ append(ts,{t,v})
+ end
+ else
+ ts = a1
+ end
+ tok(ts)
+end
+
+--- get everything in a stream upto a newline.
+-- @param tok a token stream
+-- @return a string
+function lexer.getline (tok)
+ local _,v = tok('.-\n')
+ return v
+end
+
+--- get current line number.
+-- @param tok a token stream
+-- @return the line number.
+-- if the input source is a file-like object,
+-- also return the column.
+function lexer.lineno (tok)
+ return tok(0)
+end
+
+--- get the rest of the stream.
+-- @param tok a token stream
+-- @return a string
+function lexer.getrest (tok)
+ local _,v = tok('.+')
+ return v
+end
+
+--- get the Lua keywords as a set-like table.
+-- So `res["and"]` etc would be `true`.
+-- @return a table
+function lexer.get_keywords ()
+ if not lua_keyword then
+ lua_keyword = {
+ ["and"] = true, ["break"] = true, ["do"] = true,
+ ["else"] = true, ["elseif"] = true, ["end"] = true,
+ ["false"] = true, ["for"] = true, ["function"] = true,
+ ["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true,
+ ["not"] = true, ["or"] = true, ["repeat"] = true,
+ ["return"] = true, ["then"] = true, ["true"] = true,
+ ["until"] = true, ["while"] = true
+ }
+ end
+ return lua_keyword
+end
+
+--- create a Lua token iterator from a string or file-like object.
+-- Will return the token type and value.
+-- @string s the string
+-- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}`
+-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
+-- which means convert numbers and strip string quotes.
+function lexer.lua(s,filter,options)
+ filter = filter or {space=true,comments=true}
+ lexer.get_keywords()
+ if not lua_matches then
+ lua_matches = {
+ {WSPACE,wsdump},
+ {NUMBER3,ndump},
+ {IDEN,lua_vdump},
+ {NUMBER4,ndump},
+ {NUMBER4a,ndump},
+ {NUMBER5,ndump},
+ {NUMBER5a,ndump},
+ {STRING1,sdump},
+ {STRING2,sdump},
+ {STRING3,sdump},
+ {'^%-%-%[(=*)%[.-%]%1%]',cdump},
+ {'^%-%-.-\n',cdump},
+ {'^%[(=*)%[.-%]%1%]',sdump_l},
+ {'^==',tdump},
+ {'^~=',tdump},
+ {'^<=',tdump},
+ {'^>=',tdump},
+ {'^%.%.%.',tdump},
+ {'^%.%.',tdump},
+ {'^.',tdump}
+ }
+ end
+ return lexer.scan(s,lua_matches,filter,options)
+end
+
+--- create a C/C++ token iterator from a string or file-like object.
+-- Will return the token type type and value.
+-- @string s the string
+-- @tab[opt] filter a table of token types to exclude, by default `{space=true,comments=true}`
+-- @tab[opt] options a table of options; by default, `{number=true,string=true}`,
+-- which means convert numbers and strip string quotes.
+function lexer.cpp(s,filter,options)
+ filter = filter or {space=true,comments=true}
+ if not cpp_keyword then
+ cpp_keyword = {
+ ["class"] = true, ["break"] = true, ["do"] = true, ["sizeof"] = true,
+ ["else"] = true, ["continue"] = true, ["struct"] = true,
+ ["false"] = true, ["for"] = true, ["public"] = true, ["void"] = true,
+ ["private"] = true, ["protected"] = true, ["goto"] = true,
+ ["if"] = true, ["static"] = true, ["const"] = true, ["typedef"] = true,
+ ["enum"] = true, ["char"] = true, ["int"] = true, ["bool"] = true,
+ ["long"] = true, ["float"] = true, ["true"] = true, ["delete"] = true,
+ ["double"] = true, ["while"] = true, ["new"] = true,
+ ["namespace"] = true, ["try"] = true, ["catch"] = true,
+ ["switch"] = true, ["case"] = true, ["extern"] = true,
+ ["return"] = true,["default"] = true,['unsigned'] = true,['signed'] = true,
+ ["union"] = true, ["volatile"] = true, ["register"] = true,["short"] = true,
+ }
+ end
+ if not cpp_matches then
+ cpp_matches = {
+ {WSPACE,wsdump},
+ {PREPRO,pdump},
+ {NUMBER3,ndump},
+ {IDEN,cpp_vdump},
+ {NUMBER4,ndump},
+ {NUMBER4a,ndump},
+ {NUMBER5,ndump},
+ {NUMBER5a,ndump},
+ {CHAR1,chdump},
+ {CHAR2,chdump},
+ {CHAR3,chdump},
+ {STRING1,sdump},
+ {STRING2,sdump},
+ {STRING3,sdump},
+ {'^//.-\n',cdump},
+ {'^/%*.-%*/',cdump},
+ {'^==',tdump},
+ {'^!=',tdump},
+ {'^<=',tdump},
+ {'^>=',tdump},
+ {'^->',tdump},
+ {'^&&',tdump},
+ {'^||',tdump},
+ {'^%+%+',tdump},
+ {'^%-%-',tdump},
+ {'^%+=',tdump},
+ {'^%-=',tdump},
+ {'^%*=',tdump},
+ {'^/=',tdump},
+ {'^|=',tdump},
+ {'^%^=',tdump},
+ {'^::',tdump},
+ {'^.',tdump}
+ }
+ end
+ return lexer.scan(s,cpp_matches,filter,options)
+end
+
+--- get a list of parameters separated by a delimiter from a stream.
+-- @param tok the token stream
+-- @string[opt=')'] endtoken end of list. Can be '\n'
+-- @string[opt=','] delim separator
+-- @return a list of token lists.
+function lexer.get_separated_list(tok,endtoken,delim)
+ endtoken = endtoken or ')'
+ delim = delim or ','
+ local parm_values = {}
+ local level = 1 -- used to count ( and )
+ local tl = {}
+ local function tappend (tl,t,val)
+ val = val or t
+ append(tl,{t,val})
+ end
+ local is_end
+ if endtoken == '\n' then
+ is_end = function(t,val)
+ return t == 'space' and val:find '\n'
+ end
+ else
+ is_end = function (t)
+ return t == endtoken
+ end
+ end
+ local token,value
+ while true do
+ token,value=tok()
+ if not token then return nil,'EOS' end -- end of stream is an error!
+ if is_end(token,value) and level == 1 then
+ append(parm_values,tl)
+ break
+ elseif token == '(' then
+ level = level + 1
+ tappend(tl,'(')
+ elseif token == ')' then
+ level = level - 1
+ if level == 0 then -- finished with parm list
+ append(parm_values,tl)
+ break
+ else
+ tappend(tl,')')
+ end
+ elseif token == delim and level == 1 then
+ append(parm_values,tl) -- a new parm
+ tl = {}
+ else
+ tappend(tl,token,value)
+ end
+ end
+ return parm_values,{token,value}
+end
+
+--- get the next non-space token from the stream.
+-- @param tok the token stream.
+function lexer.skipws (tok)
+ local t,v = tok()
+ while t == 'space' do
+ t,v = tok()
+ end
+ return t,v
+end
+
+local skipws = lexer.skipws
+
+--- get the next token, which must be of the expected type.
+-- Throws an error if this type does not match!
+-- @param tok the token stream
+-- @string expected_type the token type
+-- @bool no_skip_ws whether we should skip whitespace
+function lexer.expecting (tok,expected_type,no_skip_ws)
+ assert_arg(1,tok,'function')
+ assert_arg(2,expected_type,'string')
+ local t,v
+ if no_skip_ws then
+ t,v = tok()
+ else
+ t,v = skipws(tok)
+ end
+ if t ~= expected_type then error ("expecting "..expected_type,2) end
+ return v
+end
+
+return lexer
diff --git a/macros/luatex/generic/penlight/pl/luabalanced.lua b/macros/luatex/generic/penlight/pl/luabalanced.lua
new file mode 100644
index 0000000000..a1f7dc67d7
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/luabalanced.lua
@@ -0,0 +1,264 @@
+--- Extract delimited Lua sequences from strings.
+-- Inspired by Damian Conway's Text::Balanced in Perl. <br/>
+-- <ul>
+-- <li>[1] <a href="http://lua-users.org/wiki/LuaBalanced">Lua Wiki Page</a></li>
+-- <li>[2] http://search.cpan.org/dist/Text-Balanced/lib/Text/Balanced.pm</li>
+-- </ul> <br/>
+-- <pre class=example>
+-- local lb = require "pl.luabalanced"
+-- --Extract Lua expression starting at position 4.
+-- print(lb.match_expression("if x^2 + x > 5 then print(x) end", 4))
+-- --> x^2 + x > 5 16
+-- --Extract Lua string starting at (default) position 1.
+-- print(lb.match_string([["test\"123" .. "more"]]))
+-- --> "test\"123" 12
+-- </pre>
+-- (c) 2008, David Manura, Licensed under the same terms as Lua (MIT license).
+-- @class module
+-- @name pl.luabalanced
+
+local M = {}
+
+local assert = assert
+
+-- map opening brace <-> closing brace.
+local ends = { ['('] = ')', ['{'] = '}', ['['] = ']' }
+local begins = {}; for k,v in pairs(ends) do begins[v] = k end
+
+
+-- Match Lua string in string <s> starting at position <pos>.
+-- Returns <string>, <posnew>, where <string> is the matched
+-- string (or nil on no match) and <posnew> is the character
+-- following the match (or <pos> on no match).
+-- Supports all Lua string syntax: "...", '...', [[...]], [=[...]=], etc.
+local function match_string(s, pos)
+ pos = pos or 1
+ local posa = pos
+ local c = s:sub(pos,pos)
+ if c == '"' or c == "'" then
+ pos = pos + 1
+ while 1 do
+ pos = assert(s:find("[" .. c .. "\\]", pos), 'syntax error')
+ if s:sub(pos,pos) == c then
+ local part = s:sub(posa, pos)
+ return part, pos + 1
+ else
+ pos = pos + 2
+ end
+ end
+ else
+ local sc = s:match("^%[(=*)%[", pos)
+ if sc then
+ local _; _, pos = s:find("%]" .. sc .. "%]", pos)
+ assert(pos)
+ local part = s:sub(posa, pos)
+ return part, pos + 1
+ else
+ return nil, pos
+ end
+ end
+end
+M.match_string = match_string
+
+
+-- Match bracketed Lua expression, e.g. "(...)", "{...}", "[...]", "[[...]]",
+-- [=[...]=], etc.
+-- Function interface is similar to match_string.
+local function match_bracketed(s, pos)
+ pos = pos or 1
+ local posa = pos
+ local ca = s:sub(pos,pos)
+ if not ends[ca] then
+ return nil, pos
+ end
+ local stack = {}
+ while 1 do
+ pos = s:find('[%(%{%[%)%}%]\"\']', pos)
+ assert(pos, 'syntax error: unbalanced')
+ local c = s:sub(pos,pos)
+ if c == '"' or c == "'" then
+ local part; part, pos = match_string(s, pos)
+ assert(part)
+ elseif ends[c] then -- open
+ local mid, posb
+ if c == '[' then mid, posb = s:match('^%[(=*)%[()', pos) end
+ if mid then
+ pos = s:match('%]' .. mid .. '%]()', posb)
+ assert(pos, 'syntax error: long string not terminated')
+ if #stack == 0 then
+ local part = s:sub(posa, pos-1)
+ return part, pos
+ end
+ else
+ stack[#stack+1] = c
+ pos = pos + 1
+ end
+ else -- close
+ assert(stack[#stack] == assert(begins[c]), 'syntax error: unbalanced')
+ stack[#stack] = nil
+ if #stack == 0 then
+ local part = s:sub(posa, pos)
+ return part, pos+1
+ end
+ pos = pos + 1
+ end
+ end
+end
+M.match_bracketed = match_bracketed
+
+
+-- Match Lua comment, e.g. "--...\n", "--[[...]]", "--[=[...]=]", etc.
+-- Function interface is similar to match_string.
+local function match_comment(s, pos)
+ pos = pos or 1
+ if s:sub(pos, pos+1) ~= '--' then
+ return nil, pos
+ end
+ pos = pos + 2
+ local partt, post = match_string(s, pos)
+ if partt then
+ return '--' .. partt, post
+ end
+ local part; part, pos = s:match('^([^\n]*\n?)()', pos)
+ return '--' .. part, pos
+end
+
+
+-- Match Lua expression, e.g. "a + b * c[e]".
+-- Function interface is similar to match_string.
+local wordop = {['and']=true, ['or']=true, ['not']=true}
+local is_compare = {['>']=true, ['<']=true, ['~']=true}
+local function match_expression(s, pos)
+ pos = pos or 1
+ local _
+ local posa = pos
+ local lastident
+ local poscs, posce
+ while pos do
+ local c = s:sub(pos,pos)
+ if c == '"' or c == "'" or c == '[' and s:find('^[=%[]', pos+1) then
+ local part; part, pos = match_string(s, pos)
+ assert(part, 'syntax error')
+ elseif c == '-' and s:sub(pos+1,pos+1) == '-' then
+ -- note: handle adjacent comments in loop to properly support
+ -- backtracing (poscs/posce).
+ poscs = pos
+ while s:sub(pos,pos+1) == '--' do
+ local part; part, pos = match_comment(s, pos)
+ assert(part)
+ pos = s:match('^%s*()', pos)
+ posce = pos
+ end
+ elseif c == '(' or c == '{' or c == '[' then
+ _, pos = match_bracketed(s, pos)
+ elseif c == '=' and s:sub(pos+1,pos+1) == '=' then
+ pos = pos + 2 -- skip over two-char op containing '='
+ elseif c == '=' and is_compare[s:sub(pos-1,pos-1)] then
+ pos = pos + 1 -- skip over two-char op containing '='
+ elseif c:match'^[%)%}%];,=]' then
+ local part = s:sub(posa, pos-1)
+ return part, pos
+ elseif c:match'^[%w_]' then
+ local newident,newpos = s:match('^([%w_]+)()', pos)
+ if pos ~= posa and not wordop[newident] then -- non-first ident
+ local pose = ((posce == pos) and poscs or pos) - 1
+ while s:match('^%s', pose) do pose = pose - 1 end
+ local ce = s:sub(pose,pose)
+ if ce:match'[%)%}\'\"%]]' or
+ ce:match'[%w_]' and not wordop[lastident]
+ then
+ local part = s:sub(posa, pos-1)
+ return part, pos
+ end
+ end
+ lastident, pos = newident, newpos
+ else
+ pos = pos + 1
+ end
+ pos = s:find('[%(%{%[%)%}%]\"\';,=%w_%-]', pos)
+ end
+ local part = s:sub(posa, #s)
+ return part, #s+1
+end
+M.match_expression = match_expression
+
+
+-- Match name list (zero or more names). E.g. "a,b,c"
+-- Function interface is similar to match_string,
+-- but returns array as match.
+local function match_namelist(s, pos)
+ pos = pos or 1
+ local list = {}
+ while 1 do
+ local c = #list == 0 and '^' or '^%s*,%s*'
+ local item, post = s:match(c .. '([%a_][%w_]*)%s*()', pos)
+ if item then pos = post else break end
+ list[#list+1] = item
+ end
+ return list, pos
+end
+M.match_namelist = match_namelist
+
+
+-- Match expression list (zero or more expressions). E.g. "a+b,b*c".
+-- Function interface is similar to match_string,
+-- but returns array as match.
+local function match_explist(s, pos)
+ pos = pos or 1
+ local list = {}
+ while 1 do
+ if #list ~= 0 then
+ local post = s:match('^%s*,%s*()', pos)
+ if post then pos = post else break end
+ end
+ local item; item, pos = match_expression(s, pos)
+ assert(item, 'syntax error')
+ list[#list+1] = item
+ end
+ return list, pos
+end
+M.match_explist = match_explist
+
+
+-- Replace snippets of code in Lua code string <s>
+-- using replacement function f(u,sin) --> sout.
+-- <u> is the type of snippet ('c' = comment, 's' = string,
+-- 'e' = any other code).
+-- Snippet is replaced with <sout> (unless <sout> is nil or false, in
+-- which case the original snippet is kept)
+-- This is somewhat analogous to string.gsub .
+local function gsub(s, f)
+ local pos = 1
+ local posa = 1
+ local sret = ''
+ while 1 do
+ pos = s:find('[%-\'\"%[]', pos)
+ if not pos then break end
+ if s:match('^%-%-', pos) then
+ local exp = s:sub(posa, pos-1)
+ if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
+ local comment; comment, pos = match_comment(s, pos)
+ sret = sret .. (f('c', assert(comment)) or comment)
+ posa = pos
+ else
+ local posb = s:find('^[\'\"%[]', pos)
+ local str
+ if posb then str, pos = match_string(s, posb) end
+ if str then
+ local exp = s:sub(posa, posb-1)
+ if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
+ sret = sret .. (f('s', str) or str)
+ posa = pos
+ else
+ pos = pos + 1
+ end
+ end
+ end
+ local exp = s:sub(posa)
+ if #exp > 0 then sret = sret .. (f('e', exp) or exp) end
+ return sret
+end
+M.gsub = gsub
+
+
+return M
diff --git a/macros/luatex/generic/penlight/pl/operator.lua b/macros/luatex/generic/penlight/pl/operator.lua
new file mode 100644
index 0000000000..60eaffd10a
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/operator.lua
@@ -0,0 +1,209 @@
+--- Lua operators available as functions.
+--
+-- (similar to the Python module of the same name)
+--
+-- There is a module field `optable` which maps the operator strings
+-- onto these functions, e.g. `operator.optable['()']==operator.call`
+--
+-- Operator strings like '>' and '{}' can be passed to most Penlight functions
+-- expecting a function argument.
+--
+-- @module pl.operator
+
+local strfind = string.find
+
+local operator = {}
+
+--- apply function to some arguments **()**
+-- @param fn a function or callable object
+-- @param ... arguments
+function operator.call(fn,...)
+ return fn(...)
+end
+
+--- get the indexed value from a table **[]**
+-- @param t a table or any indexable object
+-- @param k the key
+function operator.index(t,k)
+ return t[k]
+end
+
+--- returns true if arguments are equal **==**
+-- @param a value
+-- @param b value
+function operator.eq(a,b)
+ return a==b
+end
+
+--- returns true if arguments are not equal **~=**
+ -- @param a value
+-- @param b value
+function operator.neq(a,b)
+ return a~=b
+end
+
+--- returns true if a is less than b **<**
+-- @param a value
+-- @param b value
+function operator.lt(a,b)
+ return a < b
+end
+
+--- returns true if a is less or equal to b **<=**
+-- @param a value
+-- @param b value
+function operator.le(a,b)
+ return a <= b
+end
+
+--- returns true if a is greater than b **>**
+-- @param a value
+-- @param b value
+function operator.gt(a,b)
+ return a > b
+end
+
+--- returns true if a is greater or equal to b **>=**
+-- @param a value
+-- @param b value
+function operator.ge(a,b)
+ return a >= b
+end
+
+--- returns length of string or table **#**
+-- @param a a string or a table
+function operator.len(a)
+ return #a
+end
+
+--- add two values **+**
+-- @param a value
+-- @param b value
+function operator.add(a,b)
+ return a+b
+end
+
+--- subtract b from a **-**
+-- @param a value
+-- @param b value
+function operator.sub(a,b)
+ return a-b
+end
+
+--- multiply two values __*__
+-- @param a value
+-- @param b value
+function operator.mul(a,b)
+ return a*b
+end
+
+--- divide first value by second **/**
+-- @param a value
+-- @param b value
+function operator.div(a,b)
+ return a/b
+end
+
+--- raise first to the power of second **^**
+-- @param a value
+-- @param b value
+function operator.pow(a,b)
+ return a^b
+end
+
+--- modulo; remainder of a divided by b **%**
+-- @param a value
+-- @param b value
+function operator.mod(a,b)
+ return a%b
+end
+
+--- concatenate two values (either strings or `__concat` defined) **..**
+-- @param a value
+-- @param b value
+function operator.concat(a,b)
+ return a..b
+end
+
+--- return the negative of a value **-**
+-- @param a value
+function operator.unm(a)
+ return -a
+end
+
+--- false if value evaluates as true **not**
+-- @param a value
+function operator.lnot(a)
+ return not a
+end
+
+--- true if both values evaluate as true **and**
+-- @param a value
+-- @param b value
+function operator.land(a,b)
+ return a and b
+end
+
+--- true if either value evaluate as true **or**
+-- @param a value
+-- @param b value
+function operator.lor(a,b)
+ return a or b
+end
+
+--- make a table from the arguments **{}**
+-- @param ... non-nil arguments
+-- @return a table
+function operator.table (...)
+ return {...}
+end
+
+--- match two strings **~**.
+-- uses @{string.find}
+function operator.match (a,b)
+ return strfind(a,b)~=nil
+end
+
+--- the null operation.
+-- @param ... arguments
+-- @return the arguments
+function operator.nop (...)
+ return ...
+end
+
+---- Map from operator symbol to function.
+-- Most of these map directly from operators;
+-- But note these extras
+--
+-- * __'()'__ `call`
+-- * __'[]'__ `index`
+-- * __'{}'__ `table`
+-- * __'~'__ `match`
+--
+-- @table optable
+-- @field operator
+ operator.optable = {
+ ['+']=operator.add,
+ ['-']=operator.sub,
+ ['*']=operator.mul,
+ ['/']=operator.div,
+ ['%']=operator.mod,
+ ['^']=operator.pow,
+ ['..']=operator.concat,
+ ['()']=operator.call,
+ ['[]']=operator.index,
+ ['<']=operator.lt,
+ ['<=']=operator.le,
+ ['>']=operator.gt,
+ ['>=']=operator.ge,
+ ['==']=operator.eq,
+ ['~=']=operator.neq,
+ ['#']=operator.len,
+ ['and']=operator.land,
+ ['or']=operator.lor,
+ ['{}']=operator.table,
+ ['~']=operator.match,
+ ['']=operator.nop,
+}
+
+return operator
diff --git a/macros/luatex/generic/penlight/pl/path.lua b/macros/luatex/generic/penlight/pl/path.lua
new file mode 100644
index 0000000000..a438a9df87
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/path.lua
@@ -0,0 +1,575 @@
+--- Path manipulation and file queries.
+--
+-- This is modelled after Python's os.path library (10.1); see @{04-paths.md|the Guide}.
+--
+-- NOTE: the functions assume the paths being dealt with to originate
+-- from the OS the application is running on. Windows drive letters are not
+-- to be used when running on a Unix system for example. The one exception
+-- is Windows paths to allow both forward and backward slashes (since Lua
+-- also accepts those)
+--
+-- Dependencies: `pl.utils`, `lfs`
+-- @module pl.path
+
+-- imports and locals
+local _G = _G
+local sub = string.sub
+local getenv = os.getenv
+local tmpnam = os.tmpname
+local package = package
+local append, concat, remove = table.insert, table.concat, table.remove
+local utils = require 'pl.utils'
+local assert_string,raise = utils.assert_string,utils.raise
+
+local res,lfs = _G.pcall(_G.require,'lfs')
+if not res then
+ error("pl.path requires LuaFileSystem")
+end
+
+local attrib = lfs.attributes
+local currentdir = lfs.currentdir
+local link_attrib = lfs.symlinkattributes
+
+local path = {}
+
+local function err_func(name, param, err, code)
+ local ret = ("%s failed"):format(tostring(name))
+ if param ~= nil then
+ ret = ret .. (" for '%s'"):format(tostring(param))
+ end
+ ret = ret .. (": %s"):format(tostring(err))
+ if code ~= nil then
+ ret = ret .. (" (code %s)"):format(tostring(code))
+ end
+ return ret
+end
+
+--- Lua iterator over the entries of a given directory.
+-- Implicit link to [`luafilesystem.dir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
+-- @function dir
+path.dir = lfs.dir
+
+--- Creates a directory.
+-- Implicit link to [`luafilesystem.mkdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
+-- @function mkdir
+path.mkdir = function(d)
+ local ok, err, code = lfs.mkdir(d)
+ if not ok then
+ return ok, err_func("mkdir", d, err, code), code
+ end
+ return ok, err, code
+end
+
+--- Removes a directory.
+-- Implicit link to [`luafilesystem.rmdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
+-- @function rmdir
+path.rmdir = function(d)
+ local ok, err, code = lfs.rmdir(d)
+ if not ok then
+ return ok, err_func("rmdir", d, err, code), code
+ end
+ return ok, err, code
+end
+
+--- Gets attributes.
+-- Implicit link to [`luafilesystem.attributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
+-- @function attrib
+path.attrib = function(d, r)
+ local ok, err, code = attrib(d, r)
+ if not ok then
+ return ok, err_func("attrib", d, err, code), code
+ end
+ return ok, err, code
+end
+
+--- Get the working directory.
+-- Implicit link to [`luafilesystem.currentdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
+-- @function currentdir
+path.currentdir = function()
+ local ok, err, code = currentdir()
+ if not ok then
+ return ok, err_func("currentdir", nil, err, code), code
+ end
+ return ok, err, code
+end
+
+--- Gets symlink attributes.
+-- Implicit link to [`luafilesystem.symlinkattributes`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
+-- @function link_attrib
+path.link_attrib = function(d, r)
+ local ok, err, code = link_attrib(d, r)
+ if not ok then
+ return ok, err_func("link_attrib", d, err, code), code
+ end
+ return ok, err, code
+end
+
+--- Changes the working directory.
+-- On Windows, if a drive is specified, it also changes the current drive. If
+-- only specifying the drive, it will only switch drive, but not modify the path.
+-- Implicit link to [`luafilesystem.chdir`](https://keplerproject.github.io/luafilesystem/manual.html#reference)
+-- @function chdir
+path.chdir = function(d)
+ local ok, err, code = lfs.chdir(d)
+ if not ok then
+ return ok, err_func("chdir", d, err, code), code
+ end
+ return ok, err, code
+end
+
+--- is this a directory?
+-- @string P A file path
+function path.isdir(P)
+ assert_string(1,P)
+ if P:match("\\$") then
+ P = P:sub(1,-2)
+ end
+ return attrib(P,'mode') == 'directory'
+end
+
+--- is this a file?
+-- @string P A file path
+function path.isfile(P)
+ assert_string(1,P)
+ return attrib(P,'mode') == 'file'
+end
+
+-- is this a symbolic link?
+-- @string P A file path
+function path.islink(P)
+ assert_string(1,P)
+ if link_attrib then
+ return link_attrib(P,'mode')=='link'
+ else
+ return false
+ end
+end
+
+--- return size of a file.
+-- @string P A file path
+function path.getsize(P)
+ assert_string(1,P)
+ return attrib(P,'size')
+end
+
+--- does a path exist?
+-- @string P A file path
+-- @return the file path if it exists (either as file, directory, socket, etc), nil otherwise
+function path.exists(P)
+ assert_string(1,P)
+ return attrib(P,'mode') ~= nil and P
+end
+
+--- Return the time of last access as the number of seconds since the epoch.
+-- @string P A file path
+function path.getatime(P)
+ assert_string(1,P)
+ return attrib(P,'access')
+end
+
+--- Return the time of last modification as the number of seconds since the epoch.
+-- @string P A file path
+function path.getmtime(P)
+ assert_string(1,P)
+ return attrib(P,'modification')
+end
+
+---Return the system's ctime as the number of seconds since the epoch.
+-- @string P A file path
+function path.getctime(P)
+ assert_string(1,P)
+ return path.attrib(P,'change')
+end
+
+
+local function at(s,i)
+ return sub(s,i,i)
+end
+
+path.is_windows = utils.is_windows
+
+local sep, other_sep, seps
+-- constant sep is the directory separator for this platform.
+-- constant dirsep is the separator in the PATH environment variable
+if path.is_windows then
+ path.sep = '\\'; other_sep = '/'
+ path.dirsep = ';'
+ seps = { ['/'] = true, ['\\'] = true }
+else
+ path.sep = '/'
+ path.dirsep = ':'
+ seps = { ['/'] = true }
+end
+sep = path.sep
+
+--- are we running Windows?
+-- @class field
+-- @name path.is_windows
+
+--- path separator for this platform.
+-- @class field
+-- @name path.sep
+
+--- separator for PATH for this platform
+-- @class field
+-- @name path.dirsep
+
+--- given a path, return the directory part and a file part.
+-- if there's no directory part, the first value will be empty
+-- @string P A file path
+-- @return directory part
+-- @return file part
+-- @usage
+-- local dir, file = path.splitpath("some/dir/myfile.txt")
+-- assert(dir == "some/dir")
+-- assert(file == "myfile.txt")
+--
+-- local dir, file = path.splitpath("some/dir/")
+-- assert(dir == "some/dir")
+-- assert(file == "")
+--
+-- local dir, file = path.splitpath("some_dir")
+-- assert(dir == "")
+-- assert(file == "some_dir")
+function path.splitpath(P)
+ assert_string(1,P)
+ local i = #P
+ local ch = at(P,i)
+ while i > 0 and ch ~= sep and ch ~= other_sep do
+ i = i - 1
+ ch = at(P,i)
+ end
+ if i == 0 then
+ return '',P
+ else
+ return sub(P,1,i-1), sub(P,i+1)
+ end
+end
+
+--- return an absolute path.
+-- @string P A file path
+-- @string[opt] pwd optional start path to use (default is current dir)
+function path.abspath(P,pwd)
+ assert_string(1,P)
+ if pwd then assert_string(2,pwd) end
+ local use_pwd = pwd ~= nil
+ if not use_pwd and not currentdir() then return P end
+ P = P:gsub('[\\/]$','')
+ pwd = pwd or currentdir()
+ if not path.isabs(P) then
+ P = path.join(pwd,P)
+ elseif path.is_windows and not use_pwd and at(P,2) ~= ':' and at(P,2) ~= '\\' then
+ P = pwd:sub(1,2)..P -- attach current drive to path like '\\fred.txt'
+ end
+ return path.normpath(P)
+end
+
+--- given a path, return the root part and the extension part.
+-- if there's no extension part, the second value will be empty
+-- @string P A file path
+-- @treturn string root part (everything upto the "."", maybe empty)
+-- @treturn string extension part (including the ".", maybe empty)
+-- @usage
+-- local file_path, ext = path.splitext("/bonzo/dog_stuff/cat.txt")
+-- assert(file_path == "/bonzo/dog_stuff/cat")
+-- assert(ext == ".txt")
+--
+-- local file_path, ext = path.splitext("")
+-- assert(file_path == "")
+-- assert(ext == "")
+function path.splitext(P)
+ assert_string(1,P)
+ local i = #P
+ local ch = at(P,i)
+ while i > 0 and ch ~= '.' do
+ if seps[ch] then
+ return P,''
+ end
+ i = i - 1
+ ch = at(P,i)
+ end
+ if i == 0 then
+ return P,''
+ else
+ return sub(P,1,i-1),sub(P,i)
+ end
+end
+
+--- return the directory part of a path
+-- @string P A file path
+-- @treturn string everything before the last dir-separator
+-- @see splitpath
+-- @usage
+-- path.dirname("/some/path/file.txt") -- "/some/path"
+-- path.dirname("file.txt") -- "" (empty string)
+function path.dirname(P)
+ assert_string(1,P)
+ local p1 = path.splitpath(P)
+ return p1
+end
+
+--- return the file part of a path
+-- @string P A file path
+-- @treturn string
+-- @see splitpath
+-- @usage
+-- path.basename("/some/path/file.txt") -- "file.txt"
+-- path.basename("/some/path/file/") -- "" (empty string)
+function path.basename(P)
+ assert_string(1,P)
+ local _,p2 = path.splitpath(P)
+ return p2
+end
+
+--- get the extension part of a path.
+-- @string P A file path
+-- @treturn string
+-- @see splitext
+-- @usage
+-- path.extension("/some/path/file.txt") -- ".txt"
+-- path.extension("/some/path/file_txt") -- "" (empty string)
+function path.extension(P)
+ assert_string(1,P)
+ local _,p2 = path.splitext(P)
+ return p2
+end
+
+--- is this an absolute path?
+-- @string P A file path
+-- @usage
+-- path.isabs("hello/path") -- false
+-- path.isabs("/hello/path") -- true
+-- -- Windows;
+-- path.isabs("hello\path") -- false
+-- path.isabs("\hello\path") -- true
+-- path.isabs("C:\hello\path") -- true
+-- path.isabs("C:hello\path") -- false
+function path.isabs(P)
+ assert_string(1,P)
+ if path.is_windows and at(P,2) == ":" then
+ return seps[at(P,3)] ~= nil
+ end
+ return seps[at(P,1)] ~= nil
+end
+
+--- return the path resulting from combining the individual paths.
+-- if the second (or later) path is absolute, we return the last absolute path (joined with any non-absolute paths following).
+-- empty elements (except the last) will be ignored.
+-- @string p1 A file path
+-- @string p2 A file path
+-- @string ... more file paths
+-- @treturn string the combined path
+-- @usage
+-- path.join("/first","second","third") -- "/first/second/third"
+-- path.join("first","second/third") -- "first/second/third"
+-- path.join("/first","/second","third") -- "/second/third"
+function path.join(p1,p2,...)
+ assert_string(1,p1)
+ assert_string(2,p2)
+ if select('#',...) > 0 then
+ local p = path.join(p1,p2)
+ local args = {...}
+ for i = 1,#args do
+ assert_string(i,args[i])
+ p = path.join(p,args[i])
+ end
+ return p
+ end
+ if path.isabs(p2) then return p2 end
+ local endc = at(p1,#p1)
+ if endc ~= path.sep and endc ~= other_sep and endc ~= "" then
+ p1 = p1..path.sep
+ end
+ return p1..p2
+end
+
+--- normalize the case of a pathname. On Unix, this returns the path unchanged,
+-- for Windows it converts;
+--
+-- * the path to lowercase
+-- * forward slashes to backward slashes
+-- @string P A file path
+-- @usage path.normcase("/Some/Path/File.txt")
+-- -- Windows: "\some\path\file.txt"
+-- -- Others : "/Some/Path/File.txt"
+function path.normcase(P)
+ assert_string(1,P)
+ if path.is_windows then
+ return P:gsub('/','\\'):lower()
+ else
+ return P
+ end
+end
+
+--- normalize a path name.
+-- `A//B`, `A/./B`, and `A/foo/../B` all become `A/B`.
+--
+-- An empty path results in '.'.
+-- @string P a file path
+function path.normpath(P)
+ assert_string(1,P)
+ -- Split path into anchor and relative path.
+ local anchor = ''
+ if path.is_windows then
+ if P:match '^\\\\' then -- UNC
+ anchor = '\\\\'
+ P = P:sub(3)
+ elseif seps[at(P, 1)] then
+ anchor = '\\'
+ P = P:sub(2)
+ elseif at(P, 2) == ':' then
+ anchor = P:sub(1, 2)
+ P = P:sub(3)
+ if seps[at(P, 1)] then
+ anchor = anchor..'\\'
+ P = P:sub(2)
+ end
+ end
+ P = P:gsub('/','\\')
+ else
+ -- According to POSIX, in path start '//' and '/' are distinct,
+ -- but '///+' is equivalent to '/'.
+ if P:match '^//' and at(P, 3) ~= '/' then
+ anchor = '//'
+ P = P:sub(3)
+ elseif at(P, 1) == '/' then
+ anchor = '/'
+ P = P:match '^/*(.*)$'
+ end
+ end
+ local parts = {}
+ for part in P:gmatch('[^'..sep..']+') do
+ if part == '..' then
+ if #parts ~= 0 and parts[#parts] ~= '..' then
+ remove(parts)
+ else
+ append(parts, part)
+ end
+ elseif part ~= '.' then
+ append(parts, part)
+ end
+ end
+ P = anchor..concat(parts, sep)
+ if P == '' then P = '.' end
+ return P
+end
+
+--- relative path from current directory or optional start point
+-- @string P a path
+-- @string[opt] start optional start point (default current directory)
+function path.relpath (P,start)
+ assert_string(1,P)
+ if start then assert_string(2,start) end
+ local split,min,append = utils.split, math.min, table.insert
+ P = path.abspath(P,start)
+ start = start or currentdir()
+ local compare
+ if path.is_windows then
+ P = P:gsub("/","\\")
+ start = start:gsub("/","\\")
+ compare = function(v) return v:lower() end
+ else
+ compare = function(v) return v end
+ end
+ local startl, Pl = split(start,sep), split(P,sep)
+ local n = min(#startl,#Pl)
+ if path.is_windows and n > 0 and at(Pl[1],2) == ':' and Pl[1] ~= startl[1] then
+ return P
+ end
+ local k = n+1 -- default value if this loop doesn't bail out!
+ for i = 1,n do
+ if compare(startl[i]) ~= compare(Pl[i]) then
+ k = i
+ break
+ end
+ end
+ local rell = {}
+ for i = 1, #startl-k+1 do rell[i] = '..' end
+ if k <= #Pl then
+ for i = k,#Pl do append(rell,Pl[i]) end
+ end
+ return table.concat(rell,sep)
+end
+
+
+--- Replace a starting '~' with the user's home directory.
+-- In windows, if HOME isn't set, then USERPROFILE is used in preference to
+-- HOMEDRIVE HOMEPATH. This is guaranteed to be writeable on all versions of Windows.
+-- @string P A file path
+function path.expanduser(P)
+ assert_string(1,P)
+ if at(P,1) == '~' then
+ local home = getenv('HOME')
+ if not home then -- has to be Windows
+ home = getenv 'USERPROFILE' or (getenv 'HOMEDRIVE' .. getenv 'HOMEPATH')
+ end
+ return home..sub(P,2)
+ else
+ return P
+ end
+end
+
+
+---Return a suitable full path to a new temporary file name.
+-- unlike os.tmpname(), it always gives you a writeable path (uses TEMP environment variable on Windows)
+function path.tmpname ()
+ local res = tmpnam()
+ -- On Windows if Lua is compiled using MSVC14 os.tmpname
+ -- already returns an absolute path within TEMP env variable directory,
+ -- no need to prepend it.
+ if path.is_windows and not res:find(':') then
+ res = getenv('TEMP')..res
+ end
+ return res
+end
+
+--- return the largest common prefix path of two paths.
+-- @string path1 a file path
+-- @string path2 a file path
+-- @return the common prefix (Windows: separators will be normalized, casing will be original)
+function path.common_prefix (path1,path2)
+ assert_string(1,path1)
+ assert_string(2,path2)
+ -- get them in order!
+ if #path1 > #path2 then path2,path1 = path1,path2 end
+ local compare
+ if path.is_windows then
+ path1 = path1:gsub("/", "\\")
+ path2 = path2:gsub("/", "\\")
+ compare = function(v) return v:lower() end
+ else
+ compare = function(v) return v end
+ end
+ for i = 1,#path1 do
+ if compare(at(path1,i)) ~= compare(at(path2,i)) then
+ local cp = path1:sub(1,i-1)
+ if at(path1,i-1) ~= sep then
+ cp = path.dirname(cp)
+ end
+ return cp
+ end
+ end
+ if at(path2,#path1+1) ~= sep then path1 = path.dirname(path1) end
+ return path1
+ --return ''
+end
+
+--- return the full path where a particular Lua module would be found.
+-- Both package.path and package.cpath is searched, so the result may
+-- either be a Lua file or a shared library.
+-- @string mod name of the module
+-- @return on success: path of module, lua or binary
+-- @return on error: nil, error string listing paths tried
+function path.package_path(mod)
+ assert_string(1,mod)
+ local res, err1, err2
+ res, err1 = package.searchpath(mod,package.path)
+ if res then return res,true end
+ res, err2 = package.searchpath(mod,package.cpath)
+ if res then return res,false end
+ return raise ('cannot find module on path\n' .. err1 .. "\n" .. err2)
+end
+
+
+---- finis -----
+return path
diff --git a/macros/luatex/generic/penlight/pl/permute.lua b/macros/luatex/generic/penlight/pl/permute.lua
new file mode 100644
index 0000000000..ce646f4d52
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/permute.lua
@@ -0,0 +1,196 @@
+--- Permutation operations.
+--
+-- Dependencies: `pl.utils`, `pl.tablex`
+-- @module pl.permute
+local tablex = require 'pl.tablex'
+local utils = require 'pl.utils'
+local copy = tablex.deepcopy
+local append = table.insert
+local assert_arg = utils.assert_arg
+
+
+local permute = {}
+
+
+--- an iterator over all order-permutations of the elements of a list.
+-- Please note that the same list is returned each time, so do not keep references!
+-- @param a list-like table
+-- @return an iterator which provides the next permutation as a list
+function permute.order_iter(a)
+ assert_arg(1,a,'table')
+
+ local t = #a
+ local stack = { 1 }
+ local function iter()
+ local h = #stack
+ local n = t - h + 1
+
+ local i = stack[h]
+ if i > t then
+ return
+ end
+
+ if n == 0 then
+ table.remove(stack)
+ h = h - 1
+
+ stack[h] = stack[h] + 1
+ return a
+
+ elseif i <= n then
+
+ -- put i-th element as the last one
+ a[n], a[i] = a[i], a[n]
+
+ -- generate all permutations of the other elements
+ table.insert(stack, 1)
+
+ else
+
+ table.remove(stack)
+ h = h - 1
+
+ n = n + 1
+ i = stack[h]
+
+ -- restore i-th element
+ a[n], a[i] = a[i], a[n]
+
+ stack[h] = stack[h] + 1
+ end
+ return iter() -- tail-call
+ end
+
+ return iter
+end
+
+
+--- construct a table containing all the order-permutations of a list.
+-- @param a list-like table
+-- @return a table of tables
+-- @usage permute.order_table {1,2,3} --> {{2,3,1},{3,2,1},{3,1,2},{1,3,2},{2,1,3},{1,2,3}}
+function permute.order_table (a)
+ assert_arg(1,a,'table')
+ local res = {}
+ for t in permute.iter(a) do
+ append(res,copy(t))
+ end
+ return res
+end
+
+
+
+--- an iterator over all permutations of the elements of the given lists.
+-- @param ... list-like tables, they are nil-safe if a length-field `n` is provided (see `utils.pack`)
+-- @return an iterator which provides the next permutation as return values in the same order as the provided lists, preceeded by an index
+-- @usage
+-- local strs = utils.pack("one", nil, "three") -- adds an 'n' field for nil-safety
+-- local bools = utils.pack(true, false)
+-- local iter = permute.list_iter(strs, bools)
+--
+-- print(iter()) --> 1, one, true
+-- print(iter()) --> 2, nil, true
+-- print(iter()) --> 3, three, true
+-- print(iter()) --> 4, one, false
+-- print(iter()) --> 5, nil, false
+-- print(iter()) --> 6, three, false
+function permute.list_iter(...)
+ local elements = {...}
+ local pointers = {}
+ local sizes = {}
+ local size = #elements
+ for i, list in ipairs(elements) do
+ assert_arg(i,list,'table')
+ pointers[i] = 1
+ sizes[i] = list.n or #list
+ end
+ local count = 0
+
+ return function()
+ if pointers[size] > sizes[size] then return end -- we're done
+ count = count + 1
+ local r = { n = #elements }
+ local cascade_up = true
+ for i = 1, size do
+ r[i] = elements[i][pointers[i]]
+ if cascade_up then
+ pointers[i] = pointers[i] + 1
+ if pointers[i] <= sizes[i] then
+ -- this list is not done yet, stop cascade
+ cascade_up = false
+ else
+ -- this list is done
+ if i ~= size then
+ -- reset pointer
+ pointers[i] = 1
+ end
+ end
+ end
+ end
+ return count, utils.unpack(r)
+ end
+end
+
+
+
+--- construct a table containing all the permutations of a set of lists.
+-- @param ... list-like tables, they are nil-safe if a length-field `n` is provided
+-- @return a list of lists, the sub-lists have an 'n' field for nil-safety
+-- @usage
+-- local strs = utils.pack("one", nil, "three") -- adds an 'n' field for nil-safety
+-- local bools = utils.pack(true, false)
+-- local results = permute.list_table(strs, bools)
+-- -- results = {
+-- -- { "one, true, n = 2 }
+-- -- { nil, true, n = 2 },
+-- -- { "three, true, n = 2 },
+-- -- { "one, false, n = 2 },
+-- -- { nil, false, n = 2 },
+-- -- { "three", false, n = 2 },
+-- -- }
+function permute.list_table(...)
+ local iter = permute.list_iter(...)
+ local results = {}
+ local i = 1
+ while true do
+ local values = utils.pack(iter())
+ if values[1] == nil then return results end
+ for i = 1, values.n do values[i] = values[i+1] end
+ values.n = values.n - 1
+ results[i] = values
+ i = i + 1
+ end
+end
+
+
+-- backward compat, to be deprecated
+
+--- deprecated.
+-- @param ...
+-- @see permute.order_iter
+function permute.iter(...)
+ utils.raise_deprecation {
+ source = "Penlight " .. utils._VERSION,
+ message = "function 'iter' was renamed to 'order_iter'",
+ version_removed = "2.0.0",
+ deprecated_after = "1.9.2",
+ }
+
+ return permute.order_iter(...)
+end
+
+--- deprecated.
+-- @param ...
+-- @see permute.order_iter
+function permute.table(...)
+ utils.raise_deprecation {
+ source = "Penlight " .. utils._VERSION,
+ message = "function 'table' was renamed to 'order_table'",
+ version_removed = "2.0.0",
+ deprecated_after = "1.9.2",
+ }
+
+ return permute.order_table(...)
+end
+
+return permute
diff --git a/macros/luatex/generic/penlight/pl/pretty.lua b/macros/luatex/generic/penlight/pl/pretty.lua
new file mode 100644
index 0000000000..09add30e24
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/pretty.lua
@@ -0,0 +1,437 @@
+--- Pretty-printing Lua tables.
+-- Also provides a sandboxed Lua table reader and
+-- a function to present large numbers in human-friendly format.
+--
+-- Dependencies: `pl.utils`, `pl.lexer`, `pl.stringx`, `debug`
+-- @module pl.pretty
+
+local append = table.insert
+local concat = table.concat
+local mfloor, mhuge = math.floor, math.huge
+local mtype = math.type
+local utils = require 'pl.utils'
+local lexer = require 'pl.lexer'
+local debug = require 'debug'
+local quote_string = require'pl.stringx'.quote_string
+local assert_arg = utils.assert_arg
+
+local original_tostring = tostring
+
+-- Patch tostring to format numbers with better precision
+-- and to produce cross-platform results for
+-- infinite values and NaN.
+local function tostring(value)
+ if type(value) ~= "number" then
+ return original_tostring(value)
+ elseif value ~= value then
+ return "NaN"
+ elseif value == mhuge then
+ return "Inf"
+ elseif value == -mhuge then
+ return "-Inf"
+ elseif (_VERSION ~= "Lua 5.3" or mtype(value) == "integer") and mfloor(value) == value then
+ return ("%d"):format(value)
+ else
+ local res = ("%.14g"):format(value)
+ if _VERSION == "Lua 5.3" and mtype(value) == "float" and not res:find("%.") then
+ -- Number is internally a float but looks like an integer.
+ -- Insert ".0" after first run of digits.
+ res = res:gsub("%d+", "%0.0", 1)
+ end
+ return res
+ end
+end
+
+local pretty = {}
+
+local function save_global_env()
+ local env = {}
+ env.hook, env.mask, env.count = debug.gethook()
+
+ -- env.hook is "external hook" if is a C hook function
+ if env.hook~="external hook" then
+ debug.sethook()
+ end
+
+ env.string_mt = getmetatable("")
+ debug.setmetatable("", nil)
+ return env
+end
+
+local function restore_global_env(env)
+ if env then
+ debug.setmetatable("", env.string_mt)
+ if env.hook~="external hook" then
+ debug.sethook(env.hook, env.mask, env.count)
+ end
+ end
+end
+
+--- Read a string representation of a Lua table.
+-- This function loads and runs the string as Lua code, but bails out
+-- if it contains a function definition.
+-- Loaded string is executed in an empty environment.
+-- @string s string to read in `{...}` format, possibly with some whitespace
+-- before or after the curly braces. A single line comment may be present
+-- at the beginning.
+-- @return a table in case of success.
+-- If loading the string failed, return `nil` and error message.
+-- If executing loaded string failed, return `nil` and the error it raised.
+function pretty.read(s)
+ assert_arg(1,s,'string')
+ if s:find '^%s*%-%-' then -- may start with a comment..
+ s = s:gsub('%-%-.-\n','')
+ end
+ if not s:find '^%s*{' then return nil,"not a Lua table" end
+ if s:find '[^\'"%w_]function[^\'"%w_]' then
+ local tok = lexer.lua(s)
+ for t,v in tok do
+ if t == 'keyword' and v == 'function' then
+ return nil,"cannot have functions in table definition"
+ end
+ end
+ end
+ s = 'return '..s
+ local chunk,err = utils.load(s,'tbl','t',{})
+ if not chunk then return nil,err end
+ local global_env = save_global_env()
+ local ok,ret = pcall(chunk)
+ restore_global_env(global_env)
+ if ok then return ret
+ else
+ return nil,ret
+ end
+end
+
+--- Read a Lua chunk.
+-- @string s Lua code.
+-- @tab[opt] env environment used to run the code, empty by default.
+-- @bool[opt] paranoid abort loading if any looping constructs a found in the code
+-- and disable string methods.
+-- @return the environment in case of success or `nil` and syntax or runtime error
+-- if something went wrong.
+function pretty.load (s, env, paranoid)
+ env = env or {}
+ if paranoid then
+ local tok = lexer.lua(s)
+ for t,v in tok do
+ if t == 'keyword'
+ and (v == 'for' or v == 'repeat' or v == 'function' or v == 'goto')
+ then
+ return nil,"looping not allowed"
+ end
+ end
+ end
+ local chunk,err = utils.load(s,'tbl','t',env)
+ if not chunk then return nil,err end
+ local global_env = paranoid and save_global_env()
+ local ok,err = pcall(chunk)
+ restore_global_env(global_env)
+ if not ok then return nil,err end
+ return env
+end
+
+local function quote_if_necessary (v)
+ if not v then return ''
+ else
+ --AAS
+ if v:find ' ' then v = quote_string(v) end
+ end
+ return v
+end
+
+local keywords
+
+local function is_identifier (s)
+ return type(s) == 'string' and s:find('^[%a_][%w_]*$') and not keywords[s]
+end
+
+local function quote (s)
+ if type(s) == 'table' then
+ return pretty.write(s,'')
+ else
+ --AAS
+ return quote_string(s)-- ('%q'):format(tostring(s))
+ end
+end
+
+local function index (numkey,key)
+ --AAS
+ if not numkey then
+ key = quote(key)
+ key = key:find("^%[") and (" " .. key .. " ") or key
+ end
+ return '['..key..']'
+end
+
+
+--- Create a string representation of a Lua table.
+-- This function never fails, but may complain by returning an
+-- extra value. Normally puts out one item per line, using
+-- the provided indent; set the second parameter to an empty string
+-- if you want output on one line.
+--
+-- *NOTE:* this is NOT a serialization function, not a full blown
+-- debug function. Checkout out respectively the
+-- [serpent](https://github.com/pkulchenko/serpent)
+-- or [inspect](https://github.com/kikito/inspect.lua)
+-- Lua modules for that if you need them.
+-- @tab tbl Table to serialize to a string.
+-- @string[opt] space The indent to use.
+-- Defaults to two spaces; pass an empty string for no indentation.
+-- @bool[opt] not_clever Pass `true` for plain output, e.g `{['key']=1}`.
+-- Defaults to `false`.
+-- @return a string
+-- @return an optional error message
+function pretty.write (tbl,space,not_clever)
+ if type(tbl) ~= 'table' then
+ local res = tostring(tbl)
+ if type(tbl) == 'string' then return quote(tbl) end
+ return res, 'not a table'
+ end
+ if not keywords then
+ keywords = lexer.get_keywords()
+ end
+ local set = ' = '
+ if space == '' then set = '=' end
+ space = space or ' '
+ local lines = {}
+ local line = ''
+ local tables = {}
+
+
+ local function put(s)
+ if #s > 0 then
+ line = line..s
+ end
+ end
+
+ local function putln (s)
+ if #line > 0 then
+ line = line..s
+ append(lines,line)
+ line = ''
+ else
+ append(lines,s)
+ end
+ end
+
+ local function eat_last_comma ()
+ local n = #lines
+ local lastch = lines[n]:sub(-1,-1)
+ if lastch == ',' then
+ lines[n] = lines[n]:sub(1,-2)
+ end
+ end
+
+
+ -- safe versions for iterators since 5.3+ honors metamethods that can throw
+ -- errors
+ local ipairs = function(t)
+ local i = 0
+ local ok, v
+ local getter = function() return t[i] end
+ return function()
+ i = i + 1
+ ok, v = pcall(getter)
+ if v == nil or not ok then return end
+ return i, t[i]
+ end
+ end
+ local pairs = function(t)
+ local k, v, ok
+ local getter = function() return next(t, k) end
+ return function()
+ ok, k, v = pcall(getter)
+ if not ok then return end
+ return k, v
+ end
+ end
+
+ local writeit
+ writeit = function (t,oldindent,indent)
+ local tp = type(t)
+ if tp ~= 'string' and tp ~= 'table' then
+ putln(quote_if_necessary(tostring(t))..',')
+ elseif tp == 'string' then
+ -- if t:find('\n') then
+ -- putln('[[\n'..t..']],')
+ -- else
+ -- putln(quote(t)..',')
+ -- end
+ --AAS
+ putln(quote_string(t) ..",")
+ elseif tp == 'table' then
+ if tables[t] then
+ putln('<cycle>,')
+ return
+ end
+ tables[t] = true
+ local newindent = indent..space
+ putln('{')
+ local used = {}
+ if not not_clever then
+ for i,val in ipairs(t) do
+ put(indent)
+ writeit(val,indent,newindent)
+ used[i] = true
+ end
+ end
+ local ordered_keys = {}
+ for k,v in pairs(t) do
+ if type(k) ~= 'number' then
+ ordered_keys[#ordered_keys + 1] = k
+ end
+ end
+ table.sort(ordered_keys, function (a, b)
+ if type(a) == type(b) and type(a) == 'string' then
+ return a < b
+ end
+ return type(a) == 'boolean' or (type(b) ~= 'boolean' and type(a) == 'table')
+ end)
+ local function write_entry (key, val)
+ local tkey = type(key)
+ local numkey = tkey == 'number'
+ if not_clever then
+ key = tostring(key)
+ put(indent..index(numkey,key)..set)
+ writeit(val,indent,newindent)
+ else
+ if not numkey or not used[key] then -- non-array indices
+ if tkey ~= 'string' then
+ key = tostring(key)
+ end
+ if numkey or not is_identifier(key) then
+ key = index(numkey,key)
+ end
+ put(indent..key..set)
+ writeit(val,indent,newindent)
+ end
+ end
+ end
+ for i = 1, #ordered_keys do
+ local key = ordered_keys[i]
+ local val = t[key]
+ write_entry(key, val)
+ end
+ for key,val in pairs(t) do
+ if type(key) == 'number' then
+ write_entry(key, val)
+ end
+ end
+ tables[t] = nil
+ eat_last_comma()
+ putln(oldindent..'},')
+ else
+ putln(tostring(t)..',')
+ end
+ end
+ writeit(tbl,'',space)
+ eat_last_comma()
+ return concat(lines,#space > 0 and '\n' or '')
+end
+
+--- Dump a Lua table out to a file or stdout.
+-- @tab t The table to write to a file or stdout.
+-- @string[opt] filename File name to write too. Defaults to writing
+-- to stdout.
+function pretty.dump (t, filename)
+ if not filename then
+ print(pretty.write(t))
+ return true
+ else
+ return utils.writefile(filename, pretty.write(t))
+ end
+end
+
+--- Dump a series of arguments to stdout for debug purposes.
+-- This function is attached to the module table `__call` method, to make it
+-- extra easy to access. So the full:
+--
+-- print(require("pl.pretty").write({...}))
+--
+-- Can be shortened to:
+--
+-- require"pl.pretty" (...)
+--
+-- Any `nil` entries will be printed as `"<nil>"` to make them explicit.
+-- @param ... the parameters to dump to stdout.
+-- @usage
+-- -- example debug output
+-- require"pl.pretty" ("hello", nil, "world", { bye = "world", true} )
+--
+-- -- output:
+-- {
+-- ["arg 1"] = "hello",
+-- ["arg 2"] = "<nil>",
+-- ["arg 3"] = "world",
+-- ["arg 4"] = {
+-- true,
+-- bye = "world"
+-- }
+-- }
+function pretty.debug(...)
+ local n = select("#", ...)
+ local t = { ... }
+ for i = 1, n do
+ local value = t[i]
+ if value == nil then
+ value = "<nil>"
+ end
+ t[i] = nil
+ t["arg " .. i] = value
+ end
+
+ print(pretty.write(t))
+ return true
+end
+
+
+local memp,nump = {'B','KiB','MiB','GiB'},{'','K','M','B'}
+
+local function comma (val)
+ local thou = math.floor(val/1000)
+ if thou > 0 then return comma(thou)..','.. tostring(val % 1000)
+ else return tostring(val) end
+end
+
+--- Format large numbers nicely for human consumption.
+-- @number num a number.
+-- @string[opt] kind one of `'M'` (memory in `KiB`, `MiB`, etc.),
+-- `'N'` (postfixes are `'K'`, `'M'` and `'B'`),
+-- or `'T'` (use commas as thousands separator), `'N'` by default.
+-- @int[opt] prec number of digits to use for `'M'` and `'N'`, `1` by default.
+function pretty.number (num,kind,prec)
+ local fmt = '%.'..(prec or 1)..'f%s'
+ if kind == 'T' then
+ return comma(num)
+ else
+ local postfixes, fact
+ if kind == 'M' then
+ fact = 1024
+ postfixes = memp
+ else
+ fact = 1000
+ postfixes = nump
+ end
+ local div = fact
+ local k = 1
+ while num >= div and k <= #postfixes do
+ div = div * fact
+ k = k + 1
+ end
+ div = div / fact
+ if k > #postfixes then k = k - 1; div = div/fact end
+ if k > 1 then
+ return fmt:format(num/div,postfixes[k] or 'duh')
+ else
+ return num..postfixes[1]
+ end
+ end
+end
+
+return setmetatable(pretty, {
+ __call = function(self, ...)
+ return self.debug(...)
+ end
+})
diff --git a/macros/luatex/generic/penlight/pl/seq.lua b/macros/luatex/generic/penlight/pl/seq.lua
new file mode 100644
index 0000000000..1c08d2059b
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/seq.lua
@@ -0,0 +1,544 @@
+--- Manipulating iterators as sequences.
+-- See @{07-functional.md.Sequences|The Guide}
+--
+-- Dependencies: `pl.utils`, `pl.types`, `debug`
+-- @module pl.seq
+
+local next,assert,pairs,tonumber,type,setmetatable = next,assert,pairs,tonumber,type,setmetatable
+local strfind,format = string.find,string.format
+local mrandom = math.random
+local tsort,tappend = table.sort,table.insert
+local io = io
+local utils = require 'pl.utils'
+local callable = require 'pl.types'.is_callable
+local function_arg = utils.function_arg
+local assert_arg = utils.assert_arg
+local debug = require 'debug'
+
+local seq = {}
+
+-- given a number, return a function(y) which returns true if y > x
+-- @param x a number
+function seq.greater_than(x)
+ return function(v)
+ return tonumber(v) > x
+ end
+end
+
+-- given a number, returns a function(y) which returns true if y < x
+-- @param x a number
+function seq.less_than(x)
+ return function(v)
+ return tonumber(v) < x
+ end
+end
+
+-- given any value, return a function(y) which returns true if y == x
+-- @param x a value
+function seq.equal_to(x)
+ if type(x) == "number" then
+ return function(v)
+ return tonumber(v) == x
+ end
+ else
+ return function(v)
+ return v == x
+ end
+ end
+end
+
+--- given a string, return a function(y) which matches y against the string.
+-- @param s a string
+function seq.matching(s)
+ return function(v)
+ return strfind(v,s)
+ end
+end
+
+local nexti
+
+--- sequence adaptor for a table. Note that if any generic function is
+-- passed a table, it will automatically use seq.list()
+-- @param t a list-like table
+-- @usage sum(list(t)) is the sum of all elements of t
+-- @usage for x in list(t) do...end
+function seq.list(t)
+ assert_arg(1,t,'table')
+ if not nexti then
+ nexti = ipairs{}
+ end
+ local key,value = 0
+ return function()
+ key,value = nexti(t,key)
+ return value
+ end
+end
+
+--- return the keys of the table.
+-- @param t an arbitrary table
+-- @return iterator over keys
+function seq.keys(t)
+ assert_arg(1,t,'table')
+ local key
+ return function()
+ key = next(t,key)
+ return key
+ end
+end
+
+local list = seq.list
+local function default_iter(iter)
+ if type(iter) == 'table' then return list(iter)
+ else return iter end
+end
+
+seq.iter = default_iter
+
+--- create an iterator over a numerical range. Like the standard Python function xrange.
+-- @param start a number
+-- @param finish a number greater than start
+function seq.range(start,finish)
+ local i = start - 1
+ return function()
+ i = i + 1
+ if i > finish then return nil
+ else return i end
+ end
+end
+
+-- count the number of elements in the sequence which satisfy the predicate
+-- @param iter a sequence
+-- @param condn a predicate function (must return either true or false)
+-- @param optional argument to be passed to predicate as second argument.
+-- @return count
+function seq.count(iter,condn,arg)
+ local i = 0
+ seq.foreach(iter,function(val)
+ if condn(val,arg) then i = i + 1 end
+ end)
+ return i
+end
+
+--- return the minimum and the maximum value of the sequence.
+-- @param iter a sequence
+-- @return minimum value
+-- @return maximum value
+function seq.minmax(iter)
+ local vmin,vmax = 1e70,-1e70
+ for v in default_iter(iter) do
+ v = tonumber(v)
+ if v < vmin then vmin = v end
+ if v > vmax then vmax = v end
+ end
+ return vmin,vmax
+end
+
+--- return the sum and element count of the sequence.
+-- @param iter a sequence
+-- @param fn an optional function to apply to the values
+function seq.sum(iter,fn)
+ local s = 0
+ local i = 0
+ for v in default_iter(iter) do
+ if fn then v = fn(v) end
+ s = s + v
+ i = i + 1
+ end
+ return s,i
+end
+
+--- create a table from the sequence. (This will make the result a List.)
+-- @param iter a sequence
+-- @return a List
+-- @usage copy(list(ls)) is equal to ls
+-- @usage copy(list {1,2,3}) == List{1,2,3}
+function seq.copy(iter)
+ local res,k = {},1
+ for v in default_iter(iter) do
+ res[k] = v
+ k = k + 1
+ end
+ setmetatable(res, require('pl.List'))
+ return res
+end
+
+--- create a table of pairs from the double-valued sequence.
+-- @param iter a double-valued sequence
+-- @param i1 used to capture extra iterator values
+-- @param i2 as with pairs & ipairs
+-- @usage copy2(ipairs{10,20,30}) == {{1,10},{2,20},{3,30}}
+-- @return a list-like table
+function seq.copy2 (iter,i1,i2)
+ local res,k = {},1
+ for v1,v2 in iter,i1,i2 do
+ res[k] = {v1,v2}
+ k = k + 1
+ end
+ return res
+end
+
+--- create a table of 'tuples' from a multi-valued sequence.
+-- A generalization of copy2 above
+-- @param iter a multiple-valued sequence
+-- @return a list-like table
+function seq.copy_tuples (iter)
+ iter = default_iter(iter)
+ local res = {}
+ local row = {iter()}
+ while #row > 0 do
+ tappend(res,row)
+ row = {iter()}
+ end
+ return res
+end
+
+--- return an iterator of random numbers.
+-- @param n the length of the sequence
+-- @param l same as the first optional argument to math.random
+-- @param u same as the second optional argument to math.random
+-- @return a sequence
+function seq.random(n,l,u)
+ local rand
+ assert(type(n) == 'number')
+ if u then
+ rand = function() return mrandom(l,u) end
+ elseif l then
+ rand = function() return mrandom(l) end
+ else
+ rand = mrandom
+ end
+
+ return function()
+ if n == 0 then return nil
+ else
+ n = n - 1
+ return rand()
+ end
+ end
+end
+
+--- return an iterator to the sorted elements of a sequence.
+-- @param iter a sequence
+-- @param comp an optional comparison function (comp(x,y) is true if x < y)
+function seq.sort(iter,comp)
+ local t = seq.copy(iter)
+ tsort(t,comp)
+ return list(t)
+end
+
+--- return an iterator which returns elements of two sequences.
+-- @param iter1 a sequence
+-- @param iter2 a sequence
+-- @usage for x,y in seq.zip(ls1,ls2) do....end
+function seq.zip(iter1,iter2)
+ iter1 = default_iter(iter1)
+ iter2 = default_iter(iter2)
+ return function()
+ return iter1(),iter2()
+ end
+end
+
+--- Makes a table where the key/values are the values and value counts of the sequence.
+-- This version works with 'hashable' values like strings and numbers.
+-- `pl.tablex.count_map` is more general.
+-- @param iter a sequence
+-- @return a map-like table
+-- @return a table
+-- @see pl.tablex.count_map
+function seq.count_map(iter)
+ local t = {}
+ local v
+ for s in default_iter(iter) do
+ v = t[s]
+ if v then t[s] = v + 1
+ else t[s] = 1 end
+ end
+ return setmetatable(t, require('pl.Map'))
+end
+
+-- given a sequence, return all the unique values in that sequence.
+-- @param iter a sequence
+-- @param returns_table true if we return a table, not a sequence
+-- @return a sequence or a table; defaults to a sequence.
+function seq.unique(iter,returns_table)
+ local t = seq.count_map(iter)
+ local res,k = {},1
+ for key in pairs(t) do res[k] = key; k = k + 1 end
+ table.sort(res)
+ if returns_table then
+ return res
+ else
+ return list(res)
+ end
+end
+
+--- print out a sequence iter with a separator.
+-- @param iter a sequence
+-- @param sep the separator (default space)
+-- @param nfields maximum number of values per line (default 7)
+-- @param fmt optional format function for each value
+function seq.printall(iter,sep,nfields,fmt)
+ local write = io.write
+ if not sep then sep = ' ' end
+ if not nfields then
+ if sep == '\n' then nfields = 1e30
+ else nfields = 7 end
+ end
+ if fmt then
+ local fstr = fmt
+ fmt = function(v) return format(fstr,v) end
+ end
+ local k = 1
+ for v in default_iter(iter) do
+ if fmt then v = fmt(v) end
+ if k < nfields then
+ write(v,sep)
+ k = k + 1
+ else
+ write(v,'\n')
+ k = 1
+ end
+ end
+ write '\n'
+end
+
+-- return an iterator running over every element of two sequences (concatenation).
+-- @param iter1 a sequence
+-- @param iter2 a sequence
+function seq.splice(iter1,iter2)
+ iter1 = default_iter(iter1)
+ iter2 = default_iter(iter2)
+ local iter = iter1
+ return function()
+ local ret = iter()
+ if ret == nil then
+ if iter == iter1 then
+ iter = iter2
+ return iter()
+ else return nil end
+ else
+ return ret
+ end
+ end
+end
+
+--- return a sequence where every element of a sequence has been transformed
+-- by a function. If you don't supply an argument, then the function will
+-- receive both values of a double-valued sequence, otherwise behaves rather like
+-- tablex.map.
+-- @param fn a function to apply to elements; may take two arguments
+-- @param iter a sequence of one or two values
+-- @param arg optional argument to pass to function.
+function seq.map(fn,iter,arg)
+ fn = function_arg(1,fn)
+ iter = default_iter(iter)
+ return function()
+ local v1,v2 = iter()
+ if v1 == nil then return nil end
+ return fn(v1,arg or v2) or false
+ end
+end
+
+--- filter a sequence using a predicate function.
+-- @param iter a sequence of one or two values
+-- @param pred a boolean function; may take two arguments
+-- @param arg optional argument to pass to function.
+function seq.filter (iter,pred,arg)
+ pred = function_arg(2,pred)
+ return function ()
+ local v1,v2
+ while true do
+ v1,v2 = iter()
+ if v1 == nil then return nil end
+ if pred(v1,arg or v2) then return v1,v2 end
+ end
+ end
+end
+
+--- 'reduce' a sequence using a binary function.
+-- @func fn a function of two arguments
+-- @param iter a sequence
+-- @param initval optional initial value
+-- @usage seq.reduce(operator.add,seq.list{1,2,3,4}) == 10
+-- @usage seq.reduce('-',{1,2,3,4,5}) == -13
+function seq.reduce (fn,iter,initval)
+ fn = function_arg(1,fn)
+ iter = default_iter(iter)
+ local val = initval or iter()
+ if val == nil then return nil end
+ for v in iter do
+ val = fn(val,v)
+ end
+ return val
+end
+
+--- take the first n values from the sequence.
+-- @param iter a sequence of one or two values
+-- @param n number of items to take
+-- @return a sequence of at most n items
+function seq.take (iter,n)
+ iter = default_iter(iter)
+ return function()
+ if n < 1 then return end
+ local val1,val2 = iter()
+ if not val1 then return end
+ n = n - 1
+ return val1,val2
+ end
+end
+
+--- skip the first n values of a sequence
+-- @param iter a sequence of one or more values
+-- @param n number of items to skip
+function seq.skip (iter,n)
+ n = n or 1
+ for i = 1,n do
+ if iter() == nil then return list{} end
+ end
+ return iter
+end
+
+--- a sequence with a sequence count and the original value.
+-- enum(copy(ls)) is a roundabout way of saying ipairs(ls).
+-- @param iter a single or double valued sequence
+-- @return sequence of (i,v), i = 1..n and v is from iter.
+function seq.enum (iter)
+ local i = 0
+ iter = default_iter(iter)
+ return function ()
+ local val1,val2 = iter()
+ if not val1 then return end
+ i = i + 1
+ return i,val1,val2
+ end
+end
+
+--- map using a named method over a sequence.
+-- @param iter a sequence
+-- @param name the method name
+-- @param arg1 optional first extra argument
+-- @param arg2 optional second extra argument
+function seq.mapmethod (iter,name,arg1,arg2)
+ iter = default_iter(iter)
+ return function()
+ local val = iter()
+ if not val then return end
+ local fn = val[name]
+ if not fn then error(type(val).." does not have method "..name) end
+ return fn(val,arg1,arg2)
+ end
+end
+
+--- a sequence of (last,current) values from another sequence.
+-- This will return S(i-1),S(i) if given S(i)
+-- @param iter a sequence
+function seq.last (iter)
+ iter = default_iter(iter)
+ local val, l = iter(), nil
+ if val == nil then return list{} end
+ return function ()
+ val,l = iter(),val
+ if val == nil then return nil end
+ return val,l
+ end
+end
+
+--- call the function on each element of the sequence.
+-- @param iter a sequence with up to 3 values
+-- @param fn a function
+function seq.foreach(iter,fn)
+ fn = function_arg(2,fn)
+ for i1,i2,i3 in default_iter(iter) do fn(i1,i2,i3) end
+end
+
+---------------------- Sequence Adapters ---------------------
+
+local SMT
+
+local function SW (iter,...)
+ if callable(iter) then
+ return setmetatable({iter=iter},SMT)
+ else
+ return iter,...
+ end
+end
+
+
+-- can't directly look these up in seq because of the wrong argument order...
+local map,reduce,mapmethod = seq.map, seq.reduce, seq.mapmethod
+local overrides = {
+ map = function(self,fun,arg)
+ return map(fun,self,arg)
+ end,
+ reduce = function(self,fun,initval)
+ return reduce(fun,self,initval)
+ end
+}
+
+SMT = {
+ __index = function (tbl,key)
+ local fn = overrides[key] or seq[key]
+ if fn then
+ return function(sw,...) return SW(fn(sw.iter,...)) end
+ else
+ return function(sw,...) return SW(mapmethod(sw.iter,key,...)) end
+ end
+ end,
+ __call = function (sw)
+ return sw.iter()
+ end,
+}
+
+setmetatable(seq,{
+ __call = function(tbl,iter,extra)
+ if not callable(iter) then
+ if type(iter) == 'table' then iter = seq.list(iter)
+ else return iter
+ end
+ end
+ if extra then
+ return setmetatable({iter=function()
+ return iter(extra)
+ end},SMT)
+ else
+ return setmetatable({iter=iter},SMT)
+ end
+ end
+})
+
+--- create a wrapped iterator over all lines in the file.
+-- @param f either a filename, file-like object, or 'STDIN' (for standard input)
+-- @param ... for Lua 5.2 only, optional format specifiers, as in `io.read`.
+-- @return a sequence wrapper
+function seq.lines (f,...)
+ local iter,obj
+ if f == 'STDIN' then
+ f = io.stdin
+ elseif type(f) == 'string' then
+ iter,obj = io.lines(f,...)
+ elseif not f.read then
+ error("Pass either a string or a file-like object",2)
+ end
+ if not iter then
+ iter,obj = f:lines(...)
+ end
+ if obj then -- LuaJIT version returns a function operating on a file
+ local lines,file = iter,obj
+ iter = function() return lines(file) end
+ end
+ return SW(iter)
+end
+
+function seq.import ()
+ debug.setmetatable(function() end,{
+ __index = function(tbl,key)
+ local s = overrides[key] or seq[key]
+ if s then return s
+ else
+ return function(s,...) return seq.mapmethod(s,key,...) end
+ end
+ end
+ })
+end
+
+return seq
diff --git a/macros/luatex/generic/penlight/pl/sip.lua b/macros/luatex/generic/penlight/pl/sip.lua
new file mode 100644
index 0000000000..5d1f2aa0cc
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/sip.lua
@@ -0,0 +1,337 @@
+--- Simple Input Patterns (SIP).
+-- SIP patterns start with '$', then a
+-- one-letter type, and then an optional variable in curly braces.
+--
+-- sip.match('$v=$q','name="dolly"',res)
+-- ==> res=={'name','dolly'}
+-- sip.match('($q{first},$q{second})','("john","smith")',res)
+-- ==> res=={second='smith',first='john'}
+--
+-- Type names:
+--
+-- v identifier
+-- i integer
+-- f floating-point
+-- q quoted string
+-- ([{< match up to closing bracket
+--
+-- See @{08-additional.md.Simple_Input_Patterns|the Guide}
+--
+-- @module pl.sip
+
+local loadstring = rawget(_G,'loadstring') or load
+local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
+
+local append,concat = table.insert,table.concat
+local ipairs,type = ipairs,type
+local io,_G = io,_G
+local print,rawget = print,rawget
+
+local patterns = {
+ FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*',
+ INTEGER = '[+%-%d]%d*',
+ IDEN = '[%a_][%w_]*',
+ OPTION = '[%a_][%w_%-]*',
+}
+
+local function assert_arg(idx,val,tp)
+ if type(val) ~= tp then
+ error("argument "..idx.." must be "..tp, 2)
+ end
+end
+
+local sip = {}
+
+local brackets = {['<'] = '>', ['('] = ')', ['{'] = '}', ['['] = ']' }
+local stdclasses = {a=1,c=0,d=1,l=1,p=0,u=1,w=1,x=1,s=0}
+
+local function group(s)
+ return '('..s..')'
+end
+
+-- escape all magic characters except $, which has special meaning
+-- Also, un-escape any characters after $, so $( and $[ passes through as is.
+local function escape (spec)
+ return (spec:gsub('[%-%.%+%[%]%(%)%^%%%?%*]','%%%0'):gsub('%$%%(%S)','$%1'))
+end
+
+-- Most spaces within patterns can match zero or more spaces.
+-- Spaces between alphanumeric characters or underscores or between
+-- patterns that can match these characters, however, must match at least
+-- one space. Otherwise '$v $v' would match 'abcd' as {'abc', 'd'}.
+-- This function replaces continuous spaces within a pattern with either
+-- '%s*' or '%s+' according to this rule. The pattern has already
+-- been stripped of pattern names by now.
+local function compress_spaces(patt)
+ return (patt:gsub("()%s+()", function(i1, i2)
+ local before = patt:sub(i1 - 2, i1 - 1)
+ if before:match('%$[vifadxlu]') or before:match('^[^%$]?[%w_]$') then
+ local after = patt:sub(i2, i2 + 1)
+ if after:match('%$[vifadxlu]') or after:match('^[%w_]') then
+ return '%s+'
+ end
+ end
+ return '%s*'
+ end))
+end
+
+local pattern_map = {
+ v = group(patterns.IDEN),
+ i = group(patterns.INTEGER),
+ f = group(patterns.FLOAT),
+ o = group(patterns.OPTION),
+ r = '(%S.*)',
+ p = '([%a]?[:]?[\\/%.%w_]+)'
+}
+
+function sip.custom_pattern(flag,patt)
+ pattern_map[flag] = patt
+end
+
+--- convert a SIP pattern into the equivalent Lua string pattern.
+-- @param spec a SIP pattern
+-- @param options a table; only the <code>at_start</code> field is
+-- currently meaningful and ensures that the pattern is anchored
+-- at the start of the string.
+-- @return a Lua string pattern.
+function sip.create_pattern (spec,options)
+ assert_arg(1,spec,'string')
+ local fieldnames,fieldtypes = {},{}
+
+ if type(spec) == 'string' then
+ spec = escape(spec)
+ else
+ local res = {}
+ for i,s in ipairs(spec) do
+ res[i] = escape(s)
+ end
+ spec = concat(res,'.-')
+ end
+
+ local kount = 1
+
+ local function addfield (name,type)
+ name = name or kount
+ append(fieldnames,name)
+ fieldtypes[name] = type
+ kount = kount + 1
+ end
+
+ local named_vars = spec:find('{%a+}')
+
+ if options and options.at_start then
+ spec = '^'..spec
+ end
+ if spec:sub(-1,-1) == '$' then
+ spec = spec:sub(1,-2)..'$r'
+ if named_vars then spec = spec..'{rest}' end
+ end
+
+ local names
+
+ if named_vars then
+ names = {}
+ spec = spec:gsub('{(%a+)}',function(name)
+ append(names,name)
+ return ''
+ end)
+ end
+ spec = compress_spaces(spec)
+
+ local k = 1
+ local err
+ local r = (spec:gsub('%$%S',function(s)
+ local type,name
+ type = s:sub(2,2)
+ if names then name = names[k]; k=k+1 end
+ -- this kludge is necessary because %q generates two matches, and
+ -- we want to ignore the first. Not a problem for named captures.
+ if not names and type == 'q' then
+ addfield(nil,'Q')
+ else
+ addfield(name,type)
+ end
+ local res
+ if pattern_map[type] then
+ res = pattern_map[type]
+ elseif type == 'q' then
+ -- some Lua pattern matching voodoo; we want to match '...' as
+ -- well as "...", and can use the fact that %n will match a
+ -- previous capture. Adding the extra field above comes from needing
+ -- to accommodate the extra spurious match (which is either ' or ")
+ addfield(name,type)
+ res = '(["\'])(.-)%'..(kount-2)
+ else
+ local endbracket = brackets[type]
+ if endbracket then
+ res = '(%b'..type..endbracket..')'
+ elseif stdclasses[type] or stdclasses[type:lower()] then
+ res = '(%'..type..'+)'
+ else
+ err = "unknown format type or character class"
+ end
+ end
+ return res
+ end))
+
+ if err then
+ return nil,err
+ else
+ return r,fieldnames,fieldtypes
+ end
+end
+
+
+local function tnumber (s)
+ return s == 'd' or s == 'i' or s == 'f'
+end
+
+function sip.create_spec_fun(spec,options)
+ local fieldtypes,fieldnames
+ local ls = {}
+ spec,fieldnames,fieldtypes = sip.create_pattern(spec,options)
+ if not spec then return spec,fieldnames end
+ local named_vars = type(fieldnames[1]) == 'string'
+ for i = 1,#fieldnames do
+ append(ls,'mm'..i)
+ end
+ ls[1] = ls[1] or "mm1" -- behave correctly if there are no patterns
+ local fun = ('return (function(s,res)\n\tlocal %s = s:match(%q)\n'):format(concat(ls,','),spec)
+ fun = fun..'\tif not mm1 then return false end\n'
+ local k=1
+ for i,f in ipairs(fieldnames) do
+ if f ~= '_' then
+ local var = 'mm'..i
+ if tnumber(fieldtypes[f]) then
+ var = 'tonumber('..var..')'
+ elseif brackets[fieldtypes[f]] then
+ var = var..':sub(2,-2)'
+ end
+ if named_vars then
+ fun = ('%s\tres.%s = %s\n'):format(fun,f,var)
+ else
+ if fieldtypes[f] ~= 'Q' then -- we skip the string-delim capture
+ fun = ('%s\tres[%d] = %s\n'):format(fun,k,var)
+ k = k + 1
+ end
+ end
+ end
+ end
+ return fun..'\treturn true\nend)\n', named_vars
+end
+
+--- convert a SIP pattern into a matching function.
+-- The returned function takes two arguments, the line and an empty table.
+-- If the line matched the pattern, then this function returns true
+-- and the table is filled with field-value pairs.
+-- @param spec a SIP pattern
+-- @param options optional table; {at_start=true} ensures that the pattern
+-- is anchored at the start of the string.
+-- @return a function if successful, or nil,error
+function sip.compile(spec,options)
+ assert_arg(1,spec,'string')
+ local fun,names = sip.create_spec_fun(spec,options)
+ if not fun then return nil,names end
+ if rawget(_G,'_DEBUG') then print(fun) end
+ local chunk,err = loadstring(fun,'tmp')
+ if err then return nil,err end
+ return chunk(),names
+end
+
+local cache = {}
+
+--- match a SIP pattern against a string.
+-- @param spec a SIP pattern
+-- @param line a string
+-- @param res a table to receive values
+-- @param options (optional) option table
+-- @return true or false
+function sip.match (spec,line,res,options)
+ assert_arg(1,spec,'string')
+ assert_arg(2,line,'string')
+ assert_arg(3,res,'table')
+ if not cache[spec] then
+ cache[spec] = sip.compile(spec,options)
+ end
+ return cache[spec](line,res)
+end
+
+--- match a SIP pattern against the start of a string.
+-- @param spec a SIP pattern
+-- @param line a string
+-- @param res a table to receive values
+-- @return true or false
+function sip.match_at_start (spec,line,res)
+ return sip.match(spec,line,res,{at_start=true})
+end
+
+--- given a pattern and a file object, return an iterator over the results
+-- @param spec a SIP pattern
+-- @param f a file-like object.
+function sip.fields (spec,f)
+ assert_arg(1,spec,'string')
+ if not f then return nil,"no file object" end
+ local fun,err = sip.compile(spec)
+ if not fun then return nil,err end
+ local res = {}
+ return function()
+ while true do
+ local line = f:read()
+ if not line then return end
+ if fun(line,res) then
+ local values = res
+ res = {}
+ return unpack(values)
+ end
+ end
+ end
+end
+
+local read_patterns = {}
+
+--- register a match which will be used in the read function.
+-- @string spec a SIP pattern
+-- @func fun a function to be called with the results of the match
+-- @see read
+function sip.pattern (spec,fun)
+ assert_arg(1,spec,'string')
+ local pat,named = sip.compile(spec)
+ append(read_patterns,{pat=pat,named=named,callback=fun})
+end
+
+--- enter a loop which applies all registered matches to the input file.
+-- @param f a file-like object
+-- @array matches optional list of `{spec,fun}` pairs, as for `pattern` above.
+function sip.read (f,matches)
+ local owned,err
+ if not f then return nil,"no file object" end
+ if type(f) == 'string' then
+ f,err = io.open(f)
+ if not f then return nil,err end
+ owned = true
+ end
+ if matches then
+ for _,p in ipairs(matches) do
+ sip.pattern(p[1],p[2])
+ end
+ end
+ local res = {}
+ for line in f:lines() do
+ for _,item in ipairs(read_patterns) do
+ if item.pat(line,res) then
+ if item.callback then
+ if item.named then
+ item.callback(res)
+ else
+ item.callback(unpack(res))
+ end
+ end
+ res = {}
+ break
+ end
+ end
+ end
+ if owned then f:close() end
+end
+
+return sip
diff --git a/macros/luatex/generic/penlight/pl/strict.lua b/macros/luatex/generic/penlight/pl/strict.lua
new file mode 100644
index 0000000000..67cbf5b7f2
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/strict.lua
@@ -0,0 +1,138 @@
+--- Checks uses of undeclared global variables.
+-- All global variables must be 'declared' through a regular assignment
+-- (even assigning `nil` will do) in a main chunk before being used
+-- anywhere or assigned to inside a function. Existing metatables `__newindex` and `__index`
+-- metamethods are respected.
+--
+-- You can set any table to have strict behaviour using `strict.module`. Creating a new
+-- module with `strict.closed_module` makes the module immune to monkey-patching, if
+-- you don't wish to encourage monkey business.
+--
+-- If the global `PENLIGHT_NO_GLOBAL_STRICT` is defined, then this module won't make the
+-- global environment strict - if you just want to explicitly set table strictness.
+--
+-- @module pl.strict
+
+require 'debug' -- for Lua 5.2
+local getinfo, error, rawset, rawget = debug.getinfo, error, rawset, rawget
+local strict = {}
+
+local function what ()
+ local d = getinfo(3, "S")
+ return d and d.what or "C"
+end
+
+--- make an existing table strict.
+-- @string[opt] name name of table
+-- @tab[opt] mod the table to protect - if `nil` then we'll return a new table
+-- @tab[opt] predeclared - table of variables that are to be considered predeclared.
+-- @return the given table, or a new table
+-- @usage
+-- local M = { hello = "world" }
+-- strict.module ("Awesome_Module", M, {
+-- Lua = true, -- defines allowed keys
+-- })
+--
+-- assert(M.hello == "world")
+-- assert(M.Lua == nil) -- access allowed, but has no value yet
+-- M.Lua = "Rocks"
+-- assert(M.Lua == "Rocks")
+-- M.not_allowed = "bad boy" -- throws an error
+function strict.module (name,mod,predeclared)
+ local mt, old_newindex, old_index, old_index_type, global
+ if predeclared then
+ global = predeclared.__global
+ end
+ if type(mod) == 'table' then
+ mt = getmetatable(mod)
+ if mt and rawget(mt,'__declared') then return end -- already patched...
+ else
+ mod = {}
+ end
+ if mt == nil then
+ mt = {}
+ setmetatable(mod, mt)
+ else
+ old_newindex = mt.__newindex
+ old_index = mt.__index
+ old_index_type = type(old_index)
+ end
+ mt.__declared = predeclared or {}
+ mt.__newindex = function(t, n, v)
+ if old_newindex then
+ old_newindex(t, n, v)
+ if rawget(t,n)~=nil then return end
+ end
+ if not mt.__declared[n] then
+ if global then
+ local w = what()
+ if w ~= "main" and w ~= "C" then
+ error("assign to undeclared global '"..n.."'", 2)
+ end
+ end
+ mt.__declared[n] = true
+ end
+ rawset(t, n, v)
+ end
+ mt.__index = function(t,n)
+ if not mt.__declared[n] and what() ~= "C" then
+ if old_index then
+ if old_index_type == "table" then
+ local fallback = old_index[n]
+ if fallback ~= nil then
+ return fallback
+ end
+ else
+ local res = old_index(t, n)
+ if res ~= nil then
+ return res
+ end
+ end
+ end
+ local msg = "variable '"..n.."' is not declared"
+ if name then
+ msg = msg .. " in '"..tostring(name).."'"
+ end
+ error(msg, 2)
+ end
+ return rawget(t, n)
+ end
+ return mod
+end
+
+--- make all tables in a table strict.
+-- So `strict.make_all_strict(_G)` prevents monkey-patching
+-- of any global table
+-- @tab T the table containing the tables to protect. Table `T` itself will NOT be protected.
+function strict.make_all_strict (T)
+ for k,v in pairs(T) do
+ if type(v) == 'table' and v ~= T then
+ strict.module(k,v)
+ end
+ end
+end
+
+--- make a new module table which is closed to further changes.
+-- @tab mod module table
+-- @string name module name
+function strict.closed_module (mod,name)
+ -- No clue to what this is useful for? see tests
+ -- Deprecate this and remove???
+ local M = {}
+ mod = mod or {}
+ local mt = getmetatable(mod)
+ if not mt then
+ mt = {}
+ setmetatable(mod,mt)
+ end
+ mt.__newindex = function(t,k,v)
+ M[k] = v
+ end
+ return strict.module(name,M)
+end
+
+if not rawget(_G,'PENLIGHT_NO_GLOBAL_STRICT') then
+ strict.module(nil,_G,{_PROMPT=true,_PROMPT2=true,__global=true})
+end
+
+return strict
diff --git a/macros/luatex/generic/penlight/pl/stringio.lua b/macros/luatex/generic/penlight/pl/stringio.lua
new file mode 100644
index 0000000000..666f415818
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/stringio.lua
@@ -0,0 +1,158 @@
+--- Reading and writing strings using file-like objects. <br>
+--
+-- f = stringio.open(text)
+-- l1 = f:read() -- read first line
+-- n,m = f:read ('*n','*n') -- read two numbers
+-- for line in f:lines() do print(line) end -- iterate over all lines
+-- f = stringio.create()
+-- f:write('hello')
+-- f:write('dolly')
+-- assert(f:value(),'hellodolly')
+--
+-- See @{03-strings.md.File_style_I_O_on_Strings|the Guide}.
+-- @module pl.stringio
+
+local unpack = rawget(_G,'unpack') or rawget(table,'unpack')
+local tonumber = tonumber
+local concat,append = table.concat,table.insert
+
+local stringio = {}
+
+-- Writer class
+local SW = {}
+SW.__index = SW
+
+local function xwrite(self,...)
+ local args = {...} --arguments may not be nil!
+ for i = 1, #args do
+ append(self.tbl,args[i])
+ end
+end
+
+function SW:write(arg1,arg2,...)
+ if arg2 then
+ xwrite(self,arg1,arg2,...)
+ else
+ append(self.tbl,arg1)
+ end
+end
+
+function SW:writef(fmt,...)
+ self:write(fmt:format(...))
+end
+
+function SW:value()
+ return concat(self.tbl)
+end
+
+function SW:__tostring()
+ return self:value()
+end
+
+function SW:close() -- for compatibility only
+end
+
+function SW:seek()
+end
+
+-- Reader class
+local SR = {}
+SR.__index = SR
+
+function SR:_read(fmt)
+ local i,str = self.i,self.str
+ local sz = #str
+ if i > sz then return nil end
+ local res
+ if fmt == '*l' or fmt == '*L' then
+ local idx = str:find('\n',i) or (sz+1)
+ res = str:sub(i,fmt == '*l' and idx-1 or idx)
+ self.i = idx+1
+ elseif fmt == '*a' then
+ res = str:sub(i)
+ self.i = sz
+ elseif fmt == '*n' then
+ local _,i2,idx
+ _,idx = str:find ('%s*%d+',i)
+ _,i2 = str:find ('^%.%d+',idx+1)
+ if i2 then idx = i2 end
+ _,i2 = str:find ('^[eE][%+%-]*%d+',idx+1)
+ if i2 then idx = i2 end
+ local val = str:sub(i,idx)
+ res = tonumber(val)
+ self.i = idx+1
+ elseif type(fmt) == 'number' then
+ res = str:sub(i,i+fmt-1)
+ self.i = i + fmt
+ else
+ error("bad read format",2)
+ end
+ return res
+end
+
+function SR:read(...)
+ if select('#',...) == 0 then
+ return self:_read('*l')
+ else
+ local res, fmts = {},{...}
+ for i = 1, #fmts do
+ res[i] = self:_read(fmts[i])
+ end
+ return unpack(res)
+ end
+end
+
+function SR:seek(whence,offset)
+ local base
+ whence = whence or 'cur'
+ offset = offset or 0
+ if whence == 'set' then
+ base = 1
+ elseif whence == 'cur' then
+ base = self.i
+ elseif whence == 'end' then
+ base = #self.str
+ end
+ self.i = base + offset
+ return self.i
+end
+
+function SR:lines(...)
+ local n, args = select('#',...)
+ if n > 0 then
+ args = {...}
+ end
+ return function()
+ if n == 0 then
+ return self:_read '*l'
+ else
+ return self:read(unpack(args))
+ end
+ end
+end
+
+function SR:close() -- for compatibility only
+end
+
+--- create a file-like object which can be used to construct a string.
+-- The resulting object has an extra `value()` method for
+-- retrieving the string value. Implements `file:write`, `file:seek`, `file:lines`,
+-- plus an extra `writef` method which works like `utils.printf`.
+-- @usage f = create(); f:write('hello, dolly\n'); print(f:value())
+function stringio.create()
+ return setmetatable({tbl={}},SW)
+end
+
+--- create a file-like object for reading from a given string.
+-- Implements `file:read`.
+-- @string s The input string.
+-- @usage fs = open '20 10'; x,y = f:read ('*n','*n'); assert(x == 20 and y == 10)
+function stringio.open(s)
+ return setmetatable({str=s,i=1},SR)
+end
+
+function stringio.lines(s,...)
+ return stringio.open(s):lines(...)
+end
+
+return stringio
diff --git a/macros/luatex/generic/penlight/pl/stringx.lua b/macros/luatex/generic/penlight/pl/stringx.lua
new file mode 100644
index 0000000000..627611316b
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/stringx.lua
@@ -0,0 +1,594 @@
+--- Python-style extended string library.
+--
+-- see 3.6.1 of the Python reference.
+-- If you want to make these available as string methods, then say
+-- `stringx.import()` to bring them into the standard `string` table.
+--
+-- See @{03-strings.md|the Guide}
+--
+-- Dependencies: `pl.utils`
+-- @module pl.stringx
+local utils = require 'pl.utils'
+local string = string
+local find = string.find
+local type,setmetatable,ipairs = type,setmetatable,ipairs
+local error = error
+local gsub = string.gsub
+local rep = string.rep
+local sub = string.sub
+local reverse = string.reverse
+local concat = table.concat
+local append = table.insert
+local escape = utils.escape
+local ceil, max = math.ceil, math.max
+local assert_arg,usplit = utils.assert_arg,utils.split
+local lstrip
+
+local function assert_string (n,s)
+ assert_arg(n,s,'string')
+end
+
+local function non_empty(s)
+ return #s > 0
+end
+
+local function assert_nonempty_string(n,s)
+ assert_arg(n,s,'string',non_empty,'must be a non-empty string')
+end
+
+local function makelist(l)
+ return setmetatable(l, require('pl.List'))
+end
+
+local stringx = {}
+
+------------------
+-- String Predicates
+-- @section predicates
+
+--- does s only contain alphabetic characters?
+-- @string s a string
+function stringx.isalpha(s)
+ assert_string(1,s)
+ return find(s,'^%a+$') == 1
+end
+
+--- does s only contain digits?
+-- @string s a string
+function stringx.isdigit(s)
+ assert_string(1,s)
+ return find(s,'^%d+$') == 1
+end
+
+--- does s only contain alphanumeric characters?
+-- @string s a string
+function stringx.isalnum(s)
+ assert_string(1,s)
+ return find(s,'^%w+$') == 1
+end
+
+--- does s only contain spaces?
+-- @string s a string
+function stringx.isspace(s)
+ assert_string(1,s)
+ return find(s,'^%s+$') == 1
+end
+
+--- does s only contain lower case characters?
+-- @string s a string
+function stringx.islower(s)
+ assert_string(1,s)
+ return find(s,'^[%l%s]+$') == 1
+end
+
+--- does s only contain upper case characters?
+-- @string s a string
+function stringx.isupper(s)
+ assert_string(1,s)
+ return find(s,'^[%u%s]+$') == 1
+end
+
+local function raw_startswith(s, prefix)
+ return find(s,prefix,1,true) == 1
+end
+
+local function raw_endswith(s, suffix)
+ return #s >= #suffix and find(s, suffix, #s-#suffix+1, true) and true or false
+end
+
+local function test_affixes(s, affixes, fn)
+ if type(affixes) == 'string' then
+ return fn(s,affixes)
+ elseif type(affixes) == 'table' then
+ for _,affix in ipairs(affixes) do
+ if fn(s,affix) then return true end
+ end
+ return false
+ else
+ error(("argument #2 expected a 'string' or a 'table', got a '%s'"):format(type(affixes)))
+ end
+end
+
+--- does s start with prefix or one of prefixes?
+-- @string s a string
+-- @param prefix a string or an array of strings
+function stringx.startswith(s,prefix)
+ assert_string(1,s)
+ return test_affixes(s,prefix,raw_startswith)
+end
+
+--- does s end with suffix or one of suffixes?
+-- @string s a string
+-- @param suffix a string or an array of strings
+function stringx.endswith(s,suffix)
+ assert_string(1,s)
+ return test_affixes(s,suffix,raw_endswith)
+end
+
+--- Strings and Lists
+-- @section lists
+
+--- concatenate the strings using this string as a delimiter.
+-- Note that the arguments are reversed from `string.concat`.
+-- @string s the string
+-- @param seq a table of strings or numbers
+-- @usage stringx.join(' ', {1,2,3}) == '1 2 3'
+function stringx.join(s,seq)
+ assert_string(1,s)
+ return concat(seq,s)
+end
+
+--- Split a string into a list of lines.
+-- `"\r"`, `"\n"`, and `"\r\n"` are considered line ends.
+-- They are not included in the lines unless `keepends` is passed.
+-- Terminal line end does not produce an extra line.
+-- Splitting an empty string results in an empty list.
+-- @string s the string.
+-- @bool[opt] keep_ends include line ends.
+-- @return List of lines
+function stringx.splitlines(s, keep_ends)
+ assert_string(1, s)
+ local res = {}
+ local pos = 1
+ while true do
+ local line_end_pos = find(s, '[\r\n]', pos)
+ if not line_end_pos then
+ break
+ end
+
+ local line_end = sub(s, line_end_pos, line_end_pos)
+ if line_end == '\r' and sub(s, line_end_pos + 1, line_end_pos + 1) == '\n' then
+ line_end = '\r\n'
+ end
+
+ local line = sub(s, pos, line_end_pos - 1)
+ if keep_ends then
+ line = line .. line_end
+ end
+ append(res, line)
+
+ pos = line_end_pos + #line_end
+ end
+
+ if pos <= #s then
+ append(res, sub(s, pos))
+ end
+ return makelist(res)
+end
+
+--- split a string into a list of strings using a delimiter.
+-- @function split
+-- @string s the string
+-- @string[opt] re a delimiter (defaults to whitespace)
+-- @int[opt] n maximum number of results
+-- @return List
+-- @usage #(stringx.split('one two')) == 2
+-- @usage stringx.split('one,two,three', ',') == List{'one','two','three'}
+-- @usage stringx.split('one,two,three', ',', 2) == List{'one','two,three'}
+function stringx.split(s,re,n)
+ assert_string(1,s)
+ local plain = true
+ if not re then -- default spaces
+ s = lstrip(s)
+ plain = false
+ end
+ local res = usplit(s,re,plain,n)
+ if re and re ~= '' and
+ find(s,re,-#re,true) and
+ (n or math.huge) > #res then
+ res[#res+1] = ""
+ end
+ return makelist(res)
+end
+
+--- replace all tabs in s with tabsize spaces. If not specified, tabsize defaults to 8.
+-- Tab stops will be honored.
+-- @string s the string
+-- @int tabsize[opt=8] number of spaces to expand each tab
+-- @return expanded string
+-- @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)
+ return before_tab .. (" "):rep(tabsize - #before_tab % tabsize)
+ end))
+end
+
+--- Finding and Replacing
+-- @section find
+
+local function _find_all(s,sub,first,last,allow_overlap)
+ first = first or 1
+ last = last or #s
+ if sub == '' then return last+1,last-first+1 end
+ local i1,i2 = find(s,sub,first,true)
+ local res
+ local k = 0
+ while i1 do
+ if last and i2 > last then break end
+ res = i1
+ k = k + 1
+ if allow_overlap then
+ i1,i2 = find(s,sub,i1+1,true)
+ else
+ i1,i2 = find(s,sub,i2+1,true)
+ end
+ end
+ return res,k
+end
+
+--- find index of first instance of sub in s from the left.
+-- @string s the string
+-- @string sub substring
+-- @int[opt] first first index
+-- @int[opt] last last index
+-- @return start index, or nil if not found
+function stringx.lfind(s,sub,first,last)
+ assert_string(1,s)
+ assert_string(2,sub)
+ local i1, i2 = find(s,sub,first,true)
+
+ if i1 and (not last or i2 <= last) then
+ return i1
+ else
+ return nil
+ end
+end
+
+--- find index of first instance of sub in s from the right.
+-- @string s the string
+-- @string sub substring
+-- @int[opt] first first index
+-- @int[opt] last last index
+-- @return start index, or nil if not found
+function stringx.rfind(s,sub,first,last)
+ assert_string(1,s)
+ assert_string(2,sub)
+ return (_find_all(s,sub,first,last,true))
+end
+
+--- replace up to n instances of old by new in the string s.
+-- If n is not present, replace all instances.
+-- @string s the string
+-- @string old the target substring
+-- @string new the substitution
+-- @int[opt] n optional maximum number of substitutions
+-- @return result string
+function stringx.replace(s,old,new,n)
+ assert_string(1,s)
+ assert_string(2,old)
+ assert_string(3,new)
+ return (gsub(s,escape(old),new:gsub('%%','%%%%'),n))
+end
+
+--- count all instances of substring in string.
+-- @string s the string
+-- @string sub substring
+-- @bool[opt] allow_overlap allow matches to overlap
+-- @usage
+-- assert(stringx.count('banana', 'ana') == 1)
+-- assert(stringx.count('banana', 'ana', true) == 2)
+function stringx.count(s,sub,allow_overlap)
+ assert_string(1,s)
+ local _,k = _find_all(s,sub,1,false,allow_overlap)
+ return k
+end
+
+--- Stripping and Justifying
+-- @section strip
+
+local function _just(s,w,ch,left,right)
+ local n = #s
+ if w > n then
+ if not ch then ch = ' ' end
+ local f1,f2
+ if left and right then
+ local rn = ceil((w-n)/2)
+ local ln = w - n - rn
+ f1 = rep(ch,ln)
+ f2 = rep(ch,rn)
+ elseif right then
+ f1 = rep(ch,w-n)
+ f2 = ''
+ else
+ f2 = rep(ch,w-n)
+ f1 = ''
+ end
+ return f1..s..f2
+ else
+ return s
+ end
+end
+
+--- left-justify s with width w.
+-- @string s the string
+-- @int w width of justification
+-- @string[opt=' '] ch padding character
+-- @usage stringx.ljust('hello', 10, '*') == '*****hello'
+function stringx.ljust(s,w,ch)
+ assert_string(1,s)
+ assert_arg(2,w,'number')
+ return _just(s,w,ch,true,false)
+end
+
+--- right-justify s with width w.
+-- @string s the string
+-- @int w width of justification
+-- @string[opt=' '] ch padding character
+-- @usage stringx.rjust('hello', 10, '*') == 'hello*****'
+function stringx.rjust(s,w,ch)
+ assert_string(1,s)
+ assert_arg(2,w,'number')
+ return _just(s,w,ch,false,true)
+end
+
+--- center-justify s with width w.
+-- @string s the string
+-- @int w width of justification
+-- @string[opt=' '] ch padding character
+-- @usage stringx.center('hello', 10, '*') == '**hello***'
+function stringx.center(s,w,ch)
+ assert_string(1,s)
+ assert_arg(2,w,'number')
+ return _just(s,w,ch,true,true)
+end
+
+local function _strip(s,left,right,chrs)
+ if not chrs then
+ chrs = '%s'
+ else
+ chrs = '['..escape(chrs)..']'
+ end
+ local f = 1
+ local t
+ if left then
+ local i1,i2 = find(s,'^'..chrs..'*')
+ if i2 >= i1 then
+ f = i2+1
+ end
+ end
+ if right then
+ if #s < 200 then
+ local i1,i2 = find(s,chrs..'*$',f)
+ if i2 >= i1 then
+ t = i1-1
+ end
+ else
+ local rs = reverse(s)
+ local i1,i2 = find(rs, '^'..chrs..'*')
+ if i2 >= i1 then
+ t = -i2-1
+ end
+ end
+ end
+ return sub(s,f,t)
+end
+
+--- trim any whitespace 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
+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.
+-- @string s the string
+-- @string[opt='%s'] chrs default any whitespace character,
+-- 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.
+-- @string s the string
+-- @string[opt='%s'] chrs default any whitespace character,
+-- but can be a string of characters to be trimmed
+function stringx.strip(s,chrs)
+ assert_string(1,s)
+ return _strip(s,true,true,chrs)
+end
+
+--- Partioning Strings
+-- @section partioning
+
+--- split a string using a pattern. Note that at least one value will be returned!
+-- @string s the string
+-- @string[opt='%s'] re a Lua string pattern (defaults to whitespace)
+-- @return the parts of the string
+-- @usage a,b = line:splitv('=')
+-- @see utils.splitv
+function stringx.splitv(s,re)
+ assert_string(1,s)
+ return utils.splitv(s,re)
+end
+
+-- The partition functions split a string using a delimiter into three parts:
+-- the part before, the delimiter itself, and the part afterwards
+local function _partition(p,delim,fn)
+ local i1,i2 = fn(p,delim)
+ if not i1 or i1 == -1 then
+ return p,'',''
+ else
+ if not i2 then i2 = i1 end
+ return sub(p,1,i1-1),sub(p,i1,i2),sub(p,i2+1)
+ end
+end
+
+--- partition the string using first occurance of a delimiter
+-- @string s the string
+-- @string ch delimiter
+-- @return part before ch
+-- @return ch
+-- @return part after ch
+-- @usage {stringx.partition('a,b,c', ','))} == {'a', ',', 'b,c'}
+-- @usage {stringx.partition('abc', 'x'))} == {'abc', '', ''}
+function stringx.partition(s,ch)
+ assert_string(1,s)
+ assert_nonempty_string(2,ch)
+ return _partition(s,ch,stringx.lfind)
+end
+
+--- partition the string p using last occurance of a delimiter
+-- @string s the string
+-- @string ch delimiter
+-- @return part before ch
+-- @return ch
+-- @return part after ch
+-- @usage {stringx.rpartition('a,b,c', ','))} == {'a,b', ',', 'c'}
+-- @usage {stringx.rpartition('abc', 'x'))} == {'', '', 'abc'}
+function stringx.rpartition(s,ch)
+ assert_string(1,s)
+ assert_nonempty_string(2,ch)
+ local a,b,c = _partition(s,ch,stringx.rfind)
+ if a == s then -- no match found
+ return c,b,a
+ end
+ return a,b,c
+end
+
+--- return the 'character' at the index.
+-- @string s the string
+-- @int idx an index (can be negative)
+-- @return a substring of length 1 if successful, empty string otherwise.
+function stringx.at(s,idx)
+ assert_string(1,s)
+ assert_arg(2,idx,'number')
+ return sub(s,idx,idx)
+end
+
+--- Miscelaneous
+-- @section misc
+
+--- return an iterator over all lines in a string
+-- @string s the string
+-- @return an iterator
+-- @usage
+-- local line_no = 1
+-- for line in stringx.lines(some_text) do
+-- print(line_no, line)
+-- line_no = line_no + 1
+-- end
+function stringx.lines(s)
+ assert_string(1,s)
+ if not s:find '\n$' then s = s..'\n' end
+ return s:gmatch('([^\n]*)\n')
+end
+
+--- inital word letters uppercase ('title case').
+-- Here 'words' mean chunks of non-space characters.
+-- @string s the string
+-- @return a string with each word's first letter uppercase
+-- @usage stringx.title("hello world") == "Hello World")
+function stringx.title(s)
+ assert_string(1,s)
+ return (s:gsub('(%S)(%S*)',function(f,r)
+ return f:upper()..r:lower()
+ end))
+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
+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
+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
+
+function stringx.import()
+ utils.import(stringx,string)
+end
+
+return stringx
diff --git a/macros/luatex/generic/penlight/pl/tablex.lua b/macros/luatex/generic/penlight/pl/tablex.lua
new file mode 100644
index 0000000000..81f739529e
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/tablex.lua
@@ -0,0 +1,999 @@
+--- Extended operations on Lua tables.
+--
+-- See @{02-arrays.md.Useful_Operations_on_Tables|the Guide}
+--
+-- Dependencies: `pl.utils`, `pl.types`
+-- @module pl.tablex
+local utils = require ('pl.utils')
+local types = require ('pl.types')
+local getmetatable,setmetatable,require = getmetatable,setmetatable,require
+local tsort,append,remove = table.sort,table.insert,table.remove
+local min = math.min
+local pairs,type,unpack,select,tostring = pairs,type,utils.unpack,select,tostring
+local function_arg = utils.function_arg
+local assert_arg = utils.assert_arg
+
+local tablex = {}
+
+-- generally, functions that make copies of tables try to preserve the metatable.
+-- However, when the source has no obvious type, then we attach appropriate metatables
+-- like List, Map, etc to the result.
+local function setmeta (res,tbl,pl_class)
+ local mt = getmetatable(tbl) or pl_class and require('pl.' .. pl_class)
+ return mt and setmetatable(res, mt) or res
+end
+
+local function makelist(l)
+ return setmetatable(l, require('pl.List'))
+end
+
+local function makemap(m)
+ return setmetatable(m, require('pl.Map'))
+end
+
+local function complain (idx,msg)
+ error(('argument %d is not %s'):format(idx,msg),3)
+end
+
+local function assert_arg_indexable (idx,val)
+ if not types.is_indexable(val) then
+ complain(idx,"indexable")
+ end
+end
+
+local function assert_arg_iterable (idx,val)
+ if not types.is_iterable(val) then
+ complain(idx,"iterable")
+ end
+end
+
+local function assert_arg_writeable (idx,val)
+ if not types.is_writeable(val) then
+ complain(idx,"writeable")
+ end
+end
+
+--- copy a table into another, in-place.
+-- @within Copying
+-- @tab t1 destination table
+-- @tab t2 source (actually any iterable object)
+-- @return first table
+function tablex.update (t1,t2)
+ assert_arg_writeable(1,t1)
+ assert_arg_iterable(2,t2)
+ for k,v in pairs(t2) do
+ t1[k] = v
+ end
+ return t1
+end
+
+--- total number of elements in this table.
+-- Note that this is distinct from `#t`, which is the number
+-- of values in the array part; this value will always
+-- be greater or equal. The difference gives the size of
+-- the hash part, for practical purposes. Works for any
+-- object with a __pairs metamethod.
+-- @tab t a table
+-- @return the size
+function tablex.size (t)
+ assert_arg_iterable(1,t)
+ local i = 0
+ for k in pairs(t) do i = i + 1 end
+ return i
+end
+
+--- make a shallow copy of a table
+-- @within Copying
+-- @tab t an iterable source
+-- @return new table
+function tablex.copy (t)
+ assert_arg_iterable(1,t)
+ local res = {}
+ for k,v in pairs(t) do
+ res[k] = v
+ end
+ return res
+end
+
+local function cycle_aware_copy(t, cache)
+ if type(t) ~= 'table' then return t end
+ if cache[t] then return cache[t] end
+ assert_arg_iterable(1,t)
+ local res = {}
+ cache[t] = res
+ local mt = getmetatable(t)
+ for k,v in pairs(t) do
+ k = cycle_aware_copy(k, cache)
+ v = cycle_aware_copy(v, cache)
+ res[k] = v
+ end
+ setmetatable(res,mt)
+ return res
+end
+
+--- make a deep copy of a table, recursively copying all the keys and fields.
+-- This supports cycles in tables; cycles will be reproduced in the copy.
+-- This will also set the copied table's metatable to that of the original.
+-- @within Copying
+-- @tab t A table
+-- @return new table
+function tablex.deepcopy(t)
+ return cycle_aware_copy(t,{})
+end
+
+local abs = math.abs
+
+local function cycle_aware_compare(t1,t2,ignore_mt,eps,cache)
+ if cache[t1] and cache[t1][t2] then return true end
+ local ty1 = type(t1)
+ local ty2 = type(t2)
+ if ty1 ~= ty2 then return false end
+ -- non-table types can be directly compared
+ if ty1 ~= 'table' then
+ if ty1 == 'number' and eps then return abs(t1-t2) < eps end
+ return t1 == t2
+ end
+ -- as well as tables which have the metamethod __eq
+ local mt = getmetatable(t1)
+ if not ignore_mt and mt and mt.__eq then return t1 == t2 end
+ for k1 in pairs(t1) do
+ if t2[k1]==nil then return false end
+ end
+ for k2 in pairs(t2) do
+ if t1[k2]==nil then return false end
+ end
+ cache[t1] = cache[t1] or {}
+ cache[t1][t2] = true
+ for k1,v1 in pairs(t1) do
+ local v2 = t2[k1]
+ if not cycle_aware_compare(v1,v2,ignore_mt,eps,cache) then return false end
+ end
+ return true
+end
+
+--- compare two values.
+-- if they are tables, then compare their keys and fields recursively.
+-- @within Comparing
+-- @param t1 A value
+-- @param t2 A value
+-- @bool[opt] ignore_mt if true, ignore __eq metamethod (default false)
+-- @number[opt] eps if defined, then used for any number comparisons
+-- @return true or false
+function tablex.deepcompare(t1,t2,ignore_mt,eps)
+ return cycle_aware_compare(t1,t2,ignore_mt,eps,{})
+end
+
+--- compare two arrays using a predicate.
+-- @within Comparing
+-- @array t1 an array
+-- @array t2 an array
+-- @func cmp A comparison function; `bool = cmp(t1_value, t2_value)`
+-- @return true or false
+-- @usage
+-- assert(tablex.compare({ 1, 2, 3 }, { 1, 2, 3 }, "=="))
+--
+-- assert(tablex.compare(
+-- {1,2,3, hello = "world"}, -- fields are not compared!
+-- {1,2,3}, function(v1, v2) return v1 == v2 end)
+function tablex.compare (t1,t2,cmp)
+ assert_arg_indexable(1,t1)
+ assert_arg_indexable(2,t2)
+ if #t1 ~= #t2 then return false end
+ cmp = function_arg(3,cmp)
+ for k = 1,#t1 do
+ if not cmp(t1[k],t2[k]) then return false end
+ end
+ return true
+end
+
+--- compare two list-like tables using an optional predicate, without regard for element order.
+-- @within Comparing
+-- @array t1 a list-like table
+-- @array t2 a list-like table
+-- @param cmp A comparison function (may be nil)
+function tablex.compare_no_order (t1,t2,cmp)
+ assert_arg_indexable(1,t1)
+ assert_arg_indexable(2,t2)
+ if cmp then cmp = function_arg(3,cmp) end
+ if #t1 ~= #t2 then return false end
+ local visited = {}
+ for i = 1,#t1 do
+ local val = t1[i]
+ local gotcha
+ for j = 1,#t2 do
+ if not visited[j] then
+ local match
+ if cmp then match = cmp(val,t2[j]) else match = val == t2[j] end
+ if match then
+ gotcha = j
+ break
+ end
+ end
+ end
+ if not gotcha then return false end
+ visited[gotcha] = true
+ end
+ return true
+end
+
+
+--- return the index of a value in a list.
+-- Like string.find, there is an optional index to start searching,
+-- which can be negative.
+-- @within Finding
+-- @array t A list-like table
+-- @param val A value
+-- @int idx index to start; -1 means last element,etc (default 1)
+-- @return index of value or nil if not found
+-- @usage find({10,20,30},20) == 2
+-- @usage find({'a','b','a','c'},'a',2) == 3
+function tablex.find(t,val,idx)
+ assert_arg_indexable(1,t)
+ idx = idx or 1
+ if idx < 0 then idx = #t + idx + 1 end
+ for i = idx,#t do
+ if t[i] == val then return i end
+ end
+ return nil
+end
+
+--- return the index of a value in a list, searching from the end.
+-- Like string.find, there is an optional index to start searching,
+-- which can be negative.
+-- @within Finding
+-- @array t A list-like table
+-- @param val A value
+-- @param idx index to start; -1 means last element,etc (default `#t`)
+-- @return index of value or nil if not found
+-- @usage rfind({10,10,10},10) == 3
+function tablex.rfind(t,val,idx)
+ assert_arg_indexable(1,t)
+ idx = idx or #t
+ if idx < 0 then idx = #t + idx + 1 end
+ for i = idx,1,-1 do
+ if t[i] == val then return i end
+ end
+ return nil
+end
+
+
+--- return the index (or key) of a value in a table using a comparison function.
+--
+-- *NOTE*: the 2nd return value of this function, the value returned
+-- by the comparison function, has a limitation that it cannot be `false`.
+-- Because if it is, then it indicates the comparison failed, and the
+-- function will continue the search. See examples.
+-- @within Finding
+-- @tab t A table
+-- @func cmp A comparison function
+-- @param arg an optional second argument to the function
+-- @return index of value, or nil if not found
+-- @return value returned by comparison function (cannot be `false`!)
+-- @usage
+-- -- using an operator
+-- local lst = { "Rudolph", true, false, 15 }
+-- local idx, cmp_result = tablex.rfind(lst, "==", "Rudolph")
+-- assert(idx == 1)
+-- assert(cmp_result == true)
+--
+-- local idx, cmp_result = tablex.rfind(lst, "==", false)
+-- assert(idx == 3)
+-- assert(cmp_result == true) -- looking up 'false' works!
+--
+-- -- using a function returning the value looked up
+-- local cmp = function(v1, v2) return v1 == v2 and v2 end
+-- local idx, cmp_result = tablex.rfind(lst, cmp, "Rudolph")
+-- assert(idx == 1)
+-- assert(cmp_result == "Rudolph") -- the value is returned
+--
+-- -- NOTE: this fails, since 'false' cannot be returned!
+-- local idx, cmp_result = tablex.rfind(lst, cmp, false)
+-- assert(idx == nil) -- looking up 'false' failed!
+-- assert(cmp_result == nil)
+function tablex.find_if(t,cmp,arg)
+ assert_arg_iterable(1,t)
+ cmp = function_arg(2,cmp)
+ for k,v in pairs(t) do
+ local c = cmp(v,arg)
+ if c then return k,c end
+ end
+ return nil
+end
+
+--- return a list of all values in a table indexed by another list.
+-- @tab tbl a table
+-- @array idx an index table (a list of keys)
+-- @return a list-like table
+-- @usage index_by({10,20,30,40},{2,4}) == {20,40}
+-- @usage index_by({one=1,two=2,three=3},{'one','three'}) == {1,3}
+function tablex.index_by(tbl,idx)
+ assert_arg_indexable(1,tbl)
+ assert_arg_indexable(2,idx)
+ local res = {}
+ for i = 1,#idx do
+ res[i] = tbl[idx[i]]
+ end
+ return setmeta(res,tbl,'List')
+end
+
+--- apply a function to all values of a table.
+-- This returns a table of the results.
+-- Any extra arguments are passed to the function.
+-- @within MappingAndFiltering
+-- @func fun A function that takes at least one argument
+-- @tab t A table
+-- @param ... optional arguments
+-- @usage map(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900,fred=4}
+function tablex.map(fun,t,...)
+ assert_arg_iterable(1,t)
+ fun = function_arg(1,fun)
+ local res = {}
+ for k,v in pairs(t) do
+ res[k] = fun(v,...)
+ end
+ return setmeta(res,t)
+end
+
+--- apply a function to all values of a list.
+-- This returns a table of the results.
+-- Any extra arguments are passed to the function.
+-- @within MappingAndFiltering
+-- @func fun A function that takes at least one argument
+-- @array t a table (applies to array part)
+-- @param ... optional arguments
+-- @return a list-like table
+-- @usage imap(function(v) return v*v end, {10,20,30,fred=2}) is {100,400,900}
+function tablex.imap(fun,t,...)
+ assert_arg_indexable(1,t)
+ fun = function_arg(1,fun)
+ local res = {}
+ for i = 1,#t do
+ res[i] = fun(t[i],...) or false
+ end
+ return setmeta(res,t,'List')
+end
+
+--- apply a named method to values from a table.
+-- @within MappingAndFiltering
+-- @string name the method name
+-- @array t a list-like table
+-- @param ... any extra arguments to the method
+-- @return a `List` with the results of the method (1st result only)
+-- @usage
+-- local Car = {}
+-- Car.__index = Car
+-- function Car.new(car)
+-- return setmetatable(car or {}, Car)
+-- end
+-- Car.speed = 0
+-- function Car:faster(increase)
+-- self.speed = self.speed + increase
+-- return self.speed
+-- end
+--
+-- local ferrari = Car.new{ name = "Ferrari" }
+-- local lamborghini = Car.new{ name = "Lamborghini", speed = 50 }
+-- local cars = { ferrari, lamborghini }
+--
+-- assert(ferrari.speed == 0)
+-- assert(lamborghini.speed == 50)
+-- tablex.map_named_method("faster", cars, 10)
+-- assert(ferrari.speed == 10)
+-- assert(lamborghini.speed == 60)
+function tablex.map_named_method (name,t,...)
+ utils.assert_string(1,name)
+ assert_arg_indexable(2,t)
+ local res = {}
+ for i = 1,#t do
+ local val = t[i]
+ local fun = val[name]
+ res[i] = fun(val,...)
+ end
+ return setmeta(res,t,'List')
+end
+
+--- apply a function to all values of a table, in-place.
+-- Any extra arguments are passed to the function.
+-- @func fun A function that takes at least one argument
+-- @tab t a table
+-- @param ... extra arguments passed to `fun`
+-- @see tablex.foreach
+function tablex.transform (fun,t,...)
+ assert_arg_iterable(1,t)
+ fun = function_arg(1,fun)
+ for k,v in pairs(t) do
+ t[k] = fun(v,...)
+ end
+end
+
+--- generate a table of all numbers in a range.
+-- This is consistent with a numerical for loop.
+-- @int start number
+-- @int finish number
+-- @int[opt=1] step make this negative for start < finish
+function tablex.range (start,finish,step)
+ local res
+ step = step or 1
+ if start == finish then
+ res = {start}
+ elseif (start > finish and step > 0) or (finish > start and step < 0) then
+ res = {}
+ else
+ local k = 1
+ res = {}
+ for i=start,finish,step do res[k]=i; k=k+1 end
+ end
+ return makelist(res)
+end
+
+--- apply a function to values from two tables.
+-- @within MappingAndFiltering
+-- @func fun a function of at least two arguments
+-- @tab t1 a table
+-- @tab t2 a table
+-- @param ... extra arguments
+-- @return a table
+-- @usage map2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23,m=44}
+function tablex.map2 (fun,t1,t2,...)
+ assert_arg_iterable(1,t1)
+ assert_arg_iterable(2,t2)
+ fun = function_arg(1,fun)
+ local res = {}
+ for k,v in pairs(t1) do
+ res[k] = fun(v,t2[k],...)
+ end
+ return setmeta(res,t1,'List')
+end
+
+--- apply a function to values from two arrays.
+-- The result will be the length of the shortest array.
+-- @within MappingAndFiltering
+-- @func fun a function of at least two arguments
+-- @array t1 a list-like table
+-- @array t2 a list-like table
+-- @param ... extra arguments
+-- @usage imap2('+',{1,2,3,m=4},{10,20,30,m=40}) is {11,22,23}
+function tablex.imap2 (fun,t1,t2,...)
+ assert_arg_indexable(2,t1)
+ assert_arg_indexable(3,t2)
+ fun = function_arg(1,fun)
+ local res,n = {},math.min(#t1,#t2)
+ for i = 1,n do
+ res[i] = fun(t1[i],t2[i],...)
+ end
+ return res
+end
+
+--- 'reduce' a list using a binary function.
+-- @func fun a function of two arguments
+-- @array t a list-like table
+-- @array memo optional initial memo value. Defaults to first value in table.
+-- @return the result of the function
+-- @usage reduce('+',{1,2,3,4}) == 10
+function tablex.reduce (fun,t,memo)
+ assert_arg_indexable(2,t)
+ fun = function_arg(1,fun)
+ local n = #t
+ if n == 0 then
+ return memo
+ end
+ local res = memo and fun(memo, t[1]) or t[1]
+ for i = 2,n do
+ res = fun(res,t[i])
+ end
+ return res
+end
+
+--- apply a function to all elements of a table.
+-- The arguments to the function will be the value,
+-- the key and _finally_ any extra arguments passed to this function.
+-- Note that the Lua 5.0 function table.foreach passed the _key_ first.
+-- @within Iterating
+-- @tab t a table
+-- @func fun a function on the elements; `function(value, key, ...)`
+-- @param ... extra arguments passed to `fun`
+-- @see tablex.transform
+function tablex.foreach(t,fun,...)
+ assert_arg_iterable(1,t)
+ fun = function_arg(2,fun)
+ for k,v in pairs(t) do
+ fun(v,k,...)
+ end
+end
+
+--- apply a function to all elements of a list-like table in order.
+-- The arguments to the function will be the value,
+-- the index and _finally_ any extra arguments passed to this function
+-- @within Iterating
+-- @array t a table
+-- @func fun a function with at least one argument
+-- @param ... optional arguments
+function tablex.foreachi(t,fun,...)
+ assert_arg_indexable(1,t)
+ fun = function_arg(2,fun)
+ for i = 1,#t do
+ fun(t[i],i,...)
+ end
+end
+
+--- Apply a function to a number of tables.
+-- A more general version of map
+-- The result is a table containing the result of applying that function to the
+-- ith value of each table. Length of output list is the minimum length of all the lists
+-- @within MappingAndFiltering
+-- @func fun a function of n arguments
+-- @tab ... n tables
+-- @usage mapn(function(x,y,z) return x+y+z end, {1,2,3},{10,20,30},{100,200,300}) is {111,222,333}
+-- @usage mapn(math.max, {1,20,300},{10,2,3},{100,200,100}) is {100,200,300}
+-- @param fun A function that takes as many arguments as there are tables
+function tablex.mapn(fun,...)
+ fun = function_arg(1,fun)
+ local res = {}
+ local lists = {...}
+ local minn = 1e40
+ for i = 1,#lists do
+ minn = min(minn,#(lists[i]))
+ end
+ for i = 1,minn do
+ local args,k = {},1
+ for j = 1,#lists do
+ args[k] = lists[j][i]
+ k = k + 1
+ end
+ res[#res+1] = fun(unpack(args))
+ end
+ return res
+end
+
+--- call the function with the key and value pairs from a table.
+-- The function can return a value and a key (note the order!). If both
+-- are not nil, then this pair is inserted into the result: if the key already exists, we convert the value for that
+-- key into a table and append into it. If only value is not nil, then it is appended to the result.
+-- @within MappingAndFiltering
+-- @func fun A function which will be passed each key and value as arguments, plus any extra arguments to pairmap.
+-- @tab t A table
+-- @param ... optional arguments
+-- @usage pairmap(function(k,v) return v end,{fred=10,bonzo=20}) is {10,20} _or_ {20,10}
+-- @usage pairmap(function(k,v) return {k,v},k end,{one=1,two=2}) is {one={'one',1},two={'two',2}}
+function tablex.pairmap(fun,t,...)
+ assert_arg_iterable(1,t)
+ fun = function_arg(1,fun)
+ local res = {}
+ for k,v in pairs(t) do
+ local rv,rk = fun(k,v,...)
+ if rk then
+ if res[rk] then
+ if type(res[rk]) == 'table' then
+ table.insert(res[rk],rv)
+ else
+ res[rk] = {res[rk], rv}
+ end
+ else
+ res[rk] = rv
+ end
+ else
+ res[#res+1] = rv
+ end
+ end
+ return res
+end
+
+local function keys_op(i,v) return i end
+
+--- return all the keys of a table in arbitrary order.
+-- @within Extraction
+-- @tab t A table
+function tablex.keys(t)
+ assert_arg_iterable(1,t)
+ return makelist(tablex.pairmap(keys_op,t))
+end
+
+local function values_op(i,v) return v end
+
+--- return all the values of the table in arbitrary order
+-- @within Extraction
+-- @tab t A table
+function tablex.values(t)
+ assert_arg_iterable(1,t)
+ return makelist(tablex.pairmap(values_op,t))
+end
+
+local function index_map_op (i,v) return i,v end
+
+--- create an index map from a list-like table. The original values become keys,
+-- and the associated values are the indices into the original list.
+-- @array t a list-like table
+-- @return a map-like table
+function tablex.index_map (t)
+ assert_arg_indexable(1,t)
+ return makemap(tablex.pairmap(index_map_op,t))
+end
+
+local function set_op(i,v) return true,v end
+
+--- create a set from a list-like table. A set is a table where the original values
+-- become keys, and the associated values are all true.
+-- @array t a list-like table
+-- @return a set (a map-like table)
+function tablex.makeset (t)
+ assert_arg_indexable(1,t)
+ return setmetatable(tablex.pairmap(set_op,t),require('pl.Set'))
+end
+
+--- combine two tables, either as union or intersection. Corresponds to
+-- set operations for sets () but more general. Not particularly
+-- useful for list-like tables.
+-- @within Merging
+-- @tab t1 a table
+-- @tab t2 a table
+-- @bool dup true for a union, false for an intersection.
+-- @usage merge({alice=23,fred=34},{bob=25,fred=34}) is {fred=34}
+-- @usage merge({alice=23,fred=34},{bob=25,fred=34},true) is {bob=25,fred=34,alice=23}
+-- @see tablex.index_map
+function tablex.merge (t1,t2,dup)
+ assert_arg_iterable(1,t1)
+ assert_arg_iterable(2,t2)
+ local res = {}
+ for k,v in pairs(t1) do
+ if dup or t2[k] then res[k] = v end
+ end
+ if dup then
+ for k,v in pairs(t2) do
+ res[k] = v
+ end
+ end
+ return setmeta(res,t1,'Map')
+end
+
+--- the union of two map-like tables.
+-- If there are duplicate keys, the second table wins.
+-- @tab t1 a table
+-- @tab t2 a table
+-- @treturn tab
+-- @see tablex.merge
+function tablex.union(t1, t2)
+ return tablex.merge(t1, t2, true)
+end
+
+--- the intersection of two map-like tables.
+-- @tab t1 a table
+-- @tab t2 a table
+-- @treturn tab
+-- @see tablex.merge
+function tablex.intersection(t1, t2)
+ return tablex.merge(t1, t2, false)
+end
+
+--- a new table which is the difference of two tables.
+-- With sets (where the values are all true) this is set difference and
+-- symmetric difference depending on the third parameter.
+-- @within Merging
+-- @tab s1 a map-like table or set
+-- @tab s2 a map-like table or set
+-- @bool symm symmetric difference (default false)
+-- @return a map-like table or set
+function tablex.difference (s1,s2,symm)
+ assert_arg_iterable(1,s1)
+ assert_arg_iterable(2,s2)
+ local res = {}
+ for k,v in pairs(s1) do
+ if s2[k] == nil then res[k] = v end
+ end
+ if symm then
+ for k,v in pairs(s2) do
+ if s1[k] == nil then res[k] = v end
+ end
+ end
+ return setmeta(res,s1,'Map')
+end
+
+--- A table where the key/values are the values and value counts of the table.
+-- @array t a list-like table
+-- @func cmp a function that defines equality (otherwise uses ==)
+-- @return a map-like table
+-- @see seq.count_map
+function tablex.count_map (t,cmp)
+ assert_arg_indexable(1,t)
+ local res,mask = {},{}
+ cmp = function_arg(2,cmp or '==')
+ local n = #t
+ for i = 1,#t do
+ local v = t[i]
+ if not mask[v] then
+ mask[v] = true
+ -- check this value against all other values
+ res[v] = 1 -- there's at least one instance
+ for j = i+1,n do
+ local w = t[j]
+ local ok = cmp(v,w)
+ if ok then
+ res[v] = res[v] + 1
+ mask[w] = true
+ end
+ end
+ end
+ end
+ return makemap(res)
+end
+
+--- filter an array's values using a predicate function
+-- @within MappingAndFiltering
+-- @array t a list-like table
+-- @func pred a boolean function
+-- @param arg optional argument to be passed as second argument of the predicate
+function tablex.filter (t,pred,arg)
+ assert_arg_indexable(1,t)
+ pred = function_arg(2,pred)
+ local res,k = {},1
+ for i = 1,#t do
+ local v = t[i]
+ if pred(v,arg) then
+ res[k] = v
+ k = k + 1
+ end
+ end
+ return setmeta(res,t,'List')
+end
+
+--- return a table where each element is a table of the ith values of an arbitrary
+-- number of tables. It is equivalent to a matrix transpose.
+-- @within Merging
+-- @usage zip({10,20,30},{100,200,300}) is {{10,100},{20,200},{30,300}}
+-- @array ... arrays to be zipped
+function tablex.zip(...)
+ return tablex.mapn(function(...) return {...} end,...)
+end
+
+local _copy
+function _copy (dest,src,idest,isrc,nsrc,clean_tail)
+ idest = idest or 1
+ isrc = isrc or 1
+ local iend
+ if not nsrc then
+ nsrc = #src
+ iend = #src
+ else
+ iend = isrc + min(nsrc-1,#src-isrc)
+ end
+ if dest == src then -- special case
+ if idest > isrc and iend >= idest then -- overlapping ranges
+ src = tablex.sub(src,isrc,nsrc)
+ isrc = 1; iend = #src
+ end
+ end
+ for i = isrc,iend do
+ dest[idest] = src[i]
+ idest = idest + 1
+ end
+ if clean_tail then
+ tablex.clear(dest,idest)
+ end
+ return dest
+end
+
+--- copy an array into another one, clearing `dest` after `idest+nsrc`, if necessary.
+-- @within Copying
+-- @array dest a list-like table
+-- @array src a list-like table
+-- @int[opt=1] idest where to start copying values into destination
+-- @int[opt=1] isrc where to start copying values from source
+-- @int[opt=#src] nsrc number of elements to copy from source
+function tablex.icopy (dest,src,idest,isrc,nsrc)
+ assert_arg_indexable(1,dest)
+ assert_arg_indexable(2,src)
+ return _copy(dest,src,idest,isrc,nsrc,true)
+end
+
+--- copy an array into another one.
+-- @within Copying
+-- @array dest a list-like table
+-- @array src a list-like table
+-- @int[opt=1] idest where to start copying values into destination
+-- @int[opt=1] isrc where to start copying values from source
+-- @int[opt=#src] nsrc number of elements to copy from source
+function tablex.move (dest,src,idest,isrc,nsrc)
+ assert_arg_indexable(1,dest)
+ assert_arg_indexable(2,src)
+ return _copy(dest,src,idest,isrc,nsrc,false)
+end
+
+function tablex._normalize_slice(self,first,last)
+ local sz = #self
+ if not first then first=1 end
+ if first<0 then first=sz+first+1 end
+ -- make the range _inclusive_!
+ if not last then last=sz end
+ if last < 0 then last=sz+1+last end
+ return first,last
+end
+
+--- Extract a range from a table, like 'string.sub'.
+-- If first or last are negative then they are relative to the end of the list
+-- eg. sub(t,-2) gives last 2 entries in a list, and
+-- sub(t,-4,-2) gives from -4th to -2nd
+-- @within Extraction
+-- @array t a list-like table
+-- @int first An index
+-- @int last An index
+-- @return a new List
+function tablex.sub(t,first,last)
+ assert_arg_indexable(1,t)
+ first,last = tablex._normalize_slice(t,first,last)
+ local res={}
+ for i=first,last do append(res,t[i]) end
+ return setmeta(res,t,'List')
+end
+
+--- set an array range to a value. If it's a function we use the result
+-- of applying it to the indices.
+-- @array t a list-like table
+-- @param val a value
+-- @int[opt=1] i1 start range
+-- @int[opt=#t] i2 end range
+function tablex.set (t,val,i1,i2)
+ assert_arg_indexable(1,t)
+ i1,i2 = i1 or 1,i2 or #t
+ if types.is_callable(val) then
+ for i = i1,i2 do
+ t[i] = val(i)
+ end
+ else
+ for i = i1,i2 do
+ t[i] = val
+ end
+ end
+end
+
+--- create a new array of specified size with initial value.
+-- @int n size
+-- @param val initial value (can be `nil`, but don't expect `#` to work!)
+-- @return the table
+function tablex.new (n,val)
+ local res = {}
+ tablex.set(res,val,1,n)
+ return res
+end
+
+--- clear out the contents of a table.
+-- @array t a list
+-- @param istart optional start position
+function tablex.clear(t,istart)
+ istart = istart or 1
+ for i = istart,#t do remove(t) end
+end
+
+--- insert values into a table.
+-- similar to `table.insert` but inserts values from given table `values`,
+-- not the object itself, into table `t` at position `pos`.
+-- @within Copying
+-- @array t the list
+-- @int[opt] position (default is at end)
+-- @array values
+function tablex.insertvalues(t, ...)
+ assert_arg(1,t,'table')
+ local pos, values
+ if select('#', ...) == 1 then
+ pos,values = #t+1, ...
+ else
+ pos,values = ...
+ end
+ if #values > 0 then
+ for i=#t,pos,-1 do
+ t[i+#values] = t[i]
+ end
+ local offset = 1 - pos
+ for i=pos,pos+#values-1 do
+ t[i] = values[i + offset]
+ end
+ end
+ return t
+end
+
+--- remove a range of values from a table.
+-- End of range may be negative.
+-- @array t a list-like table
+-- @int i1 start index
+-- @int i2 end index
+-- @return the table
+function tablex.removevalues (t,i1,i2)
+ assert_arg(1,t,'table')
+ i1,i2 = tablex._normalize_slice(t,i1,i2)
+ for i = i1,i2 do
+ remove(t,i1)
+ end
+ return t
+end
+
+local _find
+_find = function (t,value,tables)
+ for k,v in pairs(t) do
+ if v == value then return k end
+ end
+ for k,v in pairs(t) do
+ if not tables[v] and type(v) == 'table' then
+ tables[v] = true
+ local res = _find(v,value,tables)
+ if res then
+ res = tostring(res)
+ if type(k) ~= 'string' then
+ return '['..k..']'..res
+ else
+ return k..'.'..res
+ end
+ end
+ end
+ end
+end
+
+--- find a value in a table by recursive search.
+-- @within Finding
+-- @tab t the table
+-- @param value the value
+-- @array[opt] exclude any tables to avoid searching
+-- @return a fieldspec, e.g. 'a.b' or 'math.sin'
+-- @usage search(_G,math.sin,{package.path}) == 'math.sin'
+function tablex.search (t,value,exclude)
+ assert_arg_iterable(1,t)
+ local tables = {[t]=true}
+ if exclude then
+ for _,v in pairs(exclude) do tables[v] = true end
+ end
+ return _find(t,value,tables)
+end
+
+--- return an iterator to a table sorted by its keys
+-- @within Iterating
+-- @tab t the table
+-- @func f an optional comparison function (f(x,y) is true if x < y)
+-- @usage for k,v in tablex.sort(t) do print(k,v) end
+-- @return an iterator to traverse elements sorted by the keys
+function tablex.sort(t,f)
+ local keys = {}
+ for k in pairs(t) do keys[#keys + 1] = k end
+ tsort(keys,f)
+ local i = 0
+ return function()
+ i = i + 1
+ return keys[i], t[keys[i]]
+ end
+end
+
+--- return an iterator to a table sorted by its values
+-- @within Iterating
+-- @tab t the table
+-- @func f an optional comparison function (f(x,y) is true if x < y)
+-- @usage for k,v in tablex.sortv(t) do print(k,v) end
+-- @return an iterator to traverse elements sorted by the values
+function tablex.sortv(t,f)
+ f = function_arg(2, f or '<')
+ local keys = {}
+ for k in pairs(t) do keys[#keys + 1] = k end
+ tsort(keys,function(x, y) return f(t[x], t[y]) end)
+ local i = 0
+ return function()
+ i = i + 1
+ return keys[i], t[keys[i]]
+ end
+end
+
+--- modifies a table to be read only.
+-- This only offers weak protection. Tables can still be modified with
+-- `table.insert` and `rawset`.
+--
+-- *NOTE*: for Lua 5.1 length, pairs and ipairs will not work, since the
+-- equivalent metamethods are only available in Lua 5.2 and newer.
+-- @tab t the table
+-- @return the table read only (a proxy).
+function tablex.readonly(t)
+ local mt = {
+ __index=t,
+ __newindex=function(t, k, v) error("Attempt to modify read-only table", 2) end,
+ __pairs=function() return pairs(t) end,
+ __ipairs=function() return ipairs(t) end,
+ __len=function() return #t end,
+ __metatable=false
+ }
+ return setmetatable({}, mt)
+end
+
+return tablex
diff --git a/macros/luatex/generic/penlight/pl/template.lua b/macros/luatex/generic/penlight/pl/template.lua
new file mode 100644
index 0000000000..9961a1bdfe
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/template.lua
@@ -0,0 +1,202 @@
+--- A template preprocessor.
+-- Originally by [Ricki Lake](http://lua-users.org/wiki/SlightlyLessSimpleLuaPreprocessor)
+--
+-- There are two rules:
+--
+-- * lines starting with # are Lua
+-- * otherwise, `$(expr)` is the result of evaluating `expr`
+--
+-- Example:
+--
+-- # for i = 1,3 do
+-- $(i) Hello, Word!
+-- # end
+-- ===>
+-- 1 Hello, Word!
+-- 2 Hello, Word!
+-- 3 Hello, Word!
+--
+-- Other escape characters can be used, when the defaults conflict
+-- with the output language.
+--
+-- > for _,n in pairs{'one','two','three'} do
+-- static int l_${n} (luaState *state);
+-- > end
+--
+-- See @{03-strings.md.Another_Style_of_Template|the Guide}.
+--
+-- Dependencies: `pl.utils`
+-- @module pl.template
+
+local utils = require 'pl.utils'
+
+local append,format,strsub,strfind,strgsub = table.insert,string.format,string.sub,string.find,string.gsub
+
+local APPENDER = "\n__R_size = __R_size + 1; __R_table[__R_size] = "
+
+local function parseDollarParen(pieces, chunk, exec_pat, newline)
+ local s = 1
+ for term, executed, e in chunk:gmatch(exec_pat) do
+ executed = '('..strsub(executed,2,-2)..')'
+ append(pieces, APPENDER..format("%q", strsub(chunk,s, term - 1)))
+ append(pieces, APPENDER..format("__tostring(%s or '')", executed))
+ s = e
+ end
+ local r
+ if newline then
+ r = format("%q", strgsub(strsub(chunk,s),"\n",""))
+ else
+ r = format("%q", strsub(chunk,s))
+ end
+ if r ~= '""' then
+ append(pieces, APPENDER..r)
+ end
+end
+
+local function parseHashLines(chunk,inline_escape,brackets,esc,newline)
+ local exec_pat = "()"..inline_escape.."(%b"..brackets..")()"
+
+ local esc_pat = esc.."+([^\n]*\n?)"
+ local esc_pat1, esc_pat2 = "^"..esc_pat, "\n"..esc_pat
+ local pieces, s = {"return function()\nlocal __R_size, __R_table, __tostring = 0, {}, __tostring", n = 1}, 1
+ while true do
+ local _, e, lua = strfind(chunk,esc_pat1, s)
+ if not e then
+ local ss
+ ss, e, lua = strfind(chunk,esc_pat2, s)
+ parseDollarParen(pieces, strsub(chunk,s, ss), exec_pat, newline)
+ if not e then break end
+ end
+ if strsub(lua, -1, -1) == "\n" then lua = strsub(lua, 1, -2) end
+ append(pieces, "\n"..lua)
+ s = e + 1
+ end
+ append(pieces, "\nreturn __R_table\nend")
+
+ -- let's check for a special case where there is nothing to template, but it's
+ -- just a single static string
+ local short = false
+ if (#pieces == 3) and (pieces[2]:find(APPENDER, 1, true) == 1) then
+ pieces = { "return " .. pieces[2]:sub(#APPENDER+1,-1) }
+ short = true
+ end
+ -- if short == true, the generated function will not return a table of strings,
+ -- but a single string
+ return table.concat(pieces), short
+end
+
+local template = {}
+
+--- expand the template using the specified environment.
+-- This function will compile and render the template. For more performant
+-- recurring usage use the two step approach by using `compile` and `ct:render`.
+-- There are six special fields in the environment table `env`
+--
+-- * `_parent`: continue looking up in this table (e.g. `_parent=_G`).
+-- * `_brackets`: bracket pair that wraps inline Lua expressions, default is '()'.
+-- * `_escape`: character marking Lua lines, default is '#'
+-- * `_inline_escape`: character marking inline Lua expression, default is '$'.
+-- * `_chunk_name`: chunk name for loaded templates, used if there
+-- is an error in Lua code. Default is 'TMP'.
+-- * `_debug`: if truthy, the generated code will be printed upon a render error
+--
+-- @string str the template string
+-- @tab[opt] env the environment
+-- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last
+-- return value (`source_code`) is only returned if the debug option is used.
+function template.substitute(str,env)
+ env = env or {}
+ local t, err = template.compile(str, {
+ chunk_name = rawget(env,"_chunk_name"),
+ escape = rawget(env,"_escape"),
+ inline_escape = rawget(env,"_inline_escape"),
+ inline_brackets = rawget(env,"_brackets"),
+ newline = nil,
+ debug = rawget(env,"_debug")
+ })
+ if not t then return t, err end
+
+ return t:render(env, rawget(env,"_parent"), rawget(env,"_debug"))
+end
+
+--- executes the previously compiled template and renders it.
+-- @function ct:render
+-- @tab[opt] env the environment.
+-- @tab[opt] parent continue looking up in this table (e.g. `parent=_G`).
+-- @bool[opt] db if thruthy, it will print the code upon a render error
+-- (provided the template was compiled with the debug option).
+-- @return `rendered template + nil + source_code`, or `nil + error + source_code`. The last return value
+-- (`source_code`) is only returned if the template was compiled with the debug option.
+-- @usage
+-- local ct, err = template.compile(my_template)
+-- local rendered , err = ct:render(my_env, parent)
+local render = function(self, env, parent, db)
+ env = env or {}
+ if parent then -- parent is a bit silly, but for backward compatibility retained
+ setmetatable(env, {__index = parent})
+ end
+ setmetatable(self.env, {__index = env})
+
+ local res, out = xpcall(self.fn, debug.traceback)
+ if not res then
+ if self.code and db then print(self.code) end
+ return nil, out, self.code
+ end
+ return table.concat(out), nil, self.code
+end
+
+--- compiles the template.
+-- Returns an object that can repeatedly be rendered without parsing/compiling
+-- the template again.
+-- The options passed in the `opts` table support the following options:
+--
+-- * `chunk_name`: chunk name for loaded templates, used if there
+-- is an error in Lua code. Default is 'TMP'.
+-- * `escape`: character marking Lua lines, default is '#'
+-- * `inline_escape`: character marking inline Lua expression, default is '$'.
+-- * `inline_brackets`: bracket pair that wraps inline Lua expressions, default is '()'.
+-- * `newline`: string to replace newline characters, default is `nil` (not replacing newlines).
+-- * `debug`: if truthy, the generated source code will be retained within the compiled template object, default is `nil`.
+--
+-- @string str the template string
+-- @tab[opt] opts the compilation options to use
+-- @return template object, or `nil + error + source_code`
+-- @usage
+-- local ct, err = template.compile(my_template)
+-- local rendered , err = ct:render(my_env, parent)
+function template.compile(str, opts)
+ opts = opts or {}
+ local chunk_name = opts.chunk_name or 'TMP'
+ local escape = opts.escape or '#'
+ local inline_escape = opts.inline_escape or '$'
+ local inline_brackets = opts.inline_brackets or '()'
+
+ local code, short = parseHashLines(str,inline_escape,inline_brackets,escape,opts.newline)
+ local env = { __tostring = tostring }
+ local fn, err = utils.load(code, chunk_name,'t',env)
+ if not fn then return nil, err, code end
+
+ if short then
+ -- the template returns a single constant string, let's optimize for that
+ local constant_string = fn()
+ return {
+ fn = fn(),
+ env = env,
+ render = function(self) -- additional params can be ignored
+ -- skip the metatable magic and error handling in the render
+ -- function above for this special case
+ return constant_string, nil, self.code
+ end,
+ code = opts.debug and code or nil,
+ }
+ end
+
+ return {
+ fn = fn(),
+ env = env,
+ render = render,
+ code = opts.debug and code or nil,
+ }
+end
+
+return template
diff --git a/macros/luatex/generic/penlight/pl/test.lua b/macros/luatex/generic/penlight/pl/test.lua
new file mode 100644
index 0000000000..694bbc531a
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/test.lua
@@ -0,0 +1,164 @@
+--- Useful test utilities.
+--
+-- test.asserteq({1,2},{1,2}) -- can compare tables
+-- test.asserteq(1.2,1.19,0.02) -- compare FP numbers within precision
+-- T = test.tuple -- used for comparing multiple results
+-- test.asserteq(T(string.find(" me","me")),T(2,3))
+--
+-- Dependencies: `pl.utils`, `pl.tablex`, `pl.pretty`, `pl.path`, `debug`
+-- @module pl.test
+
+local tablex = require 'pl.tablex'
+local utils = require 'pl.utils'
+local pretty = require 'pl.pretty'
+local path = require 'pl.path'
+local type,unpack,pack = type,utils.unpack,utils.pack
+local clock = os.clock
+local debug = require 'debug'
+local io = io
+
+local function dump(x)
+ if type(x) == 'table' and not (getmetatable(x) and getmetatable(x).__tostring) then
+ return pretty.write(x,' ',true)
+ elseif type(x) == 'string' then
+ return '"'..x..'"'
+ else
+ return tostring(x)
+ end
+end
+
+local test = {}
+
+---- error handling for test results.
+-- By default, this writes to stderr and exits the program.
+-- Re-define this function to raise an error and/or redirect output
+function test.error_handler(file,line,got_text, needed_text,msg)
+ local err = io.stderr
+ err:write(path.basename(file)..':'..line..': assertion failed\n')
+ err:write("got:\t",got_text,'\n')
+ err:write("needed:\t",needed_text,'\n')
+ utils.quit(1,msg or "these values were not equal")
+end
+
+local function complain (x,y,msg,where)
+ local i = debug.getinfo(3 + (where or 0))
+ test.error_handler(i.short_src,i.currentline,dump(x),dump(y),msg)
+end
+
+--- general test complain message.
+-- Useful for composing new test functions (see tests/tablex.lua for an example)
+-- @param x a value
+-- @param y value to compare first value against
+-- @param msg message
+-- @param where extra level offset for errors
+-- @function complain
+test.complain = complain
+
+--- like assert, except takes two arguments that must be equal and can be tables.
+-- If they are plain tables, it will use tablex.deepcompare.
+-- @param x any value
+-- @param y a value equal to x
+-- @param eps an optional tolerance for numerical comparisons
+-- @param where extra level offset
+function test.asserteq (x,y,eps,where)
+ local res = x == y
+ if not res then
+ res = tablex.deepcompare(x,y,true,eps)
+ end
+ if not res then
+ complain(x,y,nil,where)
+ end
+end
+
+--- assert that the first string matches the second.
+-- @param s1 a string
+-- @param s2 a string
+-- @param where extra level offset
+function test.assertmatch (s1,s2,where)
+ if not s1:match(s2) then
+ complain (s1,s2,"these strings did not match",where)
+ end
+end
+
+--- assert that the function raises a particular error.
+-- @param fn a function or a table of the form {function,arg1,...}
+-- @param e a string to match the error against
+-- @param where extra level offset
+function test.assertraise(fn,e,where)
+ local ok, err
+ if type(fn) == 'table' then
+ ok, err = pcall(unpack(fn))
+ else
+ ok, err = pcall(fn)
+ end
+ if ok or err:match(e)==nil then
+ complain (err,e,"these errors did not match",where)
+ end
+end
+
+--- a version of asserteq that takes two pairs of values.
+-- <code>x1==y1 and x2==y2</code> must be true. Useful for functions that naturally
+-- return two values.
+-- @param x1 any value
+-- @param x2 any value
+-- @param y1 any value
+-- @param y2 any value
+-- @param where extra level offset
+function test.asserteq2 (x1,x2,y1,y2,where)
+ if x1 ~= y1 then complain(x1,y1,nil,where) end
+ if x2 ~= y2 then complain(x2,y2,nil,where) end
+end
+
+-- tuple type --
+
+local tuple_mt = {
+ unpack = unpack
+}
+tuple_mt.__index = tuple_mt
+
+function tuple_mt.__tostring(self)
+ local ts = {}
+ for i=1, self.n do
+ local s = self[i]
+ ts[i] = type(s) == 'string' and ('%q'):format(s) or tostring(s)
+ end
+ return 'tuple(' .. table.concat(ts, ', ') .. ')'
+end
+
+function tuple_mt.__eq(a, b)
+ if a.n ~= b.n then return false end
+ for i=1, a.n do
+ if a[i] ~= b[i] then return false end
+ end
+ return true
+end
+
+function tuple_mt.__len(self)
+ return self.n
+end
+
+--- encode an arbitrary argument list as a tuple.
+-- This can be used to compare to other argument lists, which is
+-- very useful for testing functions which return a number of values.
+-- Unlike regular array-like tables ('sequences') they may contain nils.
+-- Tuples understand equality and know how to print themselves out.
+-- The # operator is defined to be the size, irrespecive of any nils,
+-- and there is an `unpack` method.
+-- @usage asserteq(tuple( ('ab'):find 'a'), tuple(1,1))
+function test.tuple(...)
+ return setmetatable(pack(...), tuple_mt)
+end
+
+--- Time a function. Call the function a given number of times, and report the number of seconds taken,
+-- together with a message. Any extra arguments will be passed to the function.
+-- @string msg a descriptive message
+-- @int n number of times to call the function
+-- @func fun the function
+-- @param ... optional arguments to fun
+function test.timer(msg,n,fun,...)
+ local start = clock()
+ for i = 1,n do fun(...) end
+ utils.printf("%s: took %7.2f sec\n",msg,clock()-start)
+end
+
+return test
diff --git a/macros/luatex/generic/penlight/pl/text.lua b/macros/luatex/generic/penlight/pl/text.lua
new file mode 100644
index 0000000000..e626e86cdf
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/text.lua
@@ -0,0 +1,246 @@
+--- Text processing utilities.
+--
+-- This provides a Template class (modeled after the same from the Python
+-- 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.
+--
+-- > require 'pl.text'.format_operator()
+-- > = '%s = %5.3f' % {'PI',math.pi}
+-- PI = 3.142
+-- > = '$name = $value' % {name='dog',value='Pluto'}
+-- dog = Pluto
+--
+-- Dependencies: `pl.utils`, `pl.types`
+-- @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
+
+ -- 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
+
+return text
diff --git a/macros/luatex/generic/penlight/pl/types.lua b/macros/luatex/generic/penlight/pl/types.lua
new file mode 100644
index 0000000000..35b0ccb54a
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/types.lua
@@ -0,0 +1,183 @@
+---- Dealing with Detailed Type Information
+
+-- Dependencies `pl.utils`
+-- @module pl.types
+
+local utils = require 'pl.utils'
+local math_ceil = math.ceil
+local assert_arg = utils.assert_arg
+local types = {}
+
+--- is the object either a function or a callable object?.
+-- @param obj Object to check.
+function types.is_callable (obj)
+ return type(obj) == 'function' or getmetatable(obj) and getmetatable(obj).__call and true
+end
+
+--- is the object of the specified type?.
+-- If the type is a string, then use type, otherwise compare with metatable.
+--
+-- NOTE: this function is imported from `utils.is_type`.
+-- @param obj An object to check
+-- @param tp The expected type
+-- @function is_type
+-- @see utils.is_type
+types.is_type = utils.is_type
+
+local fileMT = getmetatable(io.stdout)
+
+--- a string representation of a type.
+-- For tables and userdata with metatables, we assume that the metatable has a `_name`
+-- field. If the field is not present it will return 'unknown table' or
+-- 'unknown userdata'.
+-- Lua file objects return the type 'file'.
+-- @param obj an object
+-- @return a string like 'number', 'table', 'file' or 'List'
+function types.type (obj)
+ local t = type(obj)
+ if t == 'table' or t == 'userdata' then
+ local mt = getmetatable(obj)
+ if mt == fileMT then
+ return 'file'
+ elseif mt == nil then
+ return t
+ else
+ -- TODO: the "unknown" is weird, it should just return the type
+ return mt._name or "unknown "..t
+ end
+ else
+ return t
+ end
+end
+
+--- is this number an integer?
+-- @param x a number
+-- @raise error if x is not a number
+-- @return boolean
+function types.is_integer (x)
+ return math_ceil(x)==x
+end
+
+--- Check if the object is "empty".
+-- An object is considered empty if it is:
+--
+-- - `nil`
+-- - a table without any items (key-value pairs or indexes)
+-- - a string with no content ("")
+-- - not a nil/table/string
+-- @param o The object to check if it is empty.
+-- @param ignore_spaces If the object is a string and this is true the string is
+-- considered empty if it only contains spaces.
+-- @return `true` if the object is empty, otherwise a falsy value.
+function types.is_empty(o, ignore_spaces)
+ if o == nil then
+ return true
+ elseif type(o) == "table" then
+ return next(o) == nil
+ elseif type(o) == "string" then
+ return o == "" or (not not ignore_spaces and (not not o:find("^%s+$")))
+ else
+ return true
+ end
+end
+
+local function check_meta (val)
+ if type(val) == 'table' then return true end
+ return getmetatable(val)
+end
+
+--- is an object 'array-like'?
+-- An object is array like if:
+--
+-- - it is a table, or
+-- - it has a metatable with `__len` and `__index` methods
+--
+-- NOTE: since `__len` is 5.2+, on 5.1 is usually returns `false` for userdata
+-- @param val any value.
+-- @return `true` if the object is array-like, otherwise a falsy value.
+function types.is_indexable (val)
+ local mt = check_meta(val)
+ if mt == true then return true end
+ return mt and mt.__len and mt.__index and true
+end
+
+--- can an object be iterated over with `pairs`?
+-- An object is iterable if:
+--
+-- - it is a table, or
+-- - it has a metatable with a `__pairs` meta method
+--
+-- NOTE: since `__pairs` is 5.2+, on 5.1 is usually returns `false` for userdata
+-- @param val any value.
+-- @return `true` if the object is iterable, otherwise a falsy value.
+function types.is_iterable (val)
+ local mt = check_meta(val)
+ if mt == true then return true end
+ return mt and mt.__pairs and true
+end
+
+--- can an object accept new key/pair values?
+-- An object is iterable if:
+--
+-- - it is a table, or
+-- - it has a metatable with a `__newindex` meta method
+--
+-- @param val any value.
+-- @return `true` if the object is writeable, otherwise a falsy value.
+function types.is_writeable (val)
+ local mt = check_meta(val)
+ if mt == true then return true end
+ return mt and mt.__newindex and true
+end
+
+-- Strings that should evaluate to true. -- TODO: add on/off ???
+local trues = { yes=true, y=true, ["true"]=true, t=true, ["1"]=true }
+-- Conditions types should evaluate to true.
+local true_types = {
+ boolean=function(o, true_strs, check_objs) return o end,
+ string=function(o, true_strs, check_objs)
+ o = o:lower()
+ if trues[o] then
+ return true
+ end
+ -- Check alternative user provided strings.
+ for _,v in ipairs(true_strs or {}) do
+ if type(v) == "string" and o == v:lower() then
+ return true
+ end
+ end
+ return false
+ end,
+ number=function(o, true_strs, check_objs) return o ~= 0 end,
+ table=function(o, true_strs, check_objs) if check_objs and next(o) ~= nil then return true end return false end
+}
+--- Convert to a boolean value.
+-- True values are:
+--
+-- * boolean: true.
+-- * string: 'yes', 'y', 'true', 't', '1' or additional strings specified by `true_strs`.
+-- * number: Any non-zero value.
+-- * table: Is not empty and `check_objs` is true.
+-- * everything else: Is not `nil` and `check_objs` is true.
+--
+-- @param o The object to evaluate.
+-- @param[opt] true_strs optional Additional strings that when matched should evaluate to true. Comparison is case insensitive.
+-- This should be a List of strings. E.g. "ja" to support German.
+-- @param[opt] check_objs True if objects should be evaluated.
+-- @return true if the input evaluates to true, otherwise false.
+function types.to_bool(o, true_strs, check_objs)
+ local true_func
+ if true_strs then
+ assert_arg(2, true_strs, "table")
+ end
+ true_func = true_types[type(o)]
+ if true_func then
+ return true_func(o, true_strs, check_objs)
+ elseif check_objs and o ~= nil then
+ return true
+ end
+ return false
+end
+
+
+return types
diff --git a/macros/luatex/generic/penlight/pl/url.lua b/macros/luatex/generic/penlight/pl/url.lua
new file mode 100644
index 0000000000..8c7cfeb7aa
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/url.lua
@@ -0,0 +1,51 @@
+--- Python-style URL quoting library.
+--
+-- @module pl.url
+
+local url = {}
+
+local function quote_char(c)
+ return string.format("%%%02X", string.byte(c))
+end
+
+--- Quote the url, replacing special characters using the '%xx' escape.
+-- @string s the string
+-- @bool quote_plus Also escape slashes and replace spaces by plus signs.
+-- @return The quoted string, or if `s` wasn't a string, just plain unaltered `s`.
+function url.quote(s, quote_plus)
+ if type(s) ~= "string" then
+ return s
+ end
+
+ s = s:gsub("\n", "\r\n")
+ s = s:gsub("([^A-Za-z0-9 %-_%./])", quote_char)
+ if quote_plus then
+ s = s:gsub(" ", "+")
+ s = s:gsub("/", quote_char)
+ else
+ s = s:gsub(" ", "%%20")
+ end
+
+ return s
+end
+
+local function unquote_char(h)
+ return string.char(tonumber(h, 16))
+end
+
+--- Unquote the url, replacing '%xx' escapes and plus signs.
+-- @string s the string
+-- @return The unquoted string, or if `s` wasn't a string, just plain unaltered `s`.
+function url.unquote(s)
+ if type(s) ~= "string" then
+ return s
+ end
+
+ s = s:gsub("+", " ")
+ s = s:gsub("%%(%x%x)", unquote_char)
+ s = s:gsub("\r\n", "\n")
+
+ return s
+end
+
+return url
diff --git a/macros/luatex/generic/penlight/pl/utils.lua b/macros/luatex/generic/penlight/pl/utils.lua
new file mode 100644
index 0000000000..66edda6db6
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/utils.lua
@@ -0,0 +1,777 @@
+--- Generally useful routines.
+-- See @{01-introduction.md.Generally_useful_functions|the Guide}.
+--
+-- Dependencies: `pl.compat`, all exported fields and functions from
+-- `pl.compat` are also available in this module.
+--
+-- @module pl.utils
+local format = string.format
+local compat = require 'pl.compat'
+local stdout = io.stdout
+local append = table.insert
+local _unpack = table.unpack -- always injected by 'compat'
+
+local is_windows = compat.is_windows
+local err_mode = 'default'
+local raise
+local operators
+local _function_factories = {}
+
+
+local utils = { _VERSION = "1.11.0" }
+for k, v in pairs(compat) do utils[k] = v end
+
+--- Some standard patterns
+-- @table patterns
+utils.patterns = {
+ FLOAT = '[%+%-%d]%d*%.?%d*[eE]?[%+%-]?%d*', -- floating point number
+ INTEGER = '[+%-%d]%d*', -- integer number
+ IDEN = '[%a_][%w_]*', -- identifier
+ FILE = '[%a%.\\][:%][%w%._%-\\]*', -- file
+}
+
+
+--- Standard meta-tables as used by other Penlight modules
+-- @table stdmt
+-- @field List the List metatable
+-- @field Map the Map metatable
+-- @field Set the Set metatable
+-- @field MultiMap the MultiMap metatable
+utils.stdmt = {
+ List = {_name='List'},
+ Map = {_name='Map'},
+ Set = {_name='Set'},
+ MultiMap = {_name='MultiMap'},
+}
+
+
+--- pack an argument list into a table.
+-- @param ... any arguments
+-- @return a table with field `n` set to the length
+-- @function utils.pack
+-- @see compat.pack
+-- @see utils.npairs
+-- @see utils.unpack
+utils.pack = table.pack -- added here to be symmetrical with unpack
+
+--- unpack a table and return its contents.
+--
+-- NOTE: this implementation differs from the Lua implementation in the way
+-- 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`
+-- @return multiple return values from the table
+-- @function utils.unpack
+-- @see compat.unpack
+-- @see utils.pack
+-- @see utils.npairs
+-- @usage
+-- local t = table.pack(nil, nil, nil, 4)
+-- local a, b, c, d = table.unpack(t) -- this `unpack` is NOT nil-safe, so d == nil
+--
+-- local a, b, c, d = utils.unpack(t) -- this is nil-safe, so d == 4
+function utils.unpack(t, i, j)
+ return _unpack(t, i or 1, j or t.n or #t)
+end
+
+--- print an arbitrary number of arguments using a format.
+-- Output will be sent to `stdout`.
+-- @param fmt The format (see `string.format`)
+-- @param ... Extra arguments for format
+function utils.printf(fmt, ...)
+ utils.assert_string(1, fmt)
+ utils.fprintf(stdout, fmt, ...)
+end
+
+--- write an arbitrary number of arguments to a file using a format.
+-- @param f File handle to write to.
+-- @param fmt The format (see `string.format`).
+-- @param ... Extra arguments for format
+function utils.fprintf(f,fmt,...)
+ utils.assert_string(2,fmt)
+ f:write(format(fmt,...))
+end
+
+do
+ local function import_symbol(T,k,v,libname)
+ local key = rawget(T,k)
+ -- warn about collisions!
+ if key and k ~= '_M' and k ~= '_NAME' and k ~= '_PACKAGE' and k ~= '_VERSION' then
+ utils.fprintf(io.stderr,"warning: '%s.%s' will not override existing symbol\n",libname,k)
+ return
+ end
+ rawset(T,k,v)
+ end
+
+ local function lookup_lib(T,t)
+ for k,v in pairs(T) do
+ if v == t then return k end
+ end
+ return '?'
+ end
+
+ local already_imported = {}
+
+ --- take a table and 'inject' it into the local namespace.
+ -- @param t The table (table), or module name (string), defaults to this `utils` module table
+ -- @param T An optional destination table (defaults to callers environment)
+ function utils.import(t,T)
+ T = T or _G
+ t = t or utils
+ if type(t) == 'string' then
+ t = require (t)
+ end
+ local libname = lookup_lib(T,t)
+ if already_imported[t] then return end
+ already_imported[t] = libname
+ for k,v in pairs(t) do
+ import_symbol(T,k,v,libname)
+ end
+ end
+end
+
+--- return either of two values, depending on a condition.
+-- @param cond A condition
+-- @param value1 Value returned if cond is truthy
+-- @param value2 Value returned if cond is falsy
+function utils.choose(cond, value1, value2)
+ return cond and value1 or value2
+end
+
+--- convert an array of values to strings.
+-- @param t a list-like table
+-- @param[opt] temp (table) buffer to use, otherwise allocate
+-- @param[opt] tostr custom tostring function, called with (value,index). Defaults to `tostring`.
+-- @return the converted buffer
+function utils.array_tostring (t,temp,tostr)
+ temp, tostr = temp or {}, tostr or tostring
+ for i = 1,#t do
+ temp[i] = tostr(t[i],i)
+ end
+ return temp
+end
+
+
+
+--- is the object of the specified type?
+-- If the type is a string, then use type, otherwise compare with metatable
+-- @param obj An object to check
+-- @param tp String of what type it should be
+-- @return boolean
+-- @usage utils.is_type("hello world", "string") --> true
+-- -- or check metatable
+-- local my_mt = {}
+-- local my_obj = setmetatable(my_obj, my_mt)
+-- utils.is_type(my_obj, my_mt) --> true
+function utils.is_type (obj,tp)
+ if type(tp) == 'string' then return type(obj) == tp end
+ local mt = getmetatable(obj)
+ return tp == mt
+end
+
+
+
+--- an iterator with indices, similar to `ipairs`, but with a range.
+-- This is a nil-safe index based iterator that will return `nil` when there
+-- is a hole in a list. To be safe ensure that table `t.n` contains the length.
+-- @tparam table t the table to iterate over
+-- @tparam[opt=1] integer i_start start index
+-- @tparam[opt=t.n or #t] integer i_end end index
+-- @tparam[opt=1] integer step step size
+-- @treturn integer index
+-- @treturn any value at index (which can be `nil`!)
+-- @see utils.pack
+-- @see utils.unpack
+-- @usage
+-- local t = utils.pack(nil, 123, nil) -- adds an `n` field when packing
+--
+-- for i, v in utils.npairs(t, 2) do -- start at index 2
+-- t[i] = tostring(t[i])
+-- end
+--
+-- -- t = { n = 3, [2] = "123", [3] = "nil" }
+function utils.npairs(t, i_start, i_end, step)
+ step = step or 1
+ if step == 0 then
+ error("iterator step-size cannot be 0", 2)
+ end
+ local i = (i_start or 1) - step
+ i_end = i_end or t.n or #t
+ if step < 0 then
+ return function()
+ i = i + step
+ if i < i_end then
+ return nil
+ end
+ return i, t[i]
+ end
+
+ else
+ return function()
+ i = i + step
+ if i > i_end then
+ return nil
+ end
+ return i, t[i]
+ end
+ end
+end
+
+
+
+--- Error handling
+-- @section Error-handling
+
+--- assert that the given argument is in fact of the correct type.
+-- @param n argument index
+-- @param val the value
+-- @param tp the type
+-- @param verify an optional verification function
+-- @param msg an optional custom message
+-- @param lev optional stack position for trace, default 2
+-- @return the validated value
+-- @raise if `val` is not the correct type
+-- @usage
+-- local param1 = assert_arg(1,"hello",'table') --> error: argument 1 expected a 'table', got a 'string'
+-- local param4 = assert_arg(4,'!@#$%^&*','string',path.isdir,'not a directory')
+-- --> error: argument 4: '!@#$%^&*' not a directory
+function utils.assert_arg (n,val,tp,verify,msg,lev)
+ if type(val) ~= tp then
+ error(("argument %d expected a '%s', got a '%s'"):format(n,tp,type(val)),lev or 2)
+ end
+ if verify and not verify(val) then
+ error(("argument %d: '%s' %s"):format(n,val,msg),lev or 2)
+ end
+ return val
+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>,
+-- such as '>' or '#'. If a function factory has been registered for the type, it will
+-- be called to get the function.
+-- @param idx argument index
+-- @param f a function, operator string, or callable object
+-- @param msg optional error message
+-- @return a callable
+-- @raise if idx is not a number or if f is not callable
+function utils.function_arg (idx,f,msg)
+ utils.assert_arg(1,idx,'number')
+ local tp = type(f)
+ if tp == 'function' then return f end -- no worries!
+ -- ok, a string can correspond to an operator (like '==')
+ if tp == 'string' then
+ if not operators then operators = require 'pl.operator'.optable end
+ local fn = operators[f]
+ if fn then return fn end
+ local fn, err = utils.string_lambda(f)
+ if not fn then error(err..': '..f) end
+ return fn
+ elseif tp == 'table' or tp == 'userdata' then
+ local mt = getmetatable(f)
+ if not mt then error('not a callable object',2) end
+ local ff = _function_factories[mt]
+ if not ff then
+ if not mt.__call then error('not a callable object',2) end
+ return f
+ else
+ return ff(f) -- we have a function factory for this type!
+ end
+ end
+ if not msg then msg = " must be callable" end
+ if idx > 0 then
+ error("argument "..idx..": "..msg,2)
+ else
+ error(msg,2)
+ end
+end
+
+
+--- assert the common case that the argument is a string.
+-- @param n argument index
+-- @param val a value that must be a string
+-- @return the validated value
+-- @raise val must be a string
+-- @usage
+-- local val = 42
+-- local param2 = utils.assert_string(2, val) --> error: argument 2 expected a 'string', got a 'number'
+function utils.assert_string (n, val)
+ return utils.assert_arg(n,val,'string',nil,nil,3)
+end
+
+--- control the error strategy used by Penlight.
+-- This is a global setting that controls how `utils.raise` behaves:
+--
+-- - 'default': return `nil + error` (this is the default)
+-- - 'error': throw a Lua error
+-- - 'quit': exit the program
+--
+-- @param mode either 'default', 'quit' or 'error'
+-- @see utils.raise
+function utils.on_error (mode)
+ mode = tostring(mode)
+ if ({['default'] = 1, ['quit'] = 2, ['error'] = 3})[mode] then
+ err_mode = mode
+ else
+ -- fail loudly
+ local err = "Bad argument expected string; 'default', 'quit', or 'error'. Got '"..tostring(mode).."'"
+ if err_mode == 'default' then
+ error(err, 2) -- even in 'default' mode fail loud in this case
+ end
+ raise(err)
+ end
+end
+
+--- used by Penlight functions to return errors. Its global behaviour is controlled
+-- by `utils.on_error`.
+-- To use this function you MUST use it in conjunction with `return`, since it might
+-- return `nil + error`.
+-- @param err the error string.
+-- @see utils.on_error
+-- @usage
+-- if some_condition then
+-- return utils.raise("some condition was not met") -- MUST use 'return'!
+-- end
+function utils.raise (err)
+ if err_mode == 'default' then
+ return nil, err
+ elseif err_mode == 'quit' then
+ return utils.quit(err)
+ else
+ error(err, 2)
+ end
+end
+raise = utils.raise
+
+
+
+--- File handling
+-- @section files
+
+--- return the contents of a file as a string
+-- @param filename The file path
+-- @param is_bin open in binary mode
+-- @return file contents
+function utils.readfile(filename,is_bin)
+ local mode = is_bin and 'b' or ''
+ utils.assert_string(1,filename)
+ local f,open_err = io.open(filename,'r'..mode)
+ if not f then return raise (open_err) end
+ local res,read_err = f:read('*a')
+ f:close()
+ if not res then
+ -- Errors in io.open have "filename: " prefix,
+ -- error in file:read don't, add it.
+ return raise (filename..": "..read_err)
+ end
+ return res
+end
+
+--- write a string to a file
+-- @param filename The file path
+-- @param str The string
+-- @param is_bin open in binary mode
+-- @return true or nil
+-- @return error message
+-- @raise error if filename or str aren't strings
+function utils.writefile(filename,str,is_bin)
+ local mode = is_bin and 'b' or ''
+ utils.assert_string(1,filename)
+ utils.assert_string(2,str)
+ local f,err = io.open(filename,'w'..mode)
+ if not f then return raise(err) end
+ local ok, write_err = f:write(str)
+ f:close()
+ if not ok then
+ -- Errors in io.open have "filename: " prefix,
+ -- error in file:write don't, add it.
+ return raise (filename..": "..write_err)
+ end
+ return true
+end
+
+--- return the contents of a file as a list of lines
+-- @param filename The file path
+-- @return file contents as a table
+-- @raise error if filename is not a string
+function utils.readlines(filename)
+ utils.assert_string(1,filename)
+ local f,err = io.open(filename,'r')
+ if not f then return raise(err) end
+ local res = {}
+ for line in f:lines() do
+ append(res,line)
+ end
+ f:close()
+ return res
+end
+
+--- OS functions
+-- @section OS-functions
+
+--- execute a shell command and return the output.
+-- This function redirects the output to tempfiles and returns the content of those files.
+-- @param cmd a shell command
+-- @param bin boolean, if true, read output as binary file
+-- @return true if successful
+-- @return actual return code
+-- @return stdout output (string)
+-- @return errout output (string)
+function utils.executeex(cmd, bin)
+ local outfile = os.tmpname()
+ local errfile = os.tmpname()
+
+ if is_windows and not outfile:find(':') then
+ outfile = os.getenv('TEMP')..outfile
+ errfile = os.getenv('TEMP')..errfile
+ end
+ cmd = cmd .. " > " .. utils.quote_arg(outfile) .. " 2> " .. utils.quote_arg(errfile)
+
+ local success, retcode = utils.execute(cmd)
+ local outcontent = utils.readfile(outfile, bin)
+ local errcontent = utils.readfile(errfile, bin)
+ os.remove(outfile)
+ os.remove(errfile)
+ return success, retcode, (outcontent or ""), (errcontent or "")
+end
+
+--- Quote and escape an argument of a command.
+-- Quotes a single (or list of) argument(s) of a command to be passed
+-- to `os.execute`, `pl.utils.execute` or `pl.utils.executeex`.
+-- @param argument (string or table/list) the argument to quote. If a list then
+-- all arguments in the list will be returned as a single string quoted.
+-- @return quoted and escaped argument.
+-- @usage
+-- local options = utils.quote_arg {
+-- "-lluacov",
+-- "-e",
+-- "utils = print(require('pl.utils')._VERSION",
+-- }
+-- -- returns: -lluacov -e 'utils = print(require('\''pl.utils'\'')._VERSION'
+function utils.quote_arg(argument)
+ if type(argument) == "table" then
+ -- encode an entire table
+ local r = {}
+ for i, arg in ipairs(argument) do
+ r[i] = utils.quote_arg(arg)
+ end
+
+ return table.concat(r, " ")
+ end
+ -- only a single argument
+ if is_windows then
+ if argument == "" or argument:find('[ \f\t\v]') then
+ -- Need to quote the argument.
+ -- Quotes need to be escaped with backslashes;
+ -- additionally, backslashes before a quote, escaped or not,
+ -- need to be doubled.
+ -- See documentation for CommandLineToArgvW Windows function.
+ argument = '"' .. argument:gsub([[(\*)"]], [[%1%1\"]]):gsub([[\+$]], "%0%0") .. '"'
+ end
+
+ -- os.execute() uses system() C function, which on Windows passes command
+ -- to cmd.exe. Escape its special characters.
+ return (argument:gsub('["^<>!|&%%]', "^%0"))
+ else
+ if argument == "" or argument:find('[^a-zA-Z0-9_@%+=:,./-]') then
+ -- To quote arguments on posix-like systems use single quotes.
+ -- To represent an embedded single quote close quoted string ('),
+ -- add escaped quote (\'), open quoted string again (').
+ argument = "'" .. argument:gsub("'", [['\'']]) .. "'"
+ end
+
+ return argument
+ end
+end
+
+--- error out of this program gracefully.
+-- @param[opt] code The exit code, defaults to -`1` if omitted
+-- @param msg The exit message will be sent to `stderr` (will be formatted with the extra parameters)
+-- @param ... extra arguments for message's format'
+-- @see utils.fprintf
+-- @usage utils.quit(-1, "Error '%s' happened", "42")
+-- -- is equivalent to
+-- utils.quit("Error '%s' happened", "42") --> Error '42' happened
+function utils.quit(code, msg, ...)
+ if type(code) == 'string' then
+ utils.fprintf(io.stderr, code, msg, ...)
+ io.stderr:write('\n')
+ code = -1 -- TODO: this is odd, see the test. Which returns 255 as exit code
+ elseif msg then
+ utils.fprintf(io.stderr, msg, ...)
+ io.stderr:write('\n')
+ end
+ os.exit(code, true)
+end
+
+
+--- String functions
+-- @section string-functions
+
+--- escape any Lua 'magic' characters in a string
+-- @param s The input string
+function utils.escape(s)
+ utils.assert_string(1,s)
+ return (s:gsub('[%-%.%+%[%]%(%)%$%^%%%?%*]','%%%1'))
+end
+
+--- split a string into a list of strings separated by a delimiter.
+-- @param s The input string
+-- @param re optional A Lua string pattern; defaults to '%s+'
+-- @param plain optional If truthy don't use Lua patterns
+-- @param n optional maximum number of elements (if there are more, the last will remian un-split)
+-- @return a list-like table
+-- @raise error if s is not a string
+-- @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
+ while true do
+ local i2,i3 = find(s,re,i1,plain)
+ if not i2 then
+ local last = sub(s,i1)
+ if last ~= '' then append(ls,last) end
+ if #ls == 1 and ls[1] == '' then
+ return {}
+ else
+ return ls
+ end
+ end
+ append(ls,sub(s,i1,i2-1))
+ if n and #ls == n then
+ ls[#ls] = sub(s,i1)
+ return ls
+ end
+ i1 = i3+1
+ end
+end
+
+--- split a string into a number of return values.
+-- Identical to `split` but returns multiple sub-strings instead of
+-- a single list of sub-strings.
+-- @param s the string
+-- @param re A Lua string pattern; defaults to '%s+'
+-- @param plain don't use Lua patterns
+-- @param n optional maximum number of splits
+-- @return n values
+-- @usage first,next = splitv('user=jane=doe','=', false, 2)
+-- assert(first == "user")
+-- assert(next == "jane=doe")
+-- @see split
+function utils.splitv (s,re, plain, n)
+ return _unpack(utils.split(s,re, plain, n))
+end
+
+
+--- Functional
+-- @section functional
+
+
+--- 'memoize' a function (cache returned value for next call).
+-- This is useful if you have a function which is relatively expensive,
+-- but you don't know in advance what values will be required, so
+-- building a table upfront is wasteful/impossible.
+-- @param func a function of at least one argument
+-- @return a function with at least one argument, which is used as the key.
+function utils.memoize(func)
+ local cache = {}
+ return function(k)
+ local res = cache[k]
+ if res == nil then
+ res = func(k)
+ cache[k] = res
+ end
+ return res
+ end
+end
+
+
+--- associate a function factory with a type.
+-- A function factory takes an object of the given type and
+-- returns a function for evaluating it
+-- @tab mt metatable
+-- @func fun a callable that returns a function
+function utils.add_function_factory (mt,fun)
+ _function_factories[mt] = fun
+end
+
+local function _string_lambda(f)
+ if f:find '^|' or f:find '_' then
+ local args,body = f:match '|([^|]*)|(.+)'
+ if f:find '_' then
+ args = '_'
+ body = f
+ else
+ if not args then return raise 'bad string lambda' end
+ end
+ local fstr = 'return function('..args..') return '..body..' end'
+ local fn,err = utils.load(fstr)
+ if not fn then return raise(err) end
+ fn = fn()
+ return fn
+ else
+ return raise 'not a string lambda'
+ end
+end
+
+
+--- an anonymous function as a string. This string is either of the form
+-- '|args| expression' or is a function of one argument, '_'
+-- @param lf function as a string
+-- @return a function
+-- @function utils.string_lambda
+-- @usage
+-- string_lambda '|x|x+1' (2) == 3
+-- string_lambda '_+1' (2) == 3
+utils.string_lambda = utils.memoize(_string_lambda)
+
+
+--- bind the first argument of the function to a value.
+-- @param fn a function of at least two values (may be an operator string)
+-- @param p a value
+-- @return a function such that f(x) is fn(p,x)
+-- @raise same as @{function_arg}
+-- @see func.bind1
+-- @usage local function f(msg, name)
+-- print(msg .. " " .. name)
+-- end
+--
+-- local hello = utils.bind1(f, "Hello")
+--
+-- print(hello("world")) --> "Hello world"
+-- print(hello("sunshine")) --> "Hello sunshine"
+function utils.bind1 (fn,p)
+ fn = utils.function_arg(1,fn)
+ return function(...) return fn(p,...) end
+end
+
+
+--- bind the second argument of the function to a value.
+-- @param fn a function of at least two values (may be an operator string)
+-- @param p a value
+-- @return a function such that f(x) is fn(x,p)
+-- @raise same as @{function_arg}
+-- @usage local function f(a, b, c)
+-- print(a .. " " .. b .. " " .. c)
+-- end
+--
+-- local hello = utils.bind1(f, "world")
+--
+-- print(hello("Hello", "!")) --> "Hello world !"
+-- print(hello("Bye", "?")) --> "Bye world ?"
+function utils.bind2 (fn,p)
+ fn = utils.function_arg(1,fn)
+ return function(x,...) return fn(x,p,...) end
+end
+
+
+
+
+--- Deprecation
+-- @section deprecation
+
+do
+ -- the default implementation
+ local deprecation_func = function(msg, trace)
+ if trace then
+ warn(msg, "\n", trace) -- luacheck: ignore
+ else
+ warn(msg) -- luacheck: ignore
+ end
+ end
+
+ --- Sets a deprecation warning function.
+ -- An application can override this function to support proper output of
+ -- deprecation warnings. The warnings can be generated from libraries or
+ -- functions by calling `utils.raise_deprecation`. The default function
+ -- will write to the 'warn' system (introduced in Lua 5.4, or the compatibility
+ -- function from the `compat` module for earlier versions).
+ --
+ -- Note: only applications should set/change this function, libraries should not.
+ -- @param func a callback with signature: `function(msg, trace)` both arguments are strings, the latter being optional.
+ -- @see utils.raise_deprecation
+ -- @usage
+ -- -- write to the Nginx logs with OpenResty
+ -- utils.set_deprecation_func(function(msg, trace)
+ -- ngx.log(ngx.WARN, msg, (trace and (" " .. trace) or nil))
+ -- end)
+ --
+ -- -- disable deprecation warnings
+ -- utils.set_deprecation_func()
+ function utils.set_deprecation_func(func)
+ if func == nil then
+ deprecation_func = function() end
+ else
+ utils.assert_arg(1, func, "function")
+ deprecation_func = func
+ end
+ end
+
+ --- raises a deprecation warning.
+ -- For options see the usage example below.
+ --
+ -- Note: the `opts.deprecated_after` field is the last version in which
+ -- a feature or option was NOT YET deprecated! Because when writing the code it
+ -- is quite often not known in what version the code will land. But the last
+ -- released version is usually known.
+ -- @param opts options table
+ -- @see utils.set_deprecation_func
+ -- @usage
+ -- warn("@on") -- enable Lua warnings, they are usually off by default
+ --
+ -- function stringx.islower(str)
+ -- raise_deprecation {
+ -- source = "Penlight " .. utils._VERSION, -- optional
+ -- message = "function 'islower' was renamed to 'is_lower'", -- required
+ -- version_removed = "2.0.0", -- optional
+ -- deprecated_after = "1.2.3", -- optional
+ -- no_trace = true, -- optional
+ -- }
+ -- return stringx.is_lower(str)
+ -- end
+ -- -- output: "[Penlight 1.9.2] function 'islower' was renamed to 'is_lower' (deprecated after 1.2.3, scheduled for removal in 2.0.0)"
+ function utils.raise_deprecation(opts)
+ utils.assert_arg(1, opts, "table")
+ if type(opts.message) ~= "string" then
+ error("field 'message' of the options table must be a string", 2)
+ end
+ local trace
+ if not opts.no_trace then
+ trace = debug.traceback("", 2):match("[\n%s]*(.-)$")
+ end
+ local msg
+ if opts.deprecated_after and opts.version_removed then
+ msg = (" (deprecated after %s, scheduled for removal in %s)"):format(
+ tostring(opts.deprecated_after), tostring(opts.version_removed))
+ elseif opts.deprecated_after then
+ msg = (" (deprecated after %s)"):format(tostring(opts.deprecated_after))
+ elseif opts.version_removed then
+ msg = (" (scheduled for removal in %s)"):format(tostring(opts.version_removed))
+ else
+ msg = ""
+ end
+
+ msg = opts.message .. msg
+
+ if opts.source then
+ msg = "[" .. opts.source .."] " .. msg
+ else
+ if msg:sub(1,1) == "@" then
+ -- in Lua 5.4 "@" prefixed messages are control messages to the warn system
+ error("message cannot start with '@'", 2)
+ end
+ end
+
+ deprecation_func(msg, trace)
+ end
+
+end
+
+
+return utils
+
+
diff --git a/macros/luatex/generic/penlight/pl/xml.lua b/macros/luatex/generic/penlight/pl/xml.lua
new file mode 100644
index 0000000000..4650a32099
--- /dev/null
+++ b/macros/luatex/generic/penlight/pl/xml.lua
@@ -0,0 +1,776 @@
+--- XML LOM Utilities.
+--
+-- 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)
+--
+-- > d = xml.parse "<nodes><node id='1'>alice</node></nodes>"
+-- > = d
+-- <nodes><node id='1'>alice</node></nodes>
+-- > = xml.tostring(d,'',' ')
+-- <nodes>
+-- <node id='1'>alice</node>
+-- </nodes>
+--
+-- Can be used as a lightweight one-stop-shop for simple XML processing; a simple XML parser is included
+-- but the default is to use `lxp.lom` if it can be found.
+-- <pre>
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain--
+-- classic Lua XML parser by Roberto Ierusalimschy.
+-- modified to output LOM format.
+-- http://lua-users.org/wiki/LuaXml
+-- </pre>
+-- See @{06-data.md.XML|the Guide}
+--
+-- Dependencies: `pl.utils`
+--
+-- Soft Dependencies: `lxp.lom` (fallback is to use basic Lua parser)
+-- @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 _M = {}
+local Doc = { __type = "doc" };
+Doc.__index = Doc;
+
+--- create a new document node.
+-- @param tag the tag name
+-- @param attr optional attributes (table of name-value pairs)
+function _M.new(tag, attr)
+ local doc = { tag = tag, attr = attr or {}, 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
+-- @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
+ else
+ status,lom = pcall(require,'lxp.lom')
+ if not status then parser = _M.basic_parse else parser = lom.parse end
+ 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
+ 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
+ return doc
+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)
+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;
+end
+
+--- convenient function to add a text node. This updates the last inserted position.
+-- @param text a string
+function Doc:text(text)
+ (self.last_add[#self.last_add] or self):add_direct_child(text);
+ return self;
+end
+
+---- go up one level in a document
+function Doc:up()
+ t_remove(self.last_add);
+ return self;
+end
+
+function Doc:reset()
+ 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.
+-- @param child a child node (either text or a document)
+function Doc:add_direct_child(child)
+ t_insert(self, child);
+end
+
+--- append a child to a document at the last element added
+-- @param child a child node (either text or a document)
+function Doc:add_child(child)
+ (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
+end
+
+--- set a single attribute of a document node.
+-- @param a attribute
+-- @param v its value
+function Doc:set_attrib(a,v)
+ self.attr[a] = v
+end
+
+--- access the attributes of a document node.
+function Doc:get_attribs()
+ 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 templ_cache = {}
+
+local 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
+ end
+ elseif not _M.is_tag(templ) then
+ return nil, "template is not a document"
+ end
+ return templ
+end
+
+local function is_data(data)
+ return #data == 0 or type(data[1]) ~= 'table'
+end
+
+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
+ 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)
+ end
+ templ,err = template_cache(templ)
+ if err then return nil, err end
+ local function _subst(item)
+ return _M.clone(templ,function(s)
+ return s:gsub('%$(%w+)',item)
+ end)
+ 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))
+ end
+ if data.tag then
+ list = _M.elem(data.tag,list)
+ end
+ return list
+end
+
+
+--- get the first child with a given tag name.
+-- @param tag the tag name
+function Doc:child_with_name(tag)
+ 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)
+ local res = {}
+ _children_with_name(self,tag,res,not dont_recurse)
+ return res
+end
+
+-- iterate over all children of a document node, including text nodes.
+function Doc:children()
+ 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.
+function Doc:first_childtag()
+ if #self == 0 then return end
+ for _,t in ipairs(self) do
+ if type(t) == 'table' then return t end
+ end
+end
+
+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;
+end
+
+--- iterate over all child elements of a document node.
+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;
+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.
+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
+ 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;
+end
+
+-- 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
+ 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
+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
+ end
+ _dostring(t, buf, _dostring, xml_escape, nil,idn,indent, attr_indent);
+ return t_concat(buf);
+end
+
+Doc.__tostring = _M.tostring
+
+--- get the full text value of an element
+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);
+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)
+ end
+ return setmetatable(new_table, getmetatable(object))
+ end
+
+ return _copy(doc)
+end
+
+Doc.filter = _M.clone -- also available as method
+
+--- compare two documents.
+-- @param t1 any value
+-- @param t2 any value
+function _M.compare(t1,t2)
+ local ty1 = type(t1)
+ local ty2 = type(t2)
+ if ty1 ~= ty2 then return false, 'type mismatch' end
+ if ty1 == 'string' then
+ return t1 == t2 and true or 'text '..t1..' ~= text '..t2
+ 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
+ end
+ for k,v in pairs(t2.attr) do
+ if t1.attr[k] ~= v then return false, 'mismatch attrib' 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
+ end
+ return true
+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
+
+--- 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
+ for _,d in ipairs(doc) do
+ if _M.is_tag(d) then
+ _M.walk(d,depth_first,operation)
+ end
+ end
+ if depth_first then operation(doc.tag,doc) end
+end
+
+local html_empty_elements = { --lists all HTML empty (void) elements
+ br = true,
+ img = true,
+ meta = true,
+ frame = true,
+ area = true,
+ hr = true,
+ base = true,
+ col = true,
+ link = true,
+ input = true,
+ option = true,
+ param = true,
+ isindex = true,
+ 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)
+ return _M.basic_parse(s,false,true)
+end
+
+--- Parse a simple XML document using a pure Lua parser based on Robero Ierusalimschy's original version.
+-- @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
+ local stack = {}
+ local top = {}
+
+ local function parseargs(s)
+ local arg = {}
+ s:gsub("([%w:%-_]+)%s*=%s*([\"'])(.-)%2", function (w, _, a)
+ if html then w = w:lower() end
+ arg[w] = unescape(a)
+ end)
+ if html then
+ s:gsub("([%w:%-_]+)%s*=%s*([^\"']+)%s*", function (w, a)
+ w = w:lower()
+ arg[w] = unescape(a)
+ end)
+ end
+ return arg
+ end
+
+ t_insert(stack, top)
+ local ni,c,label,xarg, empty, _, istart
+ local i = 1
+ local j
+ -- we're not interested in <?xml version="1.0"?>
+ _,istart = s_find(s,'^%s*<%?[^%?]+%?>%s*')
+ if not istart then -- or <!DOCTYPE ...>
+ _,istart = s_find(s,'^%s*<!DOCTYPE.->%s*')
+ end
+ if istart then i = istart+1 end
+ while true do
+ ni,j,c,label,xarg, empty = s_find(s, "<([%/!]?)([%w:%-_]+)(.-)(%/?)>", i)
+ if not ni then break end
+ if c == "!" then -- comment
+ -- case where there's no space inside comment
+ if not (label:match '%-%-$' and xarg == '') then
+ if xarg:match '%-%-$' then -- we've grabbed it all
+ j = j - 2
+ end
+ -- match end of comment
+ _,j = s_find(s, "-->", j, true)
+ end
+ else
+ local text = s_sub(s, i, ni-1)
+ if html then
+ label = label:lower()
+ if html_empty_elements[label] then empty = "/" end
+ end
+ if all_text or not s_find(text, "^%s*$") then
+ t_insert(top, unescape(text))
+ end
+ if empty == "/" then -- empty element tag
+ t_insert(top, setmetatable({tag=label, attr=parseargs(xarg), empty=1},Doc))
+ elseif c == "" then -- start tag
+ top = setmetatable({tag=label, attr=parseargs(xarg)},Doc)
+ t_insert(stack, top) -- new level
+ else -- end tag
+ local toclose = t_remove(stack) -- remove top
+ top = stack[#stack]
+ if #stack < 1 then
+ error("nothing to close with "..label..':'..text)
+ end
+ if toclose.tag ~= label then
+ error("trying to close "..toclose.tag.." with "..label.." "..text)
+ end
+ t_insert(top, toclose)
+ end
+ end
+ i = j+1
+ end
+ local text = s_sub(s, i)
+ if all_text or not s_find(text, "^%s*$") then
+ t_insert(stack[#stack], unescape(text))
+ end
+ if #stack > 1 then
+ error("unclosed "..stack[#stack].tag)
+ end
+ local res = stack[1]
+ 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
+
+-- 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 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
+
+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
+
+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)
+ 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
+ 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
+ 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
+ -- 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
+
+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)
+
+ local res = {}
+ local ret = match(self,pat,res,true)
+ return res,ret
+end
+
+
+return _M
+
diff --git a/support/light-latex-make/README.md b/support/light-latex-make/README.md
index 1ff40ffc21..d8cee7d45b 100644
--- a/support/light-latex-make/README.md
+++ b/support/light-latex-make/README.md
@@ -41,7 +41,7 @@ Hello \textsf{llmk}!
Suppose we save this file as `hello.tex`, then run
-```
+```shell
$ llmk hello.tex
```
@@ -60,9 +60,9 @@ latex = "lualatex"
source = "hello.tex"
```
-If you run llmk without any argument, llmk will load `llmk.toml` in the working directory, and compile files specified by `source` key with the settings written in the file.
+If you run llmk without any argument, llmk will load `llmk.toml` in the working directory, and compile files specified by the `source` key with the settings written in the file.
-```
+```shell
$ llmk
```
@@ -72,7 +72,7 @@ A few other magic comment formats that are supported by existing tools are also
The directives supported by [TeXShop](https://pages.uoregon.edu/koch/texshop/) and friends, which typically start with `% !TEX`, can be used instead of `latex` and `bibtex` keys. E.g.,
-```
+```latex
%! TEX TS-program = xelatex
%! BIB TS-program = biber
\documentclass{article}
@@ -80,7 +80,7 @@ The directives supported by [TeXShop](https://pages.uoregon.edu/koch/texshop/) a
is equivalent to:
-```
+```latex
% +++
% latex = "xelatex"
% bibtex = "biber"
@@ -90,14 +90,14 @@ is equivalent to:
Another supported format is shebang-like directive that is supported by [YaTeX mode for Emacs](https://www.yatex.org/). E.g.,
-```
+```latex
%#!pdflatex
\documentclass{article}
```
is equivalent to:
-```
+```latex
% +++
% latex = "pdflatex"
% +++
@@ -115,21 +115,21 @@ Similar to [latexmk](http://personal.psu.edu/jcc8/software/latexmk/), Actions `-
Specifically,
-```
+```shell
$ llmk --clean FILE...
```
-removes files generated by the specified `FILE`s. In case you omit the argument `FILE`, files generated by the `source` files are removed. In both case, the files to remove by these actions can be customized (see the reference manual for the details).
+removes files generated by the specified `FILE`s. In case you omit the argument `FILE`, files generated by the `source` files are removed. In both cases, the files to remove by these actions can be customized (see the reference manual for the details).
## Advanced Usage
### Custom compile sequence
-You can setup custom sequence for processing LaTeX documents; use `sequence` key to specify the order of programs to process the documents and specify the detailed settings for each program.
+You can setup custom sequence for processing LaTeX documents; use `sequence` key to specify the order of programs to process the documents and specify the detailed settings for each program in the `programs` table.
For the simple use, you can specify the command name in the top-level just like `latex = "lualatex"`, which is already shown in the former examples.
-However, it is impossible to specify more detailed settings (e.g., command line options) with this simple manner. If you want to change those settings as well, you have to use tables of TOML; write `[programs.<name>]` and then write the each setting following to that:
+However, it is impossible to specify more detailed settings (e.g., command-line options) with this simple manner. If you want to change those settings as well, you have to use tables of TOML; write `[programs.<name>]` and then write the each setting following to that:
```toml
# custom sequence
diff --git a/support/light-latex-make/llmk.1 b/support/light-latex-make/llmk.1
index 3f1b06ff45..ef5dded390 100644
--- a/support/light-latex-make/llmk.1
+++ b/support/light-latex-make/llmk.1
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
-.TH "LLMK" "1" "August 2021" "llmk 1.0.0" "llmk manual"
+.TH "LLMK" "1" "August 2021" "llmk 1.1.0" "llmk manual"
.
.SH "NAME"
\fBllmk\fR \- Light LaTeX Make
diff --git a/support/light-latex-make/llmk.lua b/support/light-latex-make/llmk.lua
index 623f45099e..79264eaeea 100755
--- a/support/light-latex-make/llmk.lua
+++ b/support/light-latex-make/llmk.lua
@@ -39,7 +39,7 @@ local M = {}
-- program information
M.prog_name = 'llmk'
-M.version = '1.0.0'
+M.version = '1.1.0'
M.copyright = 'Copyright 2018-2021'
M.author = 'Takuto ASAKURA (wtsnjp)'
M.llmk_toml = 'llmk.toml'
@@ -57,8 +57,8 @@ M.top_level_spec = {
bibtex = {'string', 'bibtex'},
clean_files = {'[string]', {
'%B.aux', '%B.bbl', '%B.bcf', '%B-blx.bib', '%B.blg', '%B.fls',
- '%B.idx', '%B.ilg', '%B.ind', '%B.log', '%B.nav', '%B.out',
- '%B.run.xml', '%B.snm', '%B.toc', '%B.vrb',
+ '%B.idx', '%B.ilg', '%B.ind', '%B.lof', '%B.log', '%B.lot', '%B.nav',
+ '%B.out', '%B.run.xml', '%B.snm', '%B.toc', '%B.vrb',
}},
clobber_files = {'[string]', {'%B.dvi', '%B.pdf', '%B.ps', '%B.synctex.gz'}},
dvipdf = {'string', 'dvipdfmx'},
@@ -873,6 +873,7 @@ function M.get_toml(fn)
-- 2. TeXShop directives
ts_tmp = string.match(l, '^%s*%%%s*!%s*TEX%s+program%s*=%s*(.-)%s*$') or
+ string.match(l, '^%s*%%%s*!%s*TeX%s+program%s*=%s*(.-)%s*$') or
string.match(l, '^%s*%%%s*!%s*TEX%s+TS%-program%s*=%s*(.-)%s*$')
if ts_tmp then
ts_latex = ts_latex or ts_tmp
diff --git a/support/light-latex-make/llmk.pdf b/support/light-latex-make/llmk.pdf
index 596127da86..6693d671be 100644
--- a/support/light-latex-make/llmk.pdf
+++ b/support/light-latex-make/llmk.pdf
Binary files differ
diff --git a/support/light-latex-make/llmk.tex b/support/light-latex-make/llmk.tex
index f643717215..61673f016e 100644
--- a/support/light-latex-make/llmk.tex
+++ b/support/light-latex-make/llmk.tex
@@ -4,13 +4,13 @@
% +++
% latex = "xelatex"
% +++
-\documentclass[draft]{llmk-doc}
+\documentclass{llmk-doc}
% Metadata
\title{llmk: Light {\LaTeX} Make}
\author{Takuto Asakura (wtsnjp)}
\subtitle{Reference Manual}
-\date{v1.0.0\quad\today}
+\date{v1.1.0\quad\today}
\keywords{llmk, build-tool, toml, lua, luatex}
\begin{document}
@@ -395,8 +395,8 @@ specifiers are available for this key. The default value is:
\begin{htcode}
[
"%B.aux", "%B.bbl", "%B.bcf", "%B-blx.bib", "%B.blg", "%B.fls",
- "%B.idx", "%B.ilg", "%B.ind", "%B.log", "%B.nav", "%B.out",
- "%B.run.xml", "%B.snm", "%B.toc", "%B.vrb"
+ "%B.idx", "%B.ilg", "%B.ind", "%B.lof", "%B.log", "%B.lot",
+ "%B.nav", "%B.out", "%B.run.xml", "%B.snm", "%B.toc", "%B.vrb"
]
\end{htcode}
\end{confkey}
@@ -429,7 +429,7 @@ ineffective.
[default: \code{[]}]
Extra files to be removed with the cleaning action (\lopt{clean}). By using
this key, you can easily add files to be removed on top of the default
-\ckey{clean\_files}.
+\ckey{clean\_files}. The format specifiers are available for this key.
\end{confkey}
\begin{confkey}{latex}{type: \type{string}}[default: \code{"lualatex"}]
@@ -747,6 +747,7 @@ of the directive is:
\newcommand{\VS}{\textvisiblespace}
\begin{htcode}
\meta{TS prefix}TEX\VS{\OS}program\meta{equals}\meta{command}\OS
+\meta{TS prefix}TeX\VS{\OS}program\meta{equals}\meta{command}\OS
\meta{TS prefix}TEX\VS{\OS}TS-program\meta{equals}\meta{command}\OS
\end{htcode}
\egroup
diff --git a/systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl b/systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl
index 738109befa..066b00dc84 100644
--- a/systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl
+++ b/systems/texlive/tlnet/tlpkg/installer/ctan-mirrors.pl
@@ -37,6 +37,7 @@ $mirrors = {
},
'Iran' => {
'http://ctan.yazd.ac.ir/' => 1,
+ 'https://ctan.asis.ai/' => 1,
'https://ctan.um.ac.ir/' => 1,
},
'Japan' => {
@@ -86,6 +87,7 @@ $mirrors = {
'https://distrib-coffee.ipsl.jussieu.fr/pub/mirrors/ctan/' => 1,
'https://mirror.ibcp.fr/pub/CTAN/' => 1,
'https://mirrors.chevalier.io/CTAN/' => 1,
+ 'https://mirrors.ircam.fr/pub/CTAN/' => 1,
'https://texlive.mycozy.space/' => 1,
},
'Germany' => {
@@ -98,7 +100,6 @@ $mirrors = {
'https://ctan.mc1.root.project-creative.net/' => 1,
'https://ctan.mirror.norbert-ruehl.de/' => 1,
'https://ctan.net/' => 1,
- 'https://ctan.space-pro.be/tex-archive/' => 1,
'https://ftp.agdsn.de/pub/mirrors/latex/dante/' => 1,
'https://ftp.fau.de/ctan/' => 1,
'https://ftp.gwdg.de/pub/ctan/' => 1,
diff --git a/systems/texlive/tlnet/tlpkg/texlive.tlpdb b/systems/texlive/tlnet/tlpkg/texlive.tlpdb
index cc23aec489..31b7862853 100644
--- a/systems/texlive/tlnet/tlpkg/texlive.tlpdb
+++ b/systems/texlive/tlnet/tlpkg/texlive.tlpdb
@@ -26,11 +26,11 @@ depend container_split_src_files/1
depend frozen/0
depend minrelease/2016
depend release/2021
-depend revision/60543
+depend revision/60554
name 00texlive.image
category TLCore
-revision 60542
+revision 60554
shortdesc TeX Live files only in the source repository
longdesc The files here are not copied by the installer and containers
longdesc are not built for them; they exist only in the source
@@ -4458,7 +4458,7 @@ depend setting_available_architectures:aarch64-linux amd64-freebsd amd64-netbsd
name 00texlive.installer
category TLCore
-revision 60532
+revision 60545
shortdesc TeX Live standalone installer package
longdesc This package defines the files to go into the installer
longdesc archives (install-tl-unx.tar.gz, install-tl.zip) built by the
@@ -186049,7 +186049,7 @@ binfiles arch=x86_64-linuxmusl size=3871
name luakeys
category Package
-revision 57463
+revision 60552
shortdesc A Lua module for parsing key-value options
relocated 1
longdesc This package provides a Lua module that can parse key-value
@@ -186059,11 +186059,11 @@ longdesc entirely by using the Lua language and does not rely on TeX.
longdesc Therefore this package can only be used with the TeX engine
longdesc LuaTeX. Since luakeys uses LPeg, the parsing mechanism should
longdesc be pretty robust.
-containersize 4904
-containerchecksum 873879c6495479db77cf0e7a0f5ac99ad2eb67088d1d4abaf55e4ec341c07103ced954310b67f5ee0bec6986793f9e68613b7a85c3a8fee8b5697e0a05f1e1c5
-doccontainersize 140576
-doccontainerchecksum 58b2550998915a20226ef4890255c8a90aad9faf248a0c2a880b086941af256d9f83edc2b29ed615da6355a5bb5d95605580824581ebd7ed191d82b92786e8ac
-docfiles size=40
+containersize 5008
+containerchecksum a5e034432e0c5ac71c4945d5020b97bf464dc118ff8a700b60895f94c20424459b1229f1458a47cd35802681e3b890d25df2222a786ff4477374172664684b95
+doccontainersize 154160
+doccontainerchecksum df705ccd4bc21fd7de40bfedb6054242a5086804b464319dbc823be0481dab04d379f297e9f0ca32e32467c147cc6cf6f789d3bd43af9680b155892d2e0043ae
+docfiles size=44
RELOC/doc/luatex/luakeys/README.md details="Readme"
RELOC/doc/luatex/luakeys/luakeys-debug.tex
RELOC/doc/luatex/luakeys/luakeys.pdf details="Package documentation"
@@ -186076,7 +186076,7 @@ catalogue-contact-repository https://github.com/Josef-Friedrich/luakeys
catalogue-ctan /macros/luatex/generic/luakeys
catalogue-license lppl1.3c
catalogue-topics keyval luatex
-catalogue-version 0.1
+catalogue-version 0.2
name lualatex-doc
category Package
@@ -232794,7 +232794,7 @@ catalogue-version 1.15
name pdfextra
category Package
-revision 60542
+revision 60553
shortdesc Extra PDF features for (Op)TeX
relocated 1
longdesc This package provides extra PDF features for OpTeX (or in
@@ -232804,11 +232804,11 @@ longdesc file format in its base. This third party package aims to
longdesc provide them. Summary of supported features: insertion of
longdesc multimedia (audio, video, 3D), hyperlinks and other actions,
longdesc triggering events, transitions, attachments.
-containersize 22584
-containerchecksum d8ce55cc2d607a08634729fcac1d4baabc6418deeb08de1b185a04c9eb91c9d08e84b8470f008302266e07986d6b24a24ab3242eabb2fd24eb521cf8798d635f
-doccontainersize 644568
-doccontainerchecksum 674e5450ec14c54d0f61fd4bebb2730cf503d7ec3eeedda51634ee8ad13df23bbfeb25e9b47a53e38d35f4ab0d45b9b9e545e9a8f3f41c8ebc2c6b6b1ff9f756
-docfiles size=190
+containersize 25320
+containerchecksum 6cb9903d116aea1fdb6a00f9588e3815a4dc56b5e4c2eda19e665bdc87511c7e4dce2a94c792d3a87c181e476f06cdeb0c71c02d48fcc265c77472a784faf98c
+doccontainersize 641672
+doccontainerchecksum 398f316661cac3977130970e50970d04fbbaf569ebe4ae6b9434eac4a5a8155fa6b1e45cda929dc19f8cb82991dd8b760c3ece64af54b141efbbe83f570d385f
+docfiles size=187
RELOC/doc/optex/pdfextra/LICENSE
RELOC/doc/optex/pdfextra/README.md details="Readme"
RELOC/doc/optex/pdfextra/examples/pdfextra-example-latex.tex
@@ -232817,8 +232817,9 @@ docfiles size=190
RELOC/doc/optex/pdfextra/examples/pdfextra-example.tex
RELOC/doc/optex/pdfextra/pdfextra-doc.pdf details="Package documentation"
RELOC/doc/optex/pdfextra/pdfextra-doc.tex
- RELOC/doc/optex/pdfextra/pdfextra.tex
-runfiles size=17
+runfiles size=21
+ RELOC/tex/luatex/pdfextra/pdfextra.sty
+ RELOC/tex/luatex/pdfextra/pdfextra.tex
RELOC/tex/optex/pdfextra/pdfextra.opm
catalogue-contact-bugs https://github.com/vlasakm/pdfextra/issues
catalogue-contact-repository https://github.com/vlasakm/pdfextra
@@ -242563,7 +242564,7 @@ catalogue-version 1.0
name pmhanguljamo
category Package
-revision 54378
+revision 60550
shortdesc Poor man's Hangul Jamo input method
relocated 1
longdesc This package provides a Hangul transliteration input method
@@ -242571,11 +242572,11 @@ longdesc that allows to typeset Korean letters (Hangul) using the proper
longdesc fonts. The name is derived from "Poor man's Hangul Jamo Input
longdesc Method". The use of XeLaTeX is recommended. pdfTeX is not
longdesc supported.
-containersize 5136
-containerchecksum a4b16b981ff495212daac434123e0ab8d91ac862acf78f92ecab20c793bfc60e1b0e159113c9a5dc15fe06653531c1ac4a4de6208512baf089c299dc7dc75642
-doccontainersize 683796
-doccontainerchecksum 7188c7bd104bb507866c5b793a3da3ed640416847a032b6c2df8eb94a47ec2ff17d11d3494b2f2755c2ce3b01a12c12a2d6abd374c887c2f7418a6fb8c93aa0c
-docfiles size=220
+containersize 5168
+containerchecksum b06867c62b6aea2f0a376f2fefde6ae5f2d28b373a332a71246016cab15eeb76e141e2036ebdbb59a040027de6dfed2525cb3ff511520c17cef3bd1d24b16156
+doccontainersize 679004
+doccontainerchecksum 71735651b373ef4af88e541390bcd3d6d78b8689f31fc283ba539a41ded8aca30d5d53cea9be87ad9c817b878b7275b4337eab09c54d25bc7f6324b0f29dba3e
+docfiles size=219
RELOC/doc/latex/pmhanguljamo/README.md details="Readme"
RELOC/doc/latex/pmhanguljamo/munjangganghwa.jpg
RELOC/doc/latex/pmhanguljamo/pmhanguljamo-doc.pdf details="Package documentation (English)"
@@ -242589,7 +242590,7 @@ catalogue-also kotex-utf
catalogue-ctan /language/korean/pmhanguljamo
catalogue-license lppl1.3
catalogue-topics korean xetex
-catalogue-version 0.3.4
+catalogue-version 0.3.6
name pmx
category Package
@@ -295274,7 +295275,7 @@ docfiles size=376
name texlive-scripts
category TLCore
-revision 60532
+revision 60545
shortdesc TeX Live infrastructure programs
longdesc Includes install-tl, tl-portable, rungs, etc.; not needed for
longdesc tlmgr to run but still ours. Not included in tlcritical.
@@ -295283,9 +295284,9 @@ depend texlive.infra
execute addMap mathpple.map
postaction shortcut type=menu name="TeX Live command-line" cmd=TEXDIR/tlpkg/installer/tl-cmd.bat
containersize 108292
-containerchecksum 89850434938f3810cbd4c01a4ffcd4c622280e1b127452fda1da47994c384870c689f661d49b4a6ba40c0ebfcb8861413a9f4ef4ce92ffd18850441e4c8782b9
-doccontainersize 400072
-doccontainerchecksum cb1849f68dbd78d13dece3609a9dc5eb9574309e7abe9e4623649a68fe8603da4c4f68122e68eaf4d9e6e83d234006566b28eca62d2d448521c65b67908faa45
+containerchecksum 5229a47b5bf1038d7b385e66f4d84b9f4a2bc83d33e2b09cc1097cddf88a78d58938565454522ca20180e242dd4d1a14d89831b9691bb980821a82296febe783
+doccontainersize 399960
+doccontainerchecksum b33a03a0df578e035388c7899eb727d21810bf0cdbbd72166bd1031d6f07ce22c78bead2f326655ff13f6f9ea73314ce21a4df8b4dbe3f3dac297648f44cf367
docfiles size=495
doc.html
texmf-dist/doc/man/man1/fmtutil-sys.1
@@ -303023,17 +303024,17 @@ catalogue-version 1.5
name tiscreen
category Package
-revision 60020
+revision 60551
shortdesc Mimic the screen of older Texas Instruments calculators
relocated 1
longdesc This package mimics the screen of older Texas Instruments dot
longdesc matrix display calculators, specifically the TI-82 STATS. It
longdesc relies on the lcd and xcolor packages.
-containersize 1184
-containerchecksum f066a7c9cbe1464bac88761c6d24f128b58dbc4a667da699dde4d8a08e1942452cb0e43f6c00b5472738896b554b45a4604c8eb53712547576c302d0daf8c622
-doccontainersize 135332
-doccontainerchecksum 47eba704465400cec1c8edf68781465bcbd89e9014d245e1c26e45701ac6e9dce5297ac18c195d2a3e36fbc66e2a39aac3f53d298aec017e9fc6ceae6f249a07
-docfiles size=35
+containersize 1584
+containerchecksum 5f3506fbb5952be7477f9c29bedc6a1ef229753ad12b484e1c286c6211bb5fc62df69f9985b44740f12728e067c69140ac064d764ca034d1edae099f623413cd
+doccontainersize 152220
+doccontainerchecksum c964f4b02f2399bc1bab1753681442076099c2f7377facf62241afc13a2db877287692b3020ca39d9a56370b13a8148d45317a4643e1552b7a436dc87c2da0f0
+docfiles size=40
RELOC/doc/latex/tiscreen/README details="Readme"
RELOC/doc/latex/tiscreen/tiscreen-doc.pdf details="Package documentation"
RELOC/doc/latex/tiscreen/tiscreen-doc.tex
@@ -334495,10 +334496,10 @@ binfiles arch=universal-darwin size=60
name xpdfopen.win32
category TLCore
-revision 59385
+revision 60546
shortdesc win32 files of xpdfopen
-containersize 629004
-containerchecksum f565eab10f56433c0e5ca04577380daff1eebd1bf2605a206cb4338611d338b46909de8f7a3260b75d07ac29f091f7f343e3a754f3c0956d04254f1d9a761fa7
+containersize 628828
+containerchecksum bfc101c03f7aa17094d0d59ae736921ce706629e0f3a425b17648ccd96a6c0248ba688b565109f328e297d704da238321e29bf0f626a4b7a9eef4104cadafbfc
binfiles arch=win32 size=392
bin/win32/pdfclose.exe
bin/win32/pdfdde.exe
diff --git a/systems/texlive/tlnet/tlpkg/texlive.tlpdb.md5 b/systems/texlive/tlnet/tlpkg/texlive.tlpdb.md5
index 817b71f5d4..a95e1a2ae0 100644
--- a/systems/texlive/tlnet/tlpkg/texlive.tlpdb.md5
+++ b/systems/texlive/tlnet/tlpkg/texlive.tlpdb.md5
@@ -1 +1 @@
-f1fa0090262119167c256abcc121f02e texlive.tlpdb
+3353508762ec8c14d9b18d6d837e25e9 texlive.tlpdb
diff --git a/systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512 b/systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512
index 02531e14a6..0107280157 100644
--- a/systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512
+++ b/systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512
@@ -1 +1 @@
-e902f68a5ecd3fc8257193e78d1862f61e7806d2c5b27d123eb36f1fbbceb704f4059b6801f0093c023685b797352f99adc843d10d13c12fe0a198b1fae25d13 texlive.tlpdb
+1b61cd651accd98a6e8dd2162f27a401f1be922747e61b88161a74d6527f7742084f7f7acf58f96bb9ac519b2dcd7344aedde4ff27facae2d5d1cfbdb2e2803a texlive.tlpdb
diff --git a/systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc b/systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc
index f877b3e70c..b8f2dc26c2 100644
--- a/systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc
+++ b/systems/texlive/tlnet/tlpkg/texlive.tlpdb.sha512.asc
@@ -1,10 +1,10 @@
-----BEGIN PGP SIGNATURE-----
-iQEcBAEBCgAGBQJhRnv9AAoJEEzhh34ZQ4xwQJYH+QH7/uaFAMswnd/gsv5C1T6Q
-JOcAvjF7uvffRpF/71eePdGZfPN4rMB7IOtQvgiaHMtbgc84hYYzWlE319q+er3j
-o+qfFqGZE8EZz67IVxjurUfTklzxkKZAmL80MS2e8h0Qu4JCM3GGElG3i3kUv9DY
-RpOO7FK/p6s3unbigsBiBUp673W51vxjCzM1+lbbAsEPO5OKqWia3jIEtIV7l4Tf
-eSQ+dmV005/XBWii7TWBSfPf/Iyj+33OajXFFPkkSFtsBtsy2zT7rChwyO4DZskd
-dU201Cmyh7i9tlZEXCJU8ZzdaHPWf8TsYheiO9vnvTJ7xxmrux7Di+KZ0GfTS3M=
-=mYwD
+iQEcBAEBCgAGBQJhR81gAAoJEEzhh34ZQ4xwYb4H/iA1TE5B3STBVuchvk0QdtCw
+XklW0onwJsMlKUZnBKM9UF5R48XgBKBz4eRHySbjowD9FRTsFpJgUa59DfN6F1dp
+T5TTrR9jOdovu24vbWGRfYG3sGTgjm4uzZsQHw2QLqtkOUVSYnwNnEMt4uu8zMyH
+qx2nMtF7CG4jCeVX1S7ZdKHaPxu6aO+7phuSbyq6XO758q2JHV7pqpfaBJSBYTZc
+QB78noIAWthByMy05WUv911qUnQshl1EDe1exzx4ungn83nKfflrnb9sb2ch2raz
+lSQ3P6+N6qTgjo6CZy74mRPyQcvLt7bLNzJ24/4rSj9GuD/aMu+O4MgAZ0kyTe4=
+=dIK1
-----END PGP SIGNATURE-----
diff --git a/systems/texlive/tlnet/tlpkg/texlive.tlpdb.xz b/systems/texlive/tlnet/tlpkg/texlive.tlpdb.xz
index 5e15e6964d..dd4f2888e2 100644
--- a/systems/texlive/tlnet/tlpkg/texlive.tlpdb.xz
+++ b/systems/texlive/tlnet/tlpkg/texlive.tlpdb.xz
Binary files differ
diff --git a/systems/win32/miktex/tm/packages/next/pr.ini b/systems/win32/miktex/tm/packages/next/pr.ini
index 60fd9c6c27..c81159d640 100644
--- a/systems/win32/miktex/tm/packages/next/pr.ini
+++ b/systems/win32/miktex/tm/packages/next/pr.ini
@@ -1,13 +1,13 @@
[repository]
-date=1631996132
+date=1632082534
lastupd=miktex-arctrl-bin-2.9 miktex-arctrl-bin-x64-2.9 miktex-asymptote-bin-2.9 miktex-asymptote-bin-x64-2.9 miktex-autosp-bin-2.9 miktex-autosp-bin-x64-2.9 miktex-axohelp-bin-2.9 miktex-axohelp-bin-x64-2.9 miktex-bibarts-bin-2.9 miktex-bibarts-bin-x64-2.9 miktex-bibtex-bin-2.9 miktex-bibtex-bin-x64-2.9 miktex-bibtex8bit-bin-2.9 miktex-bibtex8bit-bin-x64-2.9 miktex-bzip2-bin-2.9 miktex-bzip2-bin-x64-2.9 miktex-cairo-bin-2.9 miktex-cairo-bin-x64-2.9 miktex-chktex-bin-2.9 miktex-chktex-bin-x64-2.9
-lstdigest=d62b347e587f86e22b9bfcb448ec5cd3
+lstdigest=67e59890212ddd3b54fa6d7e1d45410a
numpkg=4802
relstate=next
-version=7931
+version=7932
;;;;This configuration file is signed by a MiKTeX maintainer. The signature follows.
;;;;-----BEGIN MIKTEX SIGNATURE-----
-;;;; signature/miktex: ir39daU+jXDpbLi4ods7jLP4V0TxMrycBhlBKVm7qBGeQbPsCujm0AxTA6DwRd2SYOl9f5C8TVJC1W65q8qcbtsEOqMOT3SezdwejoAwSc/N+Esg+2vgFJmmg50mQ0RWJrZhnTerkk1fWDs0DMFEkQXAgf9HbK+lZMy5wK9yUiuxiCevqM0lbqjjyss3wClKEMWPWFt8HKLMu0IaF1lgkUk9S6i2KPUnOQdPzO9kS0SYq6PwH4G5QXWZyYei3McA1rq6An3lLa0miQiTWnC1/W3nT+YcS4vq2xmLLCE/GcxfPzjpKNRhwEO/2WKt7I2rh1zYs56kzSoD76h1E8juNg==
+;;;; signature/miktex: JbLfNeWiXlsyS521ppv+KXPA/a2sOMNIitPU9hE/dcHbc/mFdZj/14CmyT+W8cAcLYwcMtWie3ITUsIR+C8gXG8k89OOP7k0x2nfWDhQNa9J9SdPR+D/n2+BZ4b1PpXq8IremH5XcQISdDrtJ2aSHONbjQG4m0UzR3XOQvPhhAupgWdMMcXvnSANSwV3VXdrwSVqdQoNFTQlKLGWDdtdlQvQd5SYuMvzrDS+Q2ghNj0yRzjjvkj+mWaKQ6GLzeOPE8erQEgHuk+w3ZdFfkwEjTIOuFlt7Mj6LcCFk6M2/topjM0hIj/PDUIwK/BOdiMpLm5L7BXPgzH/rcFQfWj0IA==
;;;;-----END MIKTEX SIGNATURE-----
diff --git a/systems/win32/miktex/tm/packages/pr.ini b/systems/win32/miktex/tm/packages/pr.ini
index 5a317fe61c..4040740dbc 100644
--- a/systems/win32/miktex/tm/packages/pr.ini
+++ b/systems/win32/miktex/tm/packages/pr.ini
@@ -1,13 +1,13 @@
[repository]
-date=1631995864
+date=1632082316
lastupd=clicks clicks__doc clicks__source crumbs crumbs__doc crumbs__source ffcode ffcode__doc ffcode__source huawei huawei__doc huawei__source kpfonts-otf kpfonts-otf__doc kpfonts-otf__source latex2nemeth latex2nemeth__doc latex2nemeth__source latexindent latexindent__doc
-lstdigest=813dbdee3398f3fa2f6812b4c55b7e19
+lstdigest=dd777ba4b5affb9c7ac82aeef02689d6
numpkg=4802
relstate=stable
-version=7931
+version=7932
;;;;This configuration file is signed by a MiKTeX maintainer. The signature follows.
;;;;-----BEGIN MIKTEX SIGNATURE-----
-;;;; signature/miktex: PaUlx8yoUDm1a9RGNGimdQWJQRXdDRYez9NYiszQl6op7JLulg1xivTS7WJTHUHCFUuC32Z4cDrE+xnrpHXWiVEhaQogbv/vmEEQBbKvG1Y3tuLn3o7Ei5bzUgoiyrhUUeF0qMoQhv1oxIOrf+3VXxcLHbkxz39nPEqQNMVtijn3B1LyzZkioX+lXrPU2kYCHOxEq6f93qHNpLMY5D1EKOjBFewjUsP+5uH6/lvnxkKlnysIjXlCsVzrV06pD0f09ErcNHD3LmQ/d5BFR05B1y2+HlpqQGzlFZ9ffM4u4jIylX5t+UfGH4OiHlqonZ74BlagEOSOFkRr6r9OAwfebA==
+;;;; signature/miktex: g4VympRalq1ODqbHHTFdMl5t925OGRIjtlmmOXr0k4Wf1HGoO9BShxKABqdYgaDTzEbJ8/aSkeBZvDhB6Z8cYwGzegXdHeEjuhTDBLLiq17gg/O6RBPMpD/2GLnlxCZMhKVTVR1RZC+NtTAOSLfnUnkUTYPzqvz5oquNlBVTj7ghR7wk4u1cznefvrVmIFG/j4mzQyIt4rB4k9HFDV//U2B8YbdZwLGMNn8hfKJPPelcDHKKNl+G7fyHORBBmQPFsZB58KfAHgVrU5/ndB3h+RkjtXJhX4uDdItCL6gTlcb4tRzbUdK3rX7eIHVdZQ8JFR2zv1AK9wSG7Rcatk9Zww==
;;;;-----END MIKTEX SIGNATURE-----
diff --git a/timestamp b/timestamp
index 1f286bd35b..fe4887f881 100644
--- a/timestamp
+++ b/timestamp
@@ -2,4 +2,4 @@
# The source CTAN of this site's material:
dante.ctan.org
# The year-month-day-hour-minute of this site's material:
-2021-09-19-02-02
+2021-09-20-02-02