summaryrefslogtreecommitdiff
path: root/macros
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2021-09-22 03:00:42 +0000
committerNorbert Preining <norbert@preining.info>2021-09-22 03:00:42 +0000
commit094234b56c200ad35a041aa579c18cbcac8933ed (patch)
tree730b37a8684e85e78d7098d8cf34facf76684b3d /macros
parent32e0f5d0a5abc4f2b6c7341f19b854b5945c3ebd (diff)
CTAN sync 202109220300
Diffstat (limited to 'macros')
-rw-r--r--macros/generic/expkv-cs/README.md2
-rw-r--r--macros/generic/expkv-cs/expkv-cs.dtx344
-rw-r--r--macros/generic/expkv-cs/expkv-cs.pdfbin454716 -> 460837 bytes
-rw-r--r--macros/generic/expkv-def/README.md2
-rw-r--r--macros/generic/expkv-def/expkv-def.dtx79
-rw-r--r--macros/generic/expkv-def/expkv-def.pdfbin407494 -> 410977 bytes
-rw-r--r--macros/generic/expkv/README.md2
-rw-r--r--macros/generic/expkv/expkv.dtx181
-rw-r--r--macros/generic/expkv/expkv.pdfbin487605 -> 490757 bytes
-rw-r--r--macros/luatex/generic/penlight/penlight.lua13705
-rw-r--r--macros/luatex/generic/penlight/penlight.pdfbin20067 -> 22977 bytes
-rw-r--r--macros/luatex/generic/penlight/penlight.sty23
-rw-r--r--macros/luatex/generic/penlight/penlight.tex65
-rw-r--r--macros/luatex/generic/penlight/penlightextras.lua413
-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--macros/xetex/latex/hanzibox/README.md52
-rwxr-xr-xmacros/xetex/latex/hanzibox/build.sh9
-rw-r--r--macros/xetex/latex/hanzibox/hanzibox.dtx832
-rw-r--r--macros/xetex/latex/hanzibox/hanzibox.pdfbin0 -> 250802 bytes
59 files changed, 15436 insertions, 13827 deletions
diff --git a/macros/generic/expkv-cs/README.md b/macros/generic/expkv-cs/README.md
index 329a8f6ef2..648b3b5a59 100644
--- a/macros/generic/expkv-cs/README.md
+++ b/macros/generic/expkv-cs/README.md
@@ -1,7 +1,7 @@
-------------------------------------------------------------------------------
# expkv-cs -- define expandable key=val macros using expkv
-Version 2021-07-07 v1.1
+Version 2021-09-20 v1.1a
Released under the LaTeX Project Public License v1.3c or later
See http://www.latex-project.org/lppl.txt
diff --git a/macros/generic/expkv-cs/expkv-cs.dtx b/macros/generic/expkv-cs/expkv-cs.dtx
index f0582ca236..ffb706b221 100644
--- a/macros/generic/expkv-cs/expkv-cs.dtx
+++ b/macros/generic/expkv-cs/expkv-cs.dtx
@@ -43,6 +43,7 @@ This work consists of the file expkv-cs.dtx
and the derived files expkv-cs.pdf
expkv-cs.sty
expkv-cs.tex
+ t-expkv-cs.tex
\endpreamble
% stop docstrip adding \endinput
@@ -50,6 +51,7 @@ and the derived files expkv-cs.pdf
\endpostamble
\generate{\file{expkv-cs.sty}{\from{expkv-cs.dtx}{pkg}}}
\generate{\file{expkv-cs.tex}{\from{expkv-cs.dtx}{tex}}}
+\generate{\file{t-expkv-cs.tex}{\from{expkv-cs.dtx}{ctx}}}
\ifx\fmtname\nameofplainTeX
\expandafter\endbatchfile
\else
@@ -108,6 +110,7 @@ and the derived files expkv-cs.pdf
TeX,
textit,texttt,
usepackage,
+ usemodule,
}
,moretexcs=[6]^^A used packages
{
@@ -285,8 +288,8 @@ and the derived files expkv-cs.pdf
\DoNotIndex{\protected,\ProvidesFile,\ProvidesPackage,\relax,\space,\advance}
\DoNotIndex{\@,\unexpanded,\string,\expanded,\dimexpr,\global,\glueexpr,\hbox}
\DoNotIndex{\numexpr,\RequirePackage,\setbox,\the,\unless,\xdef,\gdef,\newcount}
-\DoNotIndex{\number,\detokenize,\meaning}
-\DoNotIndex{\^,\@firstofone,\@firstoftwo,\@gobble,\@secondoftwo}
+\DoNotIndex{\number,\detokenize,\meaning,\lccode,\lowercase}
+\DoNotIndex{\^,\\,\@firstofone,\@firstoftwo,\@gobble,\@secondoftwo}
\DoNotIndex{\ifcsname}
\DoNotIndex{\ifx}
\DoNotIndex{\ifodd}
@@ -372,12 +375,13 @@ and the derived files expkv-cs.pdf
% |\ekvcSplit|, is very convenient in my opinion, so if you just want to define
% a single macro with just a few keys this might be the way to go.
%
-% \expkvc\ is usable as generic code and as a \LaTeX\ package. It'll
-% automatically load \expkv\ in the same mode as well. To use it, just use one
-% of
+% \expkvc\ is usable as generic code, as a \LaTeX\ package, and as a \ConTeXt\
+% module. It'll automatically load \expkv\ in the same mode as well. To use it,
+% just use one of
% \begin{lstlisting}
-% \usepackage{expkv-cs} % LaTeX
% \input expkv-cs % plainTeX
+% \usepackage{expkv-cs} % LaTeX
+% \usemodule[expkv-cs] % ConTeXt
% \end{lstlisting}
%
%
@@ -1523,6 +1527,22 @@ and the derived files expkv-cs.pdf
% \gobbledocstriptag
%</pkg>
%^^A=<<
+%^^A the ConTeXt module >>=
+% \subsection{The \ConTeXt\ module}
+% \gobbledocstriptag
+%<*ctx>
+% \begin{macrocode}
+\writestatus{loading}{ConTeXt User Module / expkv-cs}
+\usemodule[expkv]
+\unprotect
+\input expkv-cs.tex
+\writestatus{loading}
+ {ConTeXt User Module / expkv-cs / Version \ekvcVersion\space loaded}
+\protect\endinput
+% \end{macrocode}
+% \gobbledocstriptag
+%</ctx>
+%^^A=<<
%^^A main file >>=
% \subsection{The Generic Code}
% The rest of this implementation will be the generic code.
@@ -1548,8 +1568,8 @@ and the derived files expkv-cs.pdf
% \begin{macro}{\ekvcVersion,\ekvcDate}
% We're on our first input, so lets store the version and date in a macro.
% \begin{macrocode}
-\def\ekvcVersion{1.1}
-\def\ekvcDate{2021-07-07}
+\def\ekvcVersion{1.1a}
+\def\ekvcDate{2021-09-20}
% \end{macrocode}
% \end{macro}
%
@@ -1636,9 +1656,9 @@ and the derived files expkv-cs.pdf
}
\def\ekvc@ekvset@pre@expander@b#1\ekvc@stop#2\ekvc@stop
{%
- \unexpanded\expandafter{\ekv@alignsafe}%
- \unexpanded{#1}##1\unexpanded{#2}%
- \unexpanded\expandafter{\ekv@endalignsafe}%
+ \ekv@unexpanded\expandafter{\ekv@alignsafe}%
+ \ekv@unexpanded{#1}##1\ekv@unexpanded{#2}%
+ \ekv@unexpanded\expandafter{\ekv@endalignsafe}%
}
% \end{macrocode}
% \end{macro}
@@ -1675,17 +1695,17 @@ and the derived files expkv-cs.pdf
\ekvc@any@long\edef#1##1%
{%
\expandafter\ekvc@ekvset@pre@expander\expandafter{\ekvc@set}%
- \unexpanded\expandafter
+ \ekv@unexpanded\expandafter
{\csname ekvc@split@\the\ekvc@keycount\endcsname}%
- \unexpanded\expandafter{\ekvc@initials{}#2}%
+ \ekv@unexpanded\expandafter{\ekvc@initials{}#2}%
}%
}%
{%
\ekvc@any@long\edef#1##1%
{%
\expandafter\ekvc@ekvset@pre@expander\expandafter{\ekvc@set}%
- \unexpanded{#2}%
- \unexpanded\expandafter{\ekvc@initials}%
+ \ekv@unexpanded{#2}%
+ \ekv@unexpanded\expandafter{\ekvc@initials}%
}%
}%
}
@@ -1743,7 +1763,7 @@ and the derived files expkv-cs.pdf
{%
\begingroup
\edef\ekvc@tmp
- {\endgroup\def\unexpanded{\ekvc@tmp}{\ekvcSplit@build@argspec@{1}}}%
+ {\endgroup\def\ekv@unexpanded{\ekvc@tmp}{\ekvcSplit@build@argspec@{1}}}%
\ekvc@tmp
}
\def\ekvcSplit@build@argspec@#1%
@@ -1753,7 +1773,7 @@ and the derived files expkv-cs.pdf
\fi
\@firstofone
{%
- \unexpanded\expandafter{\csname ekvc@splitmark@#1\endcsname####}#1%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@#1\endcsname####}#1%
\expandafter\ekvcSplit@build@argspec@\expandafter{\the\numexpr#1+1}%
}%
}
@@ -1763,7 +1783,7 @@ and the derived files expkv-cs.pdf
% \begin{macro}[internal]
% {
% \ekvc@SetupSplitKeys, \ekvc@SetupSplitKeys@a, \ekvc@SetupSplitKeys@b,
-% \ekvc@SetupSplitKeys@c,
+% \ekvc@SetupSplitKeys@c, \ekvc@SetupSplitKeys@d
% \ekvc@SetupSplitKeys@check@unknown,\ekvc@SetupSplitKeys@unknown
% }
% These macros parse the list of keys and set up the key macros. First we need
@@ -1780,24 +1800,26 @@ and the derived files expkv-cs.pdf
% Then we need to step the key counter for each key. Also we have to check
% whether this key has a |long| prefix so we initialise |\ekvc@long|.
% \begin{macrocode}
-\protected\def\ekvc@SetupSplitKeys@a#1%
+\protected\long\def\ekvc@SetupSplitKeys@a#1%
+ {\expandafter\ekvc@SetupSplitKeys@b\detokenize{#1}\ekvc@stop}
+\protected\def\ekvc@SetupSplitKeys@b#1\ekvc@stop
{%
\advance\ekvc@keycount1
\let\ekvc@long\ekv@empty
\ekvc@ifspace{#1}%
- {\ekvc@SetupSplitKeys@b#1\ekvc@stop}%
- {\ekvc@SetupSplitKeys@c{#1}}%
+ {\ekvc@SetupSplitKeys@c#1\ekvc@stop}%
+ {\ekvc@SetupSplitKeys@d{#1}}%
}
% \end{macrocode}
% If there was a space, there might be a prefix. If so call the prefix macro,
-% else call the next step |\ekvc@SetupSplitKeys@c| which will define the key
+% else call the next step |\ekvc@SetupSplitKeys@d| which will define the key
% macro and add the key's value to the initials list.
% \begin{macrocode}
-\protected\def\ekvc@SetupSplitKeys@b#1 #2\ekvc@stop
+\protected\long\def\ekvc@SetupSplitKeys@c#1 #2\ekvc@stop
{%
\ekv@ifdefined{ekvc@split@p@#1}%
{\csname ekvc@split@p@#1\endcsname{#2}}%
- {\ekvc@SetupSplitKeys@c{#1 #2}}%
+ {\ekvc@SetupSplitKeys@d{#1 #2}}%
}
% \end{macrocode}
% The inner definition is grouped, because we don't want to actually define
@@ -1807,18 +1829,18 @@ and the derived files expkv-cs.pdf
% the value given to the key. Additionally we'll need a sorting macro for each
% key count in use so we set it up with |\ekvc@setup@splitmacro|.
% \begin{macrocode}
-\protected\long\def\ekvc@SetupSplitKeys@c#1#2%
+\protected\long\def\ekvc@SetupSplitKeys@d#1#2%
{%
\begingroup
\edef\ekvc@tmp
{%
\endgroup
- \long\def\unexpanded{\ekvc@tmp}####1####2%
- \unexpanded\expandafter
+ \long\def\ekv@unexpanded{\ekvc@tmp}####1####2%
+ \ekv@unexpanded\expandafter
{\csname ekvc@splitmark@\the\ekvc@keycount\endcsname}####3%
{%
####2%
- \unexpanded\expandafter
+ \ekv@unexpanded\expandafter
{\csname ekvc@splitmark@\the\ekvc@keycount\endcsname}{####1}%
}%
% \end{macrocode}
@@ -1829,18 +1851,19 @@ and the derived files expkv-cs.pdf
% reordering.
% \begin{macrocode}
\unless\ifx\ekvc@long\long
- \let\unexpanded\expandafter
- {\csname ekvc@\ekvc@set(#1)\endcsname\ekvc@tmp}%
- \def\unexpanded{\ekvc@tmp}####1%
+ \let\ekv@unexpanded\expandafter
+ {\csname ekvc@\ekvc@set(\detokenize{#1})\endcsname\ekvc@tmp}%
+ \def\ekv@unexpanded{\ekvc@tmp}####1%
{%
- \unexpanded\expandafter{\csname ekvc@\ekvc@set(#1)\endcsname}%
+ \ekv@unexpanded\expandafter
+ {\csname ekvc@\ekvc@set(\detokenize{#1})\endcsname}%
{####1}%
}%
\fi
- \def\unexpanded{\ekvc@initials}%
+ \def\ekv@unexpanded{\ekvc@initials}%
{%
- \unexpanded\expandafter{\ekvc@initials}%
- \unexpanded\expandafter
+ \ekv@unexpanded\expandafter{\ekvc@initials}%
+ \ekv@unexpanded\expandafter
{\csname ekvc@splitmark@\the\ekvc@keycount\endcsname{#2}}%
}%
}%
@@ -1886,7 +1909,7 @@ and the derived files expkv-cs.pdf
{##3#1{##4,##2= {##1} }}%
\long\expandafter\def\csname\ekv@name\ekvc@set{}uN\endcsname##1##2#1##3%
{##2#1{##3,##1}}%
- \edef\ekvc@initials{\unexpanded\expandafter{\ekvc@initials#1{}}}%
+ \edef\ekvc@initials{\ekv@unexpanded\expandafter{\ekvc@initials#1{}}}%
\ekvc@helpers@needed
{\expandafter\ekvc@setup@splitmacro\expandafter{\the\ekvc@keycount}}%
{}%
@@ -1902,7 +1925,7 @@ and the derived files expkv-cs.pdf
{%
\let\ekvc@long\long
\let\ekvc@any@long\long
- \ekvc@SetupSplitKeys@c
+ \ekvc@SetupSplitKeys@d
}
% \end{macrocode}
% \end{macro}
@@ -1911,7 +1934,7 @@ and the derived files expkv-cs.pdf
% The |short| prefix does essentially nothing, it is only provided to allow
% key names starting with |long| that aren't |\long|.
% \begin{macrocode}
-\def\ekvc@split@p@short{\ekvc@SetupSplitKeys@c}
+\def\ekvc@split@p@short{\ekvc@SetupSplitKeys@d}
% \end{macrocode}
% \end{macro}
%
@@ -1939,53 +1962,53 @@ and the derived files expkv-cs.pdf
\begingroup
\edef\ekvc@tmp
{%
- \long\gdef\unexpanded\expandafter{\csname ekvc@split@1\endcsname}%
- \unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}%
+ \long\gdef\ekv@unexpanded\expandafter{\csname ekvc@split@1\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}%
##1##2##3%
{##3{##1}##2}%
- \long\gdef\unexpanded\expandafter{\csname ekvc@split@2\endcsname}%
- \unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
- \unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
+ \long\gdef\ekv@unexpanded\expandafter{\csname ekvc@split@2\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
##3##4%
{##4{##1}{##2}##3}%
- \long\gdef\unexpanded\expandafter{\csname ekvc@split@3\endcsname}%
- \unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
- \unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
- \unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
+ \long\gdef\ekv@unexpanded\expandafter{\csname ekvc@split@3\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
##4##5%
{##5{##1}{##2}{##3}##4}%
- \long\gdef\unexpanded\expandafter{\csname ekvc@split@4\endcsname}%
- \unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
- \unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
- \unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
- \unexpanded\expandafter{\csname ekvc@splitmark@4\endcsname}##4%
+ \long\gdef\ekv@unexpanded\expandafter{\csname ekvc@split@4\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@4\endcsname}##4%
##5##6%
{##6{##1}{##2}{##3}{##4}##5}%
- \long\gdef\unexpanded\expandafter{\csname ekvc@split@5\endcsname}%
- \unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
- \unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
- \unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
- \unexpanded\expandafter{\csname ekvc@splitmark@4\endcsname}##4%
- \unexpanded\expandafter{\csname ekvc@splitmark@5\endcsname}##5%
+ \long\gdef\ekv@unexpanded\expandafter{\csname ekvc@split@5\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@4\endcsname}##4%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@5\endcsname}##5%
##6##7%
{##7{##1}{##2}{##3}{##4}{##5}##6}%
- \long\gdef\unexpanded\expandafter{\csname ekvc@split@6\endcsname}%
- \unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
- \unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
- \unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
- \unexpanded\expandafter{\csname ekvc@splitmark@4\endcsname}##4%
- \unexpanded\expandafter{\csname ekvc@splitmark@5\endcsname}##5%
- \unexpanded\expandafter{\csname ekvc@splitmark@6\endcsname}##6%
+ \long\gdef\ekv@unexpanded\expandafter{\csname ekvc@split@6\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@4\endcsname}##4%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@5\endcsname}##5%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@6\endcsname}##6%
##7##8%
{##8{##1}{##2}{##3}{##4}{##5}{##6}##7}%
- \long\gdef\unexpanded\expandafter{\csname ekvc@split@7\endcsname}%
- \unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
- \unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
- \unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
- \unexpanded\expandafter{\csname ekvc@splitmark@4\endcsname}##4%
- \unexpanded\expandafter{\csname ekvc@splitmark@5\endcsname}##5%
- \unexpanded\expandafter{\csname ekvc@splitmark@6\endcsname}##6%
- \unexpanded\expandafter{\csname ekvc@splitmark@7\endcsname}##7%
+ \long\gdef\ekv@unexpanded\expandafter{\csname ekvc@split@7\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@1\endcsname}##1%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@2\endcsname}##2%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@3\endcsname}##3%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@4\endcsname}##4%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@5\endcsname}##5%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@6\endcsname}##6%
+ \ekv@unexpanded\expandafter{\csname ekvc@splitmark@7\endcsname}##7%
##8##9%
{##9{##1}{##2}{##3}{##4}{##5}{##6}{##7}##8}%
}
@@ -1999,12 +2022,13 @@ and the derived files expkv-cs.pdf
\edef\ekvc@tmp
{%
\long\gdef
- \unexpanded\expandafter{\csname ekvc@split@#1\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@split@#1\endcsname}%
####1%
- \unexpanded\expandafter{\csname ekvc@splitmark@#1\endcsname}%
+ \ekv@unexpanded\expandafter
+ {\csname ekvc@splitmark@#1\endcsname}%
####2####3%
{%
- \unexpanded\expandafter
+ \ekv@unexpanded\expandafter
{\csname ekvc@split@\the\numexpr#1-1\relax\endcsname}%
####1{{####2}####3}%
}%
@@ -2043,16 +2067,16 @@ and the derived files expkv-cs.pdf
\ekvc@any@long\edef#1##1%
{%
\expandafter\ekvc@ekvset@pre@expander\expandafter{\ekvc@set}%
- \unexpanded{\ekvc@hash@pack@argument}%
- \unexpanded\expandafter{\ekvc@initials\ekvc@stop#2}%
+ \ekv@unexpanded{\ekvc@hash@pack@argument}%
+ \ekv@unexpanded\expandafter{\ekvc@initials\ekvc@stop#2}%
}%
}%
{%
\ekvc@any@long\edef#1##1%
{%
\expandafter\ekvc@ekvset@pre@expander\expandafter{\ekvc@set}%
- \unexpanded{#2}%
- \unexpanded\expandafter{\ekvc@initials\ekvc@stop}%
+ \ekv@unexpanded{#2}%
+ \ekv@unexpanded\expandafter{\ekvc@initials\ekvc@stop}%
}%
}%
}
@@ -2102,9 +2126,9 @@ and the derived files expkv-cs.pdf
% \end{macro}
%
% \begin{macro}[internal]
-% {%
+% {
% \ekvc@SetupHashKeys,\ekvc@SetupHashKeys@a,\ekvc@SetupHashKeys@b,
-% \ekvc@SetupHashKeys@c,
+% \ekvc@SetupHashKeys@c,\ekvc@SetupHashKeys@d
% \ekvc@SetupHashKeys@check@unknown,\ekvc@SetupHashKeys@unknown
% }
% This should look awfully familiar as well, since it's just the same as for
@@ -2116,18 +2140,20 @@ and the derived files expkv-cs.pdf
\let\ekvc@initials\ekv@empty
\ekvparse\ekvc@SetupHashKeys@check@unknown\ekvc@SetupHashKeys@a{#1}%
}
-\protected\def\ekvc@SetupHashKeys@a#1%
+\protected\long\def\ekvc@SetupHashKeys@a#1%
+ {\expandafter\ekvc@SetupHashKeys@b\detokenize{#1}\ekvc@stop}
+\protected\def\ekvc@SetupHashKeys@b#1\ekvc@stop
{%
\let\ekvc@long\ekv@empty
\ekvc@ifspace{#1}%
- {\ekvc@SetupHashKeys@b#1\ekvc@stop}%
- {\ekvc@SetupHashKeys@c{#1}}%
+ {\ekvc@SetupHashKeys@c#1\ekvc@stop}%
+ {\ekvc@SetupHashKeys@d{#1}}%
}
-\protected\def\ekvc@SetupHashKeys@b#1 #2\ekvc@stop
+\protected\def\ekvc@SetupHashKeys@c#1 #2\ekvc@stop
{%
\ekv@ifdefined{ekvc@hash@p@#1}%
{\csname ekvc@hash@p@#1\endcsname{#2}}%
- {\ekvc@SetupHashKeys@c{#1 #2}}%
+ {\ekvc@SetupHashKeys@d{#1 #2}}%
}
% \end{macrocode}
% Yes, even the defining macro looks awfully familiar. Instead of numbered we
@@ -2136,31 +2162,33 @@ and the derived files expkv-cs.pdf
% short keys. And instead of the |\ekvc@setup@splitmacro| we use
% |\ekvc@setup@hashmacro|.
% \begin{macrocode}
-\protected\long\def\ekvc@SetupHashKeys@c#1#2%
+\protected\long\def\ekvc@SetupHashKeys@d#1#2%
{%
\begingroup
\edef\ekvc@tmp
{%
\endgroup
- \long\def\unexpanded{\ekvc@tmp}####1####2%
- \unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname}####3%
+ \long\def\ekv@unexpanded{\ekvc@tmp}####1####2%
+ \ekv@unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname}%
+ ####3%
{%
####2%
- \unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname}{####1}%
+ \ekv@unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname}%
+ {####1}%
}%
\unless\ifx\ekvc@long\long
- \let\unexpanded\expandafter
+ \let\ekv@unexpanded\expandafter
{\csname ekvc@\ekvc@set(#1)\endcsname\ekvc@tmp}%
- \def\unexpanded{\ekvc@tmp}####1%
+ \def\ekv@unexpanded{\ekvc@tmp}####1%
{%
- \unexpanded\expandafter{\csname ekvc@\ekvc@set(#1)\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@\ekvc@set(#1)\endcsname}%
{####1}%
}%
\fi
- \def\unexpanded{\ekvc@initials}%
+ \def\ekv@unexpanded{\ekvc@initials}%
{%
- \unexpanded\expandafter{\ekvc@initials}%
- \unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname{#2}}%
+ \ekv@unexpanded\expandafter{\ekvc@initials}%
+ \ekv@unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname{#2}}%
}%
}%
\ekvc@tmp
@@ -2192,7 +2220,7 @@ and the derived files expkv-cs.pdf
\let\csname\ekv@name\ekvc@set{}u\endcsname\ekvc@hash@unknown@kv
\expandafter
\let\csname\ekv@name\ekvc@set{}uN\endcsname\ekvc@hash@unknown@k
- \edef\ekvc@initials{\unexpanded\expandafter{\ekvc@initials#1{}}}%
+ \edef\ekvc@initials{\ekv@unexpanded\expandafter{\ekvc@initials#1{}}}%
\ekvc@setup@hashmacro{...}%
}%
\long\def\ekvc@hash@unknown@kv##1##2##3#1##4{##3#1{##4,##2= {##1} }}%
@@ -2210,7 +2238,7 @@ and the derived files expkv-cs.pdf
{%
\let\ekvc@long\long
\let\ekvc@any@long\long
- \ekvc@SetupHashKeys@c
+ \ekvc@SetupHashKeys@d
}
% \end{macrocode}
% \end{macro}
@@ -2219,7 +2247,7 @@ and the derived files expkv-cs.pdf
% The |short| prefix does essentially nothing, it is only provided to allow
% key names starting with |long| that aren't |\long|.
% \begin{macrocode}
-\def\ekvc@hash@p@short{\ekvc@SetupHashKeys@c}
+\def\ekvc@hash@p@short{\ekvc@SetupHashKeys@d}
% \end{macrocode}
% \end{macro}
%
@@ -2240,47 +2268,52 @@ and the derived files expkv-cs.pdf
\edef\ekvc@tmp
{%
\long\gdef
- \unexpanded\expandafter{\csname ekvc@fasthash@#1\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@fasthash@#1\endcsname}%
####1%
- \unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname}%
- ####2####3\unexpanded{\ekvc@stop}%
+ \ekv@unexpanded\expandafter
+ {\csname ekvc@hashmark@#1\endcsname}%
+ ####2####3\ekv@unexpanded{\ekvc@stop}%
{####2}%
\long\gdef
- \unexpanded\expandafter{\csname ekvc@safehash@#1\endcsname}%
+ \ekv@unexpanded\expandafter{\csname ekvc@safehash@#1\endcsname}%
####1%
{%
- \unexpanded\expandafter{\csname ekvc@@safehash@#1\endcsname}%
- ####1\unexpanded{\ekvc@mark}{}%
- \unexpanded\expandafter
+ \ekv@unexpanded\expandafter
+ {\csname ekvc@@safehash@#1\endcsname}%
+ ####1\ekv@unexpanded{\ekvc@mark}{}%
+ \ekv@unexpanded\expandafter
{%
\csname ekvc@hashmark@#1\endcsname{}%
\ekvc@mark{\ekvc@err@missing@hash{#1}}\ekvc@stop
}%
}%
\long\gdef
- \unexpanded\expandafter{\csname ekvc@@safehash@#1\endcsname}%
+ \ekv@unexpanded\expandafter
+ {\csname ekvc@@safehash@#1\endcsname}%
####1%
- \unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname}%
- ####2####3\unexpanded{\ekvc@mark}####4####5%
- \unexpanded{\ekvc@stop}%
+ \ekv@unexpanded\expandafter
+ {\csname ekvc@hashmark@#1\endcsname}%
+ ####2####3\ekv@unexpanded{\ekvc@mark}####4####5%
+ \ekv@unexpanded{\ekvc@stop}%
{%
####4{####2}%
}%
- \long\gdef\unexpanded\expandafter
+ \long\gdef\ekv@unexpanded\expandafter
{\csname ekvc@fastsplithash@#1\endcsname}%
####1%
- \unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname}%
- ####2####3\unexpanded{\ekvc@stop}####4%
+ \ekv@unexpanded\expandafter
+ {\csname ekvc@hashmark@#1\endcsname}%
+ ####2####3\ekv@unexpanded{\ekvc@stop}####4%
{%
####4{####2}%
}%
- \long\gdef\unexpanded\expandafter
+ \long\gdef\ekv@unexpanded\expandafter
{\csname ekvc@safesplithash@#1\endcsname}####1%
{%
- \unexpanded\expandafter
+ \ekv@unexpanded\expandafter
{\csname ekvc@@safesplithash@#1\endcsname}%
- ####1\unexpanded{\ekvc@mark\ekvc@safe@after@hash}%
- \unexpanded\expandafter
+ ####1\ekv@unexpanded{\ekvc@mark\ekvc@safe@after@hash}%
+ \ekv@unexpanded\expandafter
{%
\csname ekvc@hashmark@#1\endcsname{}%
\ekvc@mark
@@ -2288,12 +2321,13 @@ and the derived files expkv-cs.pdf
\ekvc@stop
}%
}%
- \long\gdef\unexpanded\expandafter
+ \long\gdef\ekv@unexpanded\expandafter
{\csname ekvc@@safesplithash@#1\endcsname}%
####1%
- \unexpanded\expandafter{\csname ekvc@hashmark@#1\endcsname}%
- ####2####3\unexpanded{\ekvc@mark}####4####5%
- \unexpanded{\ekvc@stop}%
+ \ekv@unexpanded\expandafter
+ {\csname ekvc@hashmark@#1\endcsname}%
+ ####2####3\ekv@unexpanded{\ekvc@mark}####4####5%
+ \ekv@unexpanded{\ekvc@stop}%
{%
####4{####2}%
}%
@@ -2306,32 +2340,42 @@ and the derived files expkv-cs.pdf
% \end{macro}
%
% \begin{macro}{\ekvcValue}
+% \begin{macro}[internal]{\ekvcValue@}
% All this does is a few consistency checks on the first argument (not empty,
% hash macro exists) and then call that hash-grabbing macro that will also
% test whether the hash is inside of |#2| or not.
% \begin{macrocode}
\long\def\ekvcValue#1%
{%
- \unexpanded
+ \ekv@unexpanded
+ \expandafter\ekvcValue@\detokenize{#1}\ekvc@stop
+ }
+\def\ekvcValue@#1\ekvc@stop
+ {%
\ekv@ifdefined{ekvc@safehash@#1}%
{\csname ekvc@safehash@#1\endcsname}%
{\ekvc@err@unknown@hash{#1}\@firstoftwo{{}}}%
}
% \end{macrocode}
% \end{macro}
+% \end{macro}
%
% \begin{macro}{\ekvcValueFast}
% To be as fast as possible, this doesn't test for anything, assuming the user
% knows best.
% \begin{macrocode}
-\long\def\ekvcValueFast#1#2{\csname ekvc@fasthash@#1\endcsname#2\ekvc@stop}
+\long\def\ekvcValueFast#1#2%
+ {\csname ekvc@fasthash@\detokenize{#1}\endcsname#2\ekvc@stop}
% \end{macrocode}
% \end{macro}
%
-% \begin{macro}{\ekvcValueSplit,\ekvcValueSplit@recover}
+% \begin{macro}{\ekvcValueSplit}
+% \begin{macro}[internal]{\ekvcValueSplit@,\ekvcValueSplit@recover}
% This splits off a single value.
% \begin{macrocode}
\long\def\ekvcValueSplit#1%
+ {\expandafter\ekvcValueSplit@\detokenize{#1}\ekvc@stop}
+\def\ekvcValueSplit@#1\ekvc@stop
{%
\ekv@ifdefined{ekvc@safesplithash@#1}%
{\csname ekvc@safesplithash@#1\endcsname}%
@@ -2340,6 +2384,7 @@ and the derived files expkv-cs.pdf
\long\def\ekvcValueSplit@recover#1#2{#2{}}
% \end{macrocode}
% \end{macro}
+% \end{macro}
%
% \begin{macro}[internal]{\ekvc@safe@after@hash}
% \begin{macrocode}
@@ -2357,7 +2402,7 @@ and the derived files expkv-cs.pdf
% sequence.
% \begin{macrocode}
\long\def\ekvcValueSplitFast#1#2%
- {\csname ekvc@fastsplithash@#1\endcsname#2\ekvc@stop}
+ {\csname ekvc@fastsplithash@\detokenize{#1}\endcsname#2\ekvc@stop}
% \end{macrocode}
% \end{macro}
%
@@ -2380,6 +2425,8 @@ and the derived files expkv-cs.pdf
% \end{macro}
%
% \begin{macro}{\ekvcSecondaryKeys}
+% \begin{macro}[internal]
+% {\ekvcSecondaryKeys@a,\ekvcSecondaryKeys@b,\ekvcSecondaryKeys@c}
% The secondary keys are defined pretty similar to the way the originals are,
% but here we also introduce some key types (those have a |@t@| in their name)
% additionally to the prefixes.
@@ -2389,14 +2436,16 @@ and the derived files expkv-cs.pdf
\edef\ekvc@set{\string#1}%
\ekvparse\ekvc@err@value@required\ekvcSecondaryKeys@a{#2}%
}
-\protected\def\ekvcSecondaryKeys@a#1%
+\protected\long\def\ekvcSecondaryKeys@a#1%
+ {\expandafter\ekvcSecondaryKeys@b\detokenize{#1}\ekvc@stop}
+\protected\def\ekvcSecondaryKeys@b#1\ekvc@stop
{%
\let\ekvc@long\ekv@empty
\ekvc@ifspace{#1}%
- {\ekvcSecondaryKeys@b#1\ekvc@stop}%
+ {\ekvcSecondaryKeys@c#1\ekvc@stop}%
{\ekvc@err@missing@type{#1}\@gobble}%
}
-\protected\def\ekvcSecondaryKeys@b#1 #2\ekvc@stop
+\protected\def\ekvcSecondaryKeys@c#1 #2\ekvc@stop
{%
\ekv@ifdefined{ekvc@p@#1}%
{\csname ekvc@p@#1\endcsname}%
@@ -2409,6 +2458,7 @@ and the derived files expkv-cs.pdf
}
% \end{macrocode}
% \end{macro}
+% \end{macro}
%
% \begin{macro}{\ekvcChange}
% This can be used to change the defaults of an \expkvc\ defined macro. It
@@ -2616,7 +2666,7 @@ and the derived files expkv-cs.pdf
\ekvc@assert@not@long{default #1}%
\edef\ekvc@tmp
{%
- \unexpanded\expandafter
+ \ekv@unexpanded\expandafter
{\csname\ekv@name\ekvc@set{#1}\endcsname{#2}}%
}%
\ekvletNoVal\ekvc@set{#1}\ekvc@tmp
@@ -2882,8 +2932,8 @@ and the derived files expkv-cs.pdf
{%
\edef#1%
{%
- \unexpanded{\ekvc@flag@height}%
- \unexpanded\expandafter{\csname\ekvc@flag@name#1\endcsname}%
+ \ekv@unexpanded{\ekvc@flag@height}%
+ \ekv@unexpanded\expandafter{\csname\ekvc@flag@name#1\endcsname}%
}%
\ekv@expargtwice
{\expandafter\def\csname\ekvc@flag@name#1\endcsname##1}%
@@ -3100,7 +3150,7 @@ and the derived files expkv-cs.pdf
% \begin{macrocode}
\def\ekvc@extract@mark#1{\expandafter\ekvc@extract@mark@\meaning#1\ekv@stop}
\begingroup
-\lccode`;=`\#
+\lccode`;=`#
\lccode`/=`\\
\lowercase{\endgroup
\def\ekvc@extract@mark@#1:#2/#3 ;#4\ekv@stop{#3}%
@@ -3142,41 +3192,41 @@ and the derived files expkv-cs.pdf
{%
\errmessage{expkv-cs Error: Too many keys for macro `\string#1'}%
}
-\protected\def\ekvc@err@value@required#1%
+\protected\long\def\ekvc@err@value@required#1%
{%
- \errmessage{expkv-cs Error: Missing value for key `\unexpanded{#1}'}%
+ \errmessage{expkv-cs Error: Missing value for key `\ekv@unexpanded{#1}'}%
}
-\protected\def\ekvc@err@missing@type#1%
+\protected\long\def\ekvc@err@missing@type#1%
{%
\errmessage
- {expkv-cs Error: Missing type for secondary key `\unexpanded{#1}'}%
+ {expkv-cs Error: Missing type for secondary key `\ekv@unexpanded{#1}'}%
}
-\protected\def\ekvc@err@no@long#1%
+\protected\long\def\ekvc@err@no@long#1%
{%
\errmessage
- {expkv-cs Error: prefix `long' not accepted for `\unexpanded{#1}'}%
+ {expkv-cs Error: prefix `long' not accepted for `\ekv@unexpanded{#1}'}%
}
-\protected\def\ekvc@err@already@defined#1%
+\protected\long\def\ekvc@err@already@defined#1%
{%
\errmessage{expkv-cs Error: Macro `\string#1' already defined}%
}
-\protected\def\ekvc@err@unknown@keytype#1%
+\protected\long\def\ekvc@err@unknown@keytype#1%
{%
- \errmessage{expkv-cs Error: Unknown key type `\unexpanded{#1}'}%
+ \errmessage{expkv-cs Error: Unknown key type `\ekv@unexpanded{#1}'}%
}
-\protected\def\ekvc@err@unknown@key#1%
+\protected\long\def\ekvc@err@unknown@key#1%
{%
\errmessage
- {expkv-cs Error: Unknown key `\unexpanded{#1}' for macro `\ekvc@set'}%
+ {expkv-cs Error: Unknown key `\ekv@unexpanded{#1}' for macro `\ekvc@set'}%
}
-\long\def\ekvc@err@no@key@macro#1%
+\protected\long\def\ekvc@err@no@key@macro#1%
{\errmessage{expkv-cs Error: \string#1 is no key=val macro}}
\protected\long\def\ekvc@err@not@two#1%
{%
\errmessage
{%
- expkv-cs Error: Definition of `\unexpanded{#1}' doesn't contain exactly
- two arguments%
+ expkv-cs Error: Definition of `\ekv@unexpanded{#1}' doesn't contain
+ exactly two arguments%
}%
}
% \end{macrocode}
diff --git a/macros/generic/expkv-cs/expkv-cs.pdf b/macros/generic/expkv-cs/expkv-cs.pdf
index 2d341f0e6a..130e142fb3 100644
--- a/macros/generic/expkv-cs/expkv-cs.pdf
+++ b/macros/generic/expkv-cs/expkv-cs.pdf
Binary files differ
diff --git a/macros/generic/expkv-def/README.md b/macros/generic/expkv-def/README.md
index da6fb32a6c..3b4417a2cb 100644
--- a/macros/generic/expkv-def/README.md
+++ b/macros/generic/expkv-def/README.md
@@ -1,7 +1,7 @@
-------------------------------------------------------------------------------
# expkv-def -- a key-defining frontend for expkv
-Version 2021-09-18 v0.8b
+Version 2021-09-20 v0.8c
Released under the LaTeX Project Public License v1.3c or later
See http://www.latex-project.org/lppl.txt
diff --git a/macros/generic/expkv-def/expkv-def.dtx b/macros/generic/expkv-def/expkv-def.dtx
index 8aa537402e..c9d6ebe5b7 100644
--- a/macros/generic/expkv-def/expkv-def.dtx
+++ b/macros/generic/expkv-def/expkv-def.dtx
@@ -43,6 +43,7 @@ This work consists of the file expkv-def.dtx
and the derived files expkv-def.pdf
expkv-def.sty
expkv-def.tex
+ t-expkv-def.tex
\endpreamble
% stop docstrip adding \endinput
@@ -50,6 +51,7 @@ and the derived files expkv-def.pdf
\endpostamble
\generate{\file{expkv-def.sty}{\from{expkv-def.dtx}{pkg}}}
\generate{\file{expkv-def.tex}{\from{expkv-def.dtx}{tex}}}
+\generate{\file{t-expkv-def.tex}{\from{expkv-def.dtx}{ctx}}}
\ifx\fmtname\nameofplainTeX
\expandafter\endbatchfile
\else
@@ -317,8 +319,8 @@ and the derived files expkv-def.pdf
% \expkvd\ provides a small \kv\ interface to define keys for \expkv. Key-types
% are declared using prefixes, similar to static typed languages. The stylised
% name is \expkvd\ but the files use \file{expkv-def}, this is due to CTAN-rules
-% which don't allow \string| in package names since that is the pipe symbol in *nix
-% shells.
+% which don't allow \string| in package names since that is the pipe symbol in
+% *nix shells.
% \end{abstract}
%
% \tableofcontents
@@ -345,12 +347,13 @@ and the derived files expkv-def.pdf
% \autoref{sec:prefix:p}, the |t|-prefixes are described in
% \autoref{sec:prefix:t}.
%
-% \expkvd\ is usable as generic code and as a \LaTeX\ package. It'll
-% automatically load \expkv\ in the same mode as well. To use it, just use one
-% of
+% \expkvd\ is usable as generic code, as a \LaTeX\ package, and as a \ConTeXt\
+% module. It'll automatically load \expkv\ in the same mode as well. To use it,
+% just use one of
% \begin{lstlisting}
-% \usepackage{expkv-def} % LaTeX
% \input expkv-def % plainTeX
+% \usepackage{expkv-def} % LaTeX
+% \usemodule[expkv-def] % ConTeXt
% \end{lstlisting}
%
% \subsection{Macros}
@@ -480,8 +483,9 @@ and the derived files expkv-def.pdf
% expand the \key's code once, so will be slightly quicker, but not change if
% you redefine \key. |odefault| is just another name for |qdefault|. The
% |fdefault| version will expand the key code until a non-expandable token or
-% a space is found, a space would be gobbled.\footnotemark{} The |edefault| on the other hand fully expands
-% the \key-code with \meta{definition} as its argument inside of an |\edef|.
+% a space is found, a space would be gobbled.\footnotemark{} The |edefault| on
+% the other hand fully expands the \key-code with \meta{definition} as its
+% argument inside of an |\edef|.
% \end{function}
% \footnotetext
% {^^A
@@ -911,6 +915,22 @@ and the derived files expkv-def.pdf
% \gobbledocstriptag
%</pkg>
%^^A=<<
+%^^A the ConTeXt module >>=
+% \subsection{The \ConTeXt\ module}
+% \gobbledocstriptag
+%<*ctx>
+% \begin{macrocode}
+\writestatus{loading}{ConTeXt User Module / expkv-def}
+\usemodule[expkv]
+\unprotect
+\input expkv-def.tex
+\writestatus{loading}
+ {ConTeXt User Module / expkv-def / Version \ekvdVersion\space loaded}
+\protect\endinput
+% \end{macrocode}
+% \gobbledocstriptag
+%</ctx>
+%^^A=<<
%^^A main file >>=
% \subsection{The Generic Code}
% The rest of this implementation will be the generic code.
@@ -936,8 +956,8 @@ and the derived files expkv-def.pdf
% \begin{macro}{\ekvdVersion,\ekvdDate}
% We're on our first input, so lets store the version and date in a macro.
% \begin{macrocode}
-\def\ekvdVersion{0.8b}
-\def\ekvdDate{2021-09-18}
+\def\ekvdVersion{0.8c}
+\def\ekvdDate{2021-09-20}
% \end{macrocode}
% \end{macro}
%
@@ -998,17 +1018,17 @@ and the derived files expkv-def.pdf
% if so calls the prefix grabbing routine, else we throw an error and ignore the
% key.
% \begin{macrocode}
-\protected\def\ekvd@noarg#1%
+\protected\long\def\ekvd@noarg#1%
{%
\let\ekvd@ifnoarg\@firstoftwo
- \ekvd@handle{#1}{}%
+ \expandafter\ekvd@handle\detokenize{#1}\ekvd@stop{}%
}
-\protected\def\ekvd@arg
+\protected\long\def\ekvd@arg#1%
{%
\let\ekvd@ifnoarg\@secondoftwo
- \ekvd@handle
+ \expandafter\ekvd@handle\detokenize{#1}\ekvd@stop
}
-\protected\long\def\ekvd@handle#1#2%
+\protected\long\def\ekvd@handle#1\ekvd@stop#2%
{%
\ekvd@clear@prefixes
\edef\ekvd@cur{\detokenize{#1}}%
@@ -1172,7 +1192,7 @@ and the derived files expkv-def.pdf
\ekvd@assert@not@long
\ekvd@prot\edef\ekvd@tmp
{%
- \unexpanded\expandafter#1%
+ \ekv@unexpanded\expandafter#1%
{#2\csname\ekv@name\ekvd@set{#3}\endcsname{#4}}%
}%
\ekvd@ifalso
@@ -1358,14 +1378,16 @@ and the derived files expkv-def.pdf
}%
}
\protected\def\ekvd@t@data
- {\ekvd@type@data\@secondoftwo\edef{####2}{\unexpanded{##1}}}
+ {\ekvd@type@data\@secondoftwo\edef{####2}{\ekv@unexpanded{##1}}}
\protected\def\ekvd@t@edata{\ekvd@type@data\@secondoftwo\edef{####2}{##1}}
\protected\def\ekvd@t@gdata
- {\ekvd@type@data\@secondoftwo\xdef{####2}{\unexpanded{##1}}}
+ {\ekvd@type@data\@secondoftwo\xdef{####2}{\ekv@unexpanded{##1}}}
\protected\def\ekvd@t@xdata{\ekvd@type@data\@secondoftwo\xdef{####2}{##1}}
-\protected\def\ekvd@t@dataT{\ekvd@type@data\@gobble\edef{}{\unexpanded{##1}}}
+\protected\def\ekvd@t@dataT
+ {\ekvd@type@data\@gobble\edef{}{\ekv@unexpanded{##1}}}
\protected\def\ekvd@t@edataT{\ekvd@type@data\@gobble\edef{}{##1}}
-\protected\def\ekvd@t@gdataT{\ekvd@type@data\@gobble\xdef{}{\unexpanded{##1}}}
+\protected\def\ekvd@t@gdataT
+ {\ekvd@type@data\@gobble\xdef{}{\ekv@unexpanded{##1}}}
\protected\def\ekvd@t@xdataT{\ekvd@type@data\@gobble\xdef{}{##1}}
% \end{macrocode}
% \end{macro}
@@ -1513,8 +1535,8 @@ and the derived files expkv-def.pdf
}%
}%
}
-\protected\def\ekvd@t@store{\ekvd@type@store\edef{\unexpanded{##1}}}
-\protected\def\ekvd@t@gstore{\ekvd@type@store\xdef{\unexpanded{##1}}}
+\protected\def\ekvd@t@store{\ekvd@type@store\edef{\ekv@unexpanded{##1}}}
+\protected\def\ekvd@t@gstore{\ekvd@type@store\xdef{\ekv@unexpanded{##1}}}
\protected\def\ekvd@t@estore{\ekvd@type@store\edef{##1}}
\protected\def\ekvd@t@xstore{\ekvd@type@store\xdef{##1}}
% \end{macrocode}
@@ -1608,7 +1630,7 @@ and the derived files expkv-def.pdf
{%
\ekvd@assert@not@long
\ekvd@prot\edef\ekvd@tmp##1%
- {\unexpanded{\ekvd@h@choice}{\ekvd@choice@name\ekvd@set{#1}{##1}}}%
+ {\ekv@unexpanded{\ekvd@h@choice}{\ekvd@choice@name\ekvd@set{#1}{##1}}}%
\ekvd@ifalso
{%
\ekvd@assert@val{#1}%
@@ -2159,18 +2181,21 @@ and the derived files expkv-def.pdf
\protected\def\ekvd@err@missing@definition
{\ekvd@errm{Missing definition for key `\ekvd@cur'}}
\protected\def\ekvd@err@missing@definition@msg#1%
- {\ekvd@errm{Missing definition for key `\unexpanded{#1}'}}
+ {\ekvd@errm{Missing definition for key `\ekv@unexpanded{#1}'}}
\protected\def\ekvd@err@missing@type
{\ekvd@errm{Missing type prefix for key `\ekvd@cur'}}
\protected\def\ekvd@err@undefined@prefix#1%
{%
\ekvd@errm
- {Undefined prefix `\unexpanded{#1}' found while processing `\ekvd@cur'}%
+ {%
+ Undefined prefix `\ekv@unexpanded{#1}' found while processing
+ `\ekvd@cur'%
+ }%
}
\protected\def\ekvd@err@undefined@key#1%
{%
\ekvd@errm
- {Undefined key `\unexpanded{#1}' found while processing `\ekvd@cur'}%
+ {Undefined key `\ekv@unexpanded{#1}' found while processing `\ekvd@cur'}%
}
\protected\def\ekvd@err@undefined@noval#1%
{%
@@ -2183,7 +2208,7 @@ and the derived files expkv-def.pdf
\protected\def\ekvd@err@no@prefix#1%
{\ekvd@errm{prefix `#1' not accepted in `\ekvd@cur'}}
\protected\def\ekvd@err@no@prefix@msg#1#2%
- {\ekvd@errm{prefix `#2' not accepted in `\unexpanded{#1}'}}
+ {\ekvd@errm{prefix `#2' not accepted in `\ekv@unexpanded{#1}'}}
\protected\def\ekvd@err@no@prefix@also#1%
{\ekvd@errm{`\ekvd@cur' not allowed with a `#1' key}}
\protected\def\ekvd@err@add@val@on@noval
diff --git a/macros/generic/expkv-def/expkv-def.pdf b/macros/generic/expkv-def/expkv-def.pdf
index 59c3673f09..800ae3dae3 100644
--- a/macros/generic/expkv-def/expkv-def.pdf
+++ b/macros/generic/expkv-def/expkv-def.pdf
Binary files differ
diff --git a/macros/generic/expkv/README.md b/macros/generic/expkv/README.md
index 8c241c4217..ccfa3194b3 100644
--- a/macros/generic/expkv/README.md
+++ b/macros/generic/expkv/README.md
@@ -1,7 +1,7 @@
-------------------------------------------------------------------------------
# expkv -- an expandable key=val implementation
-Version 2021-07-15 v1.9
+Version 2021-09-20 v1.9a
Released under the LaTeX Project Public License v1.3c or later
See http://www.latex-project.org/lppl.txt
diff --git a/macros/generic/expkv/expkv.dtx b/macros/generic/expkv/expkv.dtx
index da6fcf2542..8651ad97a3 100644
--- a/macros/generic/expkv/expkv.dtx
+++ b/macros/generic/expkv/expkv.dtx
@@ -43,6 +43,7 @@ This work consists of the file expkv.dtx
and the derived files expkv.pdf
expkv.sty
expkv.tex
+ t-expkv.tex
\endpreamble
% stop docstrip adding \endinput
@@ -50,6 +51,7 @@ and the derived files expkv.pdf
\endpostamble
\generate{\file{expkv.sty}{\from{expkv.dtx}{pkg}}}
\generate{\file{expkv.tex}{\from{expkv.dtx}{tex}}}
+\generate{\file{t-expkv.tex}{\from{expkv.dtx}{ctx}}}
\ifx\fmtname\nameofplainTeX
\expandafter\endbatchfile
\else
@@ -98,7 +100,7 @@ and the derived files expkv.pdf
numexpr,
protected,
}
- ,moretexcs=[5]^^A plain/LaTeX
+ ,moretexcs=[5]^^A plain/LaTeX/ConTeXt
{
approx,
begin,
@@ -112,6 +114,7 @@ and the derived files expkv.pdf
TeX,
textit,texttt,
usepackage,
+ usemodule,
}
,moretexcs=[6]^^A used packages
{
@@ -299,16 +302,25 @@ and the derived files expkv.pdf
\newcommand\val{\meta{value}}
\newcommand\set{\meta{set}}
\hypersetup{linkcolor=red!80!black,urlcolor=purple!80!black}
+\DoNotIndex{\directlua}
\DoNotIndex{\def,\edef,\,,\=,\begingroup,\catcode,\chardef,\csname,\endcsname}
\DoNotIndex{\endgroup,\endinput,\errmessage,\expandafter,\input,\let,\long}
\DoNotIndex{\protected,\ProvidesFile,\ProvidesPackage,\relax,\space}
-\DoNotIndex{\@,\unexpanded,\string,\expanded,\detokenize,\meaning,\lastnamedcs}
-\DoNotIndex{\romannumeral,\z@}
+\DoNotIndex{\^,\@,\unexpanded,\string,\expanded,\detokenize,\meaning}
+\DoNotIndex{\lastnamedcs,\romannumeral,\z@}
+\DoNotIndex{\noexpand,\normalexpanded,\normalunexpanded}
+\DoNotIndex{\par,\protect}
+\DoNotIndex{\unprotect}
+\DoNotIndex{\writestatus}
\DoNotIndex{\ifcsname}
\DoNotIndex{\ifx}
+\DoNotIndex{\iffalse}
+\DoNotIndex{\ifnum}
\DoNotIndex{\else}
\DoNotIndex{\fi}
-\@gobble\fi ^^A ignoring \ifx and \ifcsname, but only one \fi
+\@gobble\fi ^^A ignoring \ifx etc., but only one \fi
+\@gobble\fi ^^A ignoring \ifx etc., but only one \fi
+\@gobble\fi ^^A ignoring \ifx etc., but only one \fi
\@ifdefinable\gobbledocstriptag{\def\gobbledocstriptag#1>{}}
\newcommand*\pmso[1] ^^A poor man's strike out
{^^A
@@ -365,15 +377,19 @@ and the derived files expkv.pdf
% example \pkg{babel} turns characters active for some languages, for instance
% the equal sign is turned active for Turkish.
%
-% \expkv\ is usable as generic code or as a \LaTeX\ package. To use it, just
-% use one of:
+% \expkv\ is usable as generic code, as a \LaTeX\ package or as a \ConTeXt\
+% module. To use it, just use one of:
% \begin{lstlisting}
-% \usepackage{expkv} % LaTeX
% \input expkv % plainTeX
+% \usepackage{expkv} % LaTeX
+% \usemodule[expkv] % ConTeXt
% \end{lstlisting}
-% The \LaTeX\ package doesn't do more than \file{expkv.tex}, except calling
-% |\ProvidesPackage| and setting things up such that \file{expkv.tex} will use
-% |\ProvidesFile|.
+% Both the \LaTeX\ package and the \ConTeXt\ module don't do more than
+% \file{expkv.tex}, except calling |\ProvidesPackage| and setting things up such
+% that \file{expkv.tex} will use |\ProvidesFile|, or printing some status
+% information. The \ConTeXt\ support is not thoroughly tested, though (since I
+% don't use \ConTeXt\ myself I don't know if there are additional pitfalls I
+% wasn't aware of).
%
% In the \expkv\ family are other packages contained which provide additional
% functionality. Those packages currently are:
@@ -410,7 +426,7 @@ and the derived files expkv.pdf
% prefixes allowed for |\let| can prefix those with ``|let|'' in their name,
% accordingly. Neither \set\ nor \key\ are allowed to be empty for new keys.
% \set\ will be used as is inside of |\csname ...\endcsname| and \key\ will get
-% |\detokenize|d.
+% |\detokenize|d. Also \set\ should not contain an explicit |\par| token.
%
% \begin{function}{\ekvdef}
% \begin{syntax}
@@ -919,6 +935,11 @@ and the derived files expkv.pdf
% \end{verbatim}
% Any clean up has to be done by you, |\ekverr| will expand to nothing after
% throwing the error message.
+%
+% In \ConTeXt\ this macro works differently. While still being fully expandable,
+% it doesn't have the character count limitation and doesn't impose restrictions
+% on \meta{package}. It will not display the additional text and adding line
+% breaks is not possible.
% \example Say we set up a small calculation which works with user input. In our
% calculation we need a division, so have to watch out for division by zero. If
% we detect such a case we throw an error and do the recovery by using the
@@ -926,7 +947,7 @@ and the derived files expkv.pdf
% \begin{lstlisting}
% \newcommand*\mydivision[2]
% {%
-% \number\numexpr
+% \the\numexpr
% \ifnum\numexpr#2=0 % space here on purpose
% \ekverr{my}{division by 0. Setting result to 2147483647.}%
% 2147483647%
@@ -1562,6 +1583,27 @@ and the derived files expkv.pdf
% \gobbledocstriptag
%</pkg>
%^^A=<<
+%^^A the ConTeXt module >>=
+% \subsection{The \ConTeXt\ module}
+% This is pretty straight forward, we just have to change the error throwing
+% mechanism for \ConTeXt\ (the approach taken for plain and \LaTeX\ breaks in
+% \ConTeXt, effectively breaking \ConTeXt, dropping you in an interactive \TeX\
+% session with almost no means of escape).
+% \gobbledocstriptag
+%<*ctx>
+% \begin{macrocode}
+\writestatus{loading}{ConTeXt User Module / expkv}
+\unprotect
+\input expkv.tex
+\long\def\ekv@err@collect#1\par#2%
+ {\directlua{tex.error[[\detokenize{#2} Error: #1]]}}
+\writestatus{loading}
+ {ConTeXt User Module / expkv / Version \ekvVersion\space loaded}
+\protect\endinput
+% \end{macrocode}
+% \gobbledocstriptag
+%</ctx>
+%^^A=<<
%^^A main file >>=
% \subsection{The Generic Code}
% The rest of this implementation will be the generic code.
@@ -1584,17 +1626,30 @@ and the derived files expkv.pdf
\expandafter
\endgroup
\ifx\ekvtmpa\ekvtmpb
+ \expandafter\let\csname ekv@expanded\endcsname\expanded
+ \expandafter\let\csname ekv@unexpanded\endcsname\unexpanded
\else
- \errmessage{expkv Error: e-TeX and \noexpand\expanded required}
- \expandafter\endinput
+ \begingroup
+ \edef\ekvtmpa{\string\expanded}
+ \edef\ekvtmpb{\meaning\normalexpanded}
+ \expandafter
+ \endgroup
+ \ifx\ekvtmpa\ekvtmpb
+ \expandafter\let\csname ekv@expanded\endcsname\normalexpanded
+ \expandafter\let\csname ekv@unexpanded\endcsname\normalunexpanded
+ \else
+ \errmessage
+ {expkv Error: e-TeX and the \noexpand\expanded primitive required}%
+ \expandafter\endinput
+ \fi
\fi
% \end{macrocode}
%
% \begin{macro}{\ekvVersion,\ekvDate}
% We're on our first input, so lets store the version and date in a macro.
% \begin{macrocode}
-\def\ekvVersion{1.9}
-\def\ekvDate{2021-07-15}
+\def\ekvVersion{1.9a}
+\def\ekvDate{2021-09-20}
% \end{macrocode}
% \end{macro}
%
@@ -1731,7 +1786,7 @@ and the derived files expkv.pdf
% \begin{macrocode}
\ekv@if@lastnamedcs
{%
- \def\ekv@ifdefined#1{\ifcsname#1\endcsname\ekv@ifdef@\fi\@secondoftwo}
+ \long\def\ekv@ifdefined#1{\ifcsname#1\endcsname\ekv@ifdef@\fi\@secondoftwo}
\def\ekv@ifdef@\fi\@secondoftwo
{%
\fi
@@ -1742,7 +1797,7 @@ and the derived files expkv.pdf
}
}
{%
- \def\ekv@ifdefined#1%
+ \long\def\ekv@ifdefined#1%
{%
\ifcsname#1\endcsname\ekv@ifdef@\fi\ekv@ifdef@false#1\endcsname\relax
\ekv@fi@secondoftwo
@@ -1853,7 +1908,7 @@ and the derived files expkv.pdf
}%
}
\expandafter\ekv@csv@loop\expandafter{\ekv@strip{#2}}
-\long\def\ekv@csv@loop@do#1#2{\unexpanded{#2{#1}}\ekv@csv@loop{#2}\ekv@mark}
+\long\def\ekv@csv@loop@do#1#2{\ekv@unexpanded{#2{#1}}\ekv@csv@loop{#2}\ekv@mark}
\def\ekv@csv@loop@end#1%
{%
\long\def\ekv@csv@loop@end
@@ -1875,13 +1930,13 @@ and the derived files expkv.pdf
% The keys will all follow the same naming scheme, so we define it here.
% \begin{macrocode}
\def\ekv@name@set#1{ekv#1(}
-\def\ekv@name@key#1{#1)}
+\long\def\ekv@name@key#1{#1)}
\edef\ekv@name
{%
- \unexpanded\expandafter{\ekv@name@set{#1}}%
- \unexpanded\expandafter{\ekv@name@key{\detokenize{#2}}}%
+ \ekv@unexpanded\expandafter{\ekv@name@set{#1}}%
+ \ekv@unexpanded\expandafter{\ekv@name@key{\detokenize{#2}}}%
}
-\ekv@exparg{\def\ekv@name#1#2}{\ekv@name}
+\long\ekv@exparg{\def\ekv@name#1#2}{\ekv@name}
% \end{macrocode}
% \end{macro}
%
@@ -1908,15 +1963,15 @@ and the derived files expkv.pdf
% \begin{macrocode}
\edef\ekv@checkvalid
{%
- \unexpanded\expandafter{\ekv@ifempty{#1}}%
- \unexpanded
+ \ekv@unexpanded\expandafter{\ekv@ifempty{#1}}%
+ \ekv@unexpanded
{{%
\def\ekv@tmp{}%
\errmessage{expkv Error: empty set name not allowed}%
}}%
{%
- \unexpanded\expandafter{\ekv@ifempty{#2}}%
- \unexpanded
+ \ekv@unexpanded\expandafter{\ekv@ifempty{#2}}%
+ \ekv@unexpanded
{%
{%
\def\ekv@tmp{}%
@@ -1925,7 +1980,7 @@ and the derived files expkv.pdf
\@secondoftwo
}%
}%
- \unexpanded{\@gobble}%
+ \ekv@unexpanded{\@gobble}%
}
\ekv@exparg{\protected\def\ekv@checkvalid#1#2}{\ekv@checkvalid}%
% \end{macrocode}
@@ -1934,9 +1989,9 @@ and the derived files expkv.pdf
% \begin{macro}{\ekvifdefined,\ekvifdefinedNoVal}
% And provide user-level macros to test whether a key is defined.
% \begin{macrocode}
-\ekv@expargtwice{\def\ekvifdefined#1#2}%
+\ekv@expargtwice{\long\def\ekvifdefined#1#2}%
{\expandafter\ekv@ifdefined\expandafter{\ekv@name{#1}{#2}}}
-\ekv@expargtwice{\def\ekvifdefinedNoVal#1#2}%
+\ekv@expargtwice{\long\def\ekvifdefinedNoVal#1#2}%
{\expandafter\ekv@ifdefined\expandafter{\ekv@name{#1}{#2}N}}
% \end{macrocode}
% \end{macro}
@@ -1955,9 +2010,9 @@ and the derived files expkv.pdf
{#1{\expandafter\def\csname#2\endcsname####1{##3}#3}}%
\protected\long\def\ekvdefNoVal##1##2##3%
{#1{\expandafter\def\csname#2N\endcsname{##3}#3}}%
- \protected\def\ekvlet##1##2##3%
+ \protected\long\def\ekvlet##1##2##3%
{#1{\expandafter\let\csname#2\endcsname##3#3}}%
- \protected\def\ekvletNoVal##1##2##3%
+ \protected\long\def\ekvletNoVal##1##2##3%
{#1{\expandafter\let\csname#2N\endcsname##3#3}}%
\ekv@expargtwice{\protected\long\def\ekvdefunknown##1##2}%
{%
@@ -1981,7 +2036,7 @@ and the derived files expkv.pdf
}%
{\ekv@zero\ekv@checkvalid{##1}.}%
}%
- \protected\def\ekvletkv##1##2##3##4%
+ \protected\long\def\ekvletkv##1##2##3##4%
{%
#1%
{%
@@ -1990,7 +2045,7 @@ and the derived files expkv.pdf
#3%
}%
}%
- \protected\def\ekvletkvNoVal##1##2##3##4%
+ \protected\long\def\ekvletkvNoVal##1##2##3##4%
{%
#1%
{%
@@ -2002,13 +2057,13 @@ and the derived files expkv.pdf
}
\edef\ekvdefNoVal
{%
- {\unexpanded\expandafter{\ekv@checkvalid{#1}{#2}}}%
- {\unexpanded\expandafter{\ekv@name{#1}{#2}}}%
+ {\ekv@unexpanded\expandafter{\ekv@checkvalid{#1}{#2}}}%
+ {\ekv@unexpanded\expandafter{\ekv@name{#1}{#2}}}%
{%
- \unexpanded{\expandafter\ekv@defsetmacro\csname}%
- \unexpanded\expandafter{\ekv@undefined@set{#1}\endcsname{#1}}%
+ \ekv@unexpanded{\expandafter\ekv@defsetmacro\csname}%
+ \ekv@unexpanded\expandafter{\ekv@undefined@set{#1}\endcsname{#1}}%
}%
- {\unexpanded\expandafter{\ekv@name{#3}{#4}}}%
+ {\ekv@unexpanded\expandafter{\ekv@name{#3}{#4}}}%
}
\expandafter\ekvdef\ekvdefNoVal
% \end{macrocode}
@@ -2047,7 +2102,7 @@ and the derived files expkv.pdf
\edef\ekv@tmp
{%
\ekvcsvloop#1{#6}%
- \unexpanded{#2}%
+ \ekv@unexpanded{#2}%
{\ekvcsvloop{}{#5,#6}}%
}%
\ekv@expargtwice
@@ -2092,7 +2147,7 @@ and the derived files expkv.pdf
{%
\ifcsname#2\endcsname\ekv@redirect@kv@a\fi\@gobble{##1}%
##4{##1}{##2}%
- }
+ }%
\def\ekv@redirect@kv@a\fi\@gobble
{\fi\expandafter\ekv@redirect@kv@b\lastnamedcs}%
}
@@ -2111,17 +2166,17 @@ and the derived files expkv.pdf
\ifcsname#2\endcsname\ekv@redirect@kv@a\fi\ekv@redirect@kv@a@
#2\endcsname{##1}%
##4{##1}{##2}%
- }
+ }%
\long\def\ekv@redirect@kv@a@#4\endcsname##3{}%
\def\ekv@redirect@kv@a\fi\ekv@redirect@kv@a@
{\fi\expandafter\ekv@redirect@kv@b\csname}%
- }
+ }%
}
% \end{macrocode}
% The key name given to this loop will already be |\detokenize|d by |\ekvset|,
% so we can safely remove the |\detokenize| here for some performance gain.
% \begin{macrocode}
-\def\ekv@redirect@kv#1\detokenize#2#3\ekv@stop{\unexpanded{#1#2#3}}
+\def\ekv@redirect@kv#1\detokenize#2#3\ekv@stop{\ekv@unexpanded{#1#2#3}}
\edef\ekv@redirect@kv
{%
{\expandafter\ekv@redirect@kv\ekv@name{#2}{#1}N\ekv@stop}%
@@ -2158,12 +2213,12 @@ and the derived files expkv.pdf
% \begin{macrocode}
\edef\ekv@defsetmacro
{%
- \unexpanded{\ifx#1\relax\edef#1##1}%
+ \ekv@unexpanded{\ifx#1\relax\edef#1##1}%
{%
- \unexpanded\expandafter{\ekv@name@set{#2}}%
- \unexpanded\expandafter{\ekv@name@key{##1}}%
+ \ekv@unexpanded\expandafter{\ekv@name@set{#2}}%
+ \ekv@unexpanded\expandafter{\ekv@name@key{##1}}%
}%
- \unexpanded{\fi}%
+ \ekv@unexpanded{\fi}%
}
\ekv@exparg{\protected\def\ekv@defsetmacro#1#2}{\ekv@defsetmacro}
% \end{macrocode}
@@ -2341,12 +2396,12 @@ and the derived files expkv.pdf
% \begin{macrocode}
\edef\ekv@set@noeq
{%
- \unexpanded
+ \ekv@unexpanded
{%
\ekv@ifblank@##1\ekv@nil\ekv@ifempty@B\ekv@set@was@blank
\ekv@ifempty@A\ekv@ifempty@B
}%
- \unexpanded\expandafter
+ \ekv@unexpanded\expandafter
{\ekv@strip{##1}{\expandafter\ekv@set@key\detokenize}\ekv@mark}%
}
\ekv@exparg
@@ -2398,12 +2453,15 @@ and the derived files expkv.pdf
% macro, that has the same scope as |\keyval_parse:NNn| from \pkg{expl3}.
% This is pretty analogue to the |\ekvset| implementation, we just put an
% |\unexpanded| here and there instead of other macros to stop the |\expanded|
-% on our output. The |\unexpanded\expanded{{...}}| ensures that the material is
-% in an alignment safe group at all time, and that it doesn't expand any further
-% in an |\edef| or |\expanded| context.
+% on our output. The |\unexpanded\expanded{{...}}| ensures that the material
+% is in an alignment safe group at all time, and that it doesn't expand any
+% further in an |\edef| or |\expanded| context.
% \begin{macrocode}
\long\def\ekvparse##1##2##3%
- {\unexpanded\expanded{{\ekv@parse{##1}{##2}\ekv@mark##3#1\ekv@stop#1}}}
+ {%
+ \ekv@unexpanded\ekv@expanded
+ {{\ekv@parse{##1}{##2}\ekv@mark##3#1\ekv@stop#1}}%
+ }
% \end{macrocode}
% \end{macro}
%
@@ -2485,12 +2543,12 @@ and the derived files expkv.pdf
% \begin{macrocode}
\edef\ekv@parse@noeq
{%
- \unexpanded
+ \ekv@unexpanded
{%
\ekv@ifblank@##1\ekv@nil\ekv@ifempty@B\ekv@parse@was@blank
\ekv@ifempty@A\ekv@ifempty@B
}%
- \unexpanded\expandafter{\ekv@strip{##1}\ekv@parse@key}%
+ \ekv@unexpanded\expandafter{\ekv@strip{##1}\ekv@parse@key}%
}
\ekv@exparg
{%
@@ -2521,7 +2579,7 @@ and the derived files expkv.pdf
{\ekv@strip{##2}\ekv@parse@pair@{##1}}
\long\def\ekv@parse@pair@##1##2##3##4%
{%
- \unexpanded{##4{##2}{##1}}%
+ \ekv@unexpanded{##4{##2}{##1}}%
\ekv@parse@other{##3}{##4}%
}
% \end{macrocode}
@@ -2531,7 +2589,7 @@ and the derived files expkv.pdf
% \begin{macrocode}
\long\def\ekv@parse@key##1##2%
{%
- \unexpanded{##2{##1}}%
+ \ekv@unexpanded{##2{##1}}%
\ekv@parse@other{##2}%
}
% \end{macrocode}
@@ -2552,8 +2610,8 @@ and the derived files expkv.pdf
% \begin{macrocode}
\edef\ekvsetSneaked
{%
- \unexpanded{\ekvsneak{#2}}%
- \unexpanded\expandafter{\ekvset{#1}{#3}}%
+ \ekv@unexpanded{\ekvsneak{#2}}%
+ \ekv@unexpanded\expandafter{\ekvset{#1}{#3}}%
}
\ekv@expargtwice{\long\def\ekvsetSneaked#1#2#3}{\ekvsetSneaked}
% \end{macrocode}
@@ -2891,7 +2949,8 @@ and the derived files expkv.pdf
\ekv@exparg{\long\def\ekv@err@common@#1`#2' #3.#4#5}%
{\ekv@err{#4 `#5' in set `#2'}}
\ekv@exparg{\long\def\ekv@err@unknown#1}{\ekv@err@common{unknown key}{#1}}
-\ekv@exparg{\long\def\ekv@err@noarg #1}{\ekv@err@common{unwanted value for}{#1}}
+\ekv@exparg{\long\def\ekv@err@noarg #1}
+ {\ekv@err@common{unwanted value for}{#1}}
\ekv@exparg{\long\def\ekv@err@reqval #1}{\ekv@err@common{missing value for}{#1}}
\ekv@exparg{\long\def\ekv@err@redirect@kv@notfound#1#2#3\ekv@stop}%
{\ekv@err{no key `#2' in sets #3}}
diff --git a/macros/generic/expkv/expkv.pdf b/macros/generic/expkv/expkv.pdf
index 59012eccd1..2da6f6dada 100644
--- a/macros/generic/expkv/expkv.pdf
+++ b/macros/generic/expkv/expkv.pdf
Binary files differ
diff --git a/macros/luatex/generic/penlight/penlight.lua b/macros/luatex/generic/penlight/penlight.lua
new file mode 100644
index 0000000000..bea9316071
--- /dev/null
+++ b/macros/luatex/generic/penlight/penlight.lua
@@ -0,0 +1,13705 @@
+
+---------------------------------------------------------
+----------------Auto generated code block----------------
+---------------------------------------------------------
+
+do
+ local searchers = package.searchers or package.loaders
+ local origin_seacher = searchers[2]
+ searchers[2] = function(path)
+ local files =
+ {
+------------------------
+-- Modules part begin --
+------------------------
+
+["pl.xml"] = function()
+--------------------
+-- Module: 'pl.xml'
+--------------------
+--- 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
+
+
+end,
+
+["pl.app"] = function()
+--------------------
+-- Module: 'pl.app'
+--------------------
+--- 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
+
+end,
+
+["pl.array2d"] = function()
+--------------------
+-- Module: 'pl.array2d'
+--------------------
+--- 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
+
+end,
+
+["pl.class"] = function()
+--------------------
+-- Module: 'pl.class'
+--------------------
+--- 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
+
+
+end,
+
+["pl.compat"] = function()
+--------------------
+-- Module: 'pl.compat'
+--------------------
+----------------
+--- 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
+
+end,
+
+["pl.comprehension"] = function()
+--------------------
+-- Module: 'pl.comprehension'
+--------------------
+--- 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
+
+end,
+
+["pl.config"] = function()
+--------------------
+-- Module: 'pl.config'
+--------------------
+--- 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
+
+end,
+
+["pl.data"] = function()
+--------------------
+-- Module: 'pl.data'
+--------------------
+--- 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
+
+
+end,
+
+["pl.Date"] = function()
+--------------------
+-- Module: 'pl.Date'
+--------------------
+--- 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
+
+
+end,
+
+["pl.dir"] = function()
+--------------------
+-- Module: 'pl.dir'
+--------------------
+--- 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
+
+end,
+
+["pl.file"] = function()
+--------------------
+-- Module: 'pl.file'
+--------------------
+--- 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
+
+end,
+
+["pl.func"] = function()
+--------------------
+-- Module: 'pl.func'
+--------------------
+--- 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
+
+
+
+end,
+
+["pl.import_into"] = function()
+--------------------
+-- Module: 'pl.import_into'
+--------------------
+--------------
+-- 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
+
+end,
+
+["pl.init"] = function()
+--------------------
+-- Module: 'pl.init'
+--------------------
+--------------
+-- 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
+
+end,
+
+["pl.input"] = function()
+--------------------
+-- Module: 'pl.input'
+--------------------
+--- 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
+
+
+end,
+
+["pl.lapp"] = function()
+--------------------
+-- Module: 'pl.lapp'
+--------------------
+--- 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
+
+
+
+end,
+
+["pl.lexer"] = function()
+--------------------
+-- Module: 'pl.lexer'
+--------------------
+--- 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
+
+end,
+
+["pl.List"] = function()
+--------------------
+-- Module: 'pl.List'
+--------------------
+--- 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
+
+
+end,
+
+["pl.luabalanced"] = function()
+--------------------
+-- Module: 'pl.luabalanced'
+--------------------
+--- 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
+
+end,
+
+["pl.Map"] = function()
+--------------------
+-- Module: 'pl.Map'
+--------------------
+--- 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
+
+end,
+
+["pl.MultiMap"] = function()
+--------------------
+-- Module: 'pl.MultiMap'
+--------------------
+--- 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
+
+end,
+
+["pl.operator"] = function()
+--------------------
+-- Module: 'pl.operator'
+--------------------
+--- 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
+
+end,
+
+["pl.OrderedMap"] = function()
+--------------------
+-- Module: 'pl.OrderedMap'
+--------------------
+--- 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
+
+
+
+
+end,
+
+["pl.path"] = function()
+--------------------
+-- Module: 'pl.path'
+--------------------
+--- 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
+
+end,
+
+["pl.permute"] = function()
+--------------------
+-- Module: 'pl.permute'
+--------------------
+--- 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
+
+end,
+
+["pl.pretty"] = function()
+--------------------
+-- Module: 'pl.pretty'
+--------------------
+--- 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
+})
+
+end,
+
+["pl.seq"] = function()
+--------------------
+-- Module: 'pl.seq'
+--------------------
+--- 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
+
+end,
+
+["pl.Set"] = function()
+--------------------
+-- Module: 'pl.Set'
+--------------------
+--- 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
+
+end,
+
+["pl.sip"] = function()
+--------------------
+-- Module: 'pl.sip'
+--------------------
+--- 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
+
+end,
+
+["pl.strict"] = function()
+--------------------
+-- Module: 'pl.strict'
+--------------------
+--- 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
+
+end,
+
+["pl.stringio"] = function()
+--------------------
+-- Module: 'pl.stringio'
+--------------------
+--- 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
+
+end,
+
+["pl.stringx"] = function()
+--------------------
+-- Module: 'pl.stringx'
+--------------------
+--- 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
+
+end,
+
+["pl.tablex"] = function()
+--------------------
+-- Module: 'pl.tablex'
+--------------------
+--- 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
+
+end,
+
+["pl.template"] = function()
+--------------------
+-- Module: 'pl.template'
+--------------------
+--- 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
+
+end,
+
+["pl.test"] = function()
+--------------------
+-- Module: 'pl.test'
+--------------------
+--- 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
+
+end,
+
+["pl.text"] = function()
+--------------------
+-- Module: 'pl.text'
+--------------------
+--- 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
+
+end,
+
+["pl.types"] = function()
+--------------------
+-- Module: 'pl.types'
+--------------------
+---- 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
+
+end,
+
+["pl.url"] = function()
+--------------------
+-- Module: 'pl.url'
+--------------------
+--- 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
+
+end,
+
+["pl.utils"] = function()
+--------------------
+-- Module: 'pl.utils'
+--------------------
+--- 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
+
+
+
+end,
+
+----------------------
+-- Modules part end --
+----------------------
+ }
+ if files[path] then
+ return files[path]
+ else
+ return origin_seacher(path)
+ end
+ end
+end
+---------------------------------------------------------
+----------------Auto generated code block----------------
+---------------------------------------------------------
+return require('pl.import_into')() \ No newline at end of file
diff --git a/macros/luatex/generic/penlight/penlight.pdf b/macros/luatex/generic/penlight/penlight.pdf
index 3502bb32ec..688706ad51 100644
--- a/macros/luatex/generic/penlight/penlight.pdf
+++ b/macros/luatex/generic/penlight/penlight.pdf
Binary files differ
diff --git a/macros/luatex/generic/penlight/penlight.sty b/macros/luatex/generic/penlight/penlight.sty
index 7a04192f58..9b0baed08b 100644
--- a/macros/luatex/generic/penlight/penlight.sty
+++ b/macros/luatex/generic/penlight/penlight.sty
@@ -1,5 +1,5 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-09-19
+% 2021-09-20
%
% Copyright (C) 2021 Kale Ewasiuk
%
@@ -23,14 +23,21 @@
% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
% OR OTHER DEALINGS IN THE SOFTWARE.
-\ProvidesPackage{penlight}[2021-09-19]
+\ProvidesPackage{penlight}[2021-09-20]
\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')}}
+\DeclareOption{pl}{\directlua{
+ pl = require('penlight')
+ __PENLIGHT__ = 'pl'
+}}
+\DeclareOption{penlight}{\directlua{
+ penlight = require('penlight')
+ __PENLIGHT__ = 'penlight'
+}}
+\DeclareOption{stringx}{\directlua{_G[__PENLIGHT__].stringx.import()}}
+\DeclareOption{format}{\directlua{_G[__PENLIGHT__].text.format_operator()}}
+\DeclareOption{func}{\directlua{_G[__PENLIGHT__].utils.import(__PENLIGHT__..'.func')}}
+\DeclareOption{extras}{\directlua{require('penlightextras')}}
-\ProcessOptions\relax
+\ProcessOptions*\relax
diff --git a/macros/luatex/generic/penlight/penlight.tex b/macros/luatex/generic/penlight/penlight.tex
index 2259cf27fd..9d3330bc9b 100644
--- a/macros/luatex/generic/penlight/penlight.tex
+++ b/macros/luatex/generic/penlight/penlight.tex
@@ -1,5 +1,5 @@
% Kale Ewasiuk (kalekje@gmail.com)
-% 2021-09-19
+% 2021-09-20
%
% Copyright (C) 2021 Kale Ewasiuk
%
@@ -24,50 +24,60 @@
% 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}
+ \section*{Penlight -- Lua libraries for use in LuaLaTeX}
+ v. 2021-09-20, Kale Ewasiuk, \url{kalekje@gmail.com}\\\\
- \subsection*{Default}
-
- \begin{verbatim}
- \usepackage{penlight}
- \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#}}
+ \\
- 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:
+ \subsection*{Required Package Option}
+ The first option sent to this package MUST be one of: \\
+ \texttt{[penlight]} \ \ \ or \ \ \ \texttt{[pl]}.\\
+ All Penlight sub-modules are then available under this global variable by either\\
+ \texttt{penlight.XYZ} or \texttt{pl.XYZ}
- \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}
+ \subsection*{Additional 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 \\
+ \texttt{func} & allows placehold expressions eg. \texttt{\_1+1} to be used \\
& this will be ran in pre-amble: \texttt{penlight.utils.import('pl.func')}\\
& \hspace*{-4em}\url{https://stevedonovan.github.io/Penlight/api/libraries/pl.func.html}\\\\
+ \texttt{extras} & adds some additional functions. Adds functions to some Penlight sub-modules. The following Lua globals will be defined:
+ \texttt{close\_bkt\_cnt,
+ add\_bkt\_cnt,
+ reset\_bkt\_cnt,
+ \_NumBkts,
+ \_xTrue,
+ \_xFalse,
+ \_xNoValue,
+ \_\_SKIP\_TEX\_\_,
+ hasval,
+ mod, mod2,
+ }\\
\end{tabular}
+ \\\\
+ If you want to use Penlight (and extras) with the \texttt{texlua} intrepreter (no document made, only for Lua files, useful for testing),
+ you can access it by the following:
\begin{verbatim}
-
+ package.path = package.path .. ';'..'path/to/texmf/tex/latex/penlight/?.lua'
+ penlight = require('penlight')
+ __SKIP_TEX__ = true --only required if you want to use
+ --penlightextras without a LaTeX run
+ require('penlightextras')
\end{verbatim}
@@ -75,10 +85,11 @@
\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}
+ 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}\\
+ The author of this library has merged all Lua sub-modules into one file for this package.
\end{document} \ No newline at end of file
diff --git a/macros/luatex/generic/penlight/penlightextras.lua b/macros/luatex/generic/penlight/penlightextras.lua
new file mode 100644
index 0000000000..f612bd3370
--- /dev/null
+++ b/macros/luatex/generic/penlight/penlightextras.lua
@@ -0,0 +1,413 @@
+-- requires penlight
+local pl = _G['penlight'] or _G['pl'] -- penlight for this namespace is pl
+local bind = bind or pl.func.bind
+
+function help_wrt(s1, s2) -- helpful printing, makes it easy to debug, s1 is object, s2 is note
+ local wrt = wrt or texio.write_nl
+ local wrt = wrt or print
+ s2 = s2 or ''
+ wrt('\nvvvvv '..s2)
+ if type(s1) == 'table' then
+ wrt(pl.pretty.write(s1))
+ else
+ wrt(tostring(s1))
+ end
+ wrt('^^^^^\n')
+end
+
+
+function prt_array2d(t)
+ for _, r in ipairs(t) do
+ local s = ''
+ for _, v in ipairs(r) do
+ s = s.. tostring(v)..', '
+ end
+ print(s)
+ end
+end
+
+-- -- -- -- --
+
+
+-- -- -- -- functions below are helpers for arrays and 2d
+
+local function compare_elements(a, b, op, ele)
+ op = op or pl.oper.gt
+ ele = ele or 1
+ return op(a[ele], b[ele])
+end
+
+local function comp_2ele_func(op, ele) -- make a 2 element comparison function,
+ --sort with function on element nnum
+ return bind(compare_elements, _1, _2, op, ele)
+end
+
+
+-- -- -- --
+
+local function check_index(ij, rc) -- converts array index to positive value if negative
+ if type(ij) ~= 'number' then
+ return 1
+ else
+ if ij < 0 then
+ ij = rc + ij + 1
+ elseif ij > rc then
+ ij = rc
+ end
+ return ij
+ end
+end
+local function check_slice(M, i1, j1, i2, j2) -- ensure a slice is valid; i.e. all positive numbers
+ r, c = pl.array2d.size(M)
+ i1 = check_index(i1, r)
+ i2 = check_index(i2, r)
+ j1 = check_index(j1, c)
+ j2 = check_index(j2, c)
+ return i1, j1, i2, j2
+end
+
+local function check_func(func) -- check if a function is a PE, if so, make it a function
+ if type(func) ~= 'function' then
+ __func = I(func)
+ end
+ return __func
+end
+
+
+-- -- -- -- -- -- --
+-- -- -- -- functions below extend the array2d module
+
+
+local function map_slice1(func, L, i1, i2) -- map a function to a slice of an array, can use PlcExpr
+ i2 = i2 or i1
+ local len = #L
+ i1 = check_index(i1, len)
+ i2 = check_index(i2, len)
+ func = check_func(func)
+ for i in pl.seq.range(i1,i2) do
+ L[i] = func(L[i])
+ end
+ return L
+end
+
+local function map_slice2(func, M, i1, j1, i2, j2) -- map a function to a slice of a Matrix
+ i1, j1, i2, j2 = check_slice(M, i1, j1, i2, j2)
+ --for i,j in array2d.iter(M, true, i1, j1, i2, j2) do --todo this did not work, penlight may have fixed this
+ func = check_func(func)
+ for i in pl.seq.range(i1,i2) do
+ for j in pl.seq.range(j1,j2) do
+ M[i][j] = func(M[i][j])
+ end
+ end
+ return M
+end
+
+local function map_columns(func, M, j1, j2) -- map function to columns of matrix
+ j2 = j2 or j1
+ return map_slice2(func, M, 1, j1, -1, j2)
+end
+
+local function map_rows(func, M, i1, i2) -- map function to rows of matrix
+ i2 = i2 or i1
+ return map_slice2(func, M, i1, 1, i2, -1)
+end
+
+
+-- -- -- -- -- -- -- --
+
+function sortOP(M, op, ele) -- sort a 2d array based on operator criteria, ele is column, ie sort on which element
+ M_new = {}
+ for row in pl.seq.sort(M, comp_2ele_func(op, ele)) do
+ M_new[#M_new+1] = row
+ end
+ return M_new
+end
+
+local function like(M1, v)
+ v = v or 0
+ r, c = pl.array2d.size(M1)
+ return pl.array2d.new(r,c,v)
+end
+
+function from_table(t) -- turns a labelled table to a 2d, label-free array
+ t_new = {}
+ for k, v in pairs(t) do
+ if type(v) == 'table' then
+ t_new_row = {k}
+ for _, v_ in ipairs(v) do
+ t_new_row[#t_new_row+1] = v_
+ end
+ t_new[#t_new+1] = t_new_row
+ else
+ t_new[#t_new+1] = {k, v}
+ end
+ end
+ return t_new
+end
+
+function toTeX(M, EL) --puts & between columns, can choose to end line with \\ if EL is true (end-line)
+ EL = EL or false
+ if EL then EL = '\\\\' else EL = '' end
+ return pl.array2d.reduce2(_1..EL.._2, _1..'&'.._2, M)..EL
+end
+
+-- -- -- -- -- -- --
+
+-- add to array2d module
+pl.array2d['map_columns'] = map_columns
+pl.array2d['map_rows'] = map_rows
+pl.array2d['map_slice2'] = map_slice2
+pl.array2d['map_slice1'] = map_slice1
+pl.array2d['from_table'] = from_table
+pl.array2d['toTeX'] = toTeX
+pl.array2d['sortOP'] = sortOP
+pl.array2d['like'] = like
+
+
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- functions below extend the operator module
+
+local function strgt(a,b) return tostring(a) > tostring(b) end
+local function strlt(a,b) return tostring(a) < tostring(b) end
+pl.operator['strgt'] = strgt
+pl.operator['strlt'] = strlt
+
+
+
+
+
+local function gextract(s, pat) --extract a pattern from string, returns both
+ local s_extr = ''
+ local s_rem = s
+ for e in s:gmatch(pat) do
+ s_extr = s_extr..e
+ s_rem = s_rem:gsub(e,'')
+ end
+ return s_extr, s_rem
+end
+
+local function gfirst(s, t) -- get the first pattern found from a table of pattern
+ for _, pat in pairs(t) do
+ if string.find(s, pat) then
+ return pat
+ end
+ end
+end
+
+local function appif(S, W, B, O) --append W ord to S tring if B oolean true, otherwise O ther
+ --append Word to String
+ if B then --if b is true
+ S = S .. W
+ else --consider Other word
+ O = O or ''
+ S = S .. O
+ end
+ return S
+end
+
+local mt = getmetatable("") -- register functions with str
+mt.__index["gextract"] = gextract
+mt.__index["gfirst"] = gfirst
+mt.__index["app_if"] = appif
+
+
+function mod(a, b) -- math modulo, return remainder only
+ return a - (math.floor(a/b)*b)
+end
+
+function mod2(a) -- math modulo 2
+ return mod(a,2)
+end
+
+function hasval(x) -- if something has value
+ -- todo check for Lists, other penlight objects
+ if (x == nil) or (x == false) or (x == 0) or (x == '') then
+ return false
+ elseif type(x) == 'table' then
+ if #x == 0 then
+ return false
+ else
+ return true
+ end
+ end
+ return true
+end
+
+
+
+
+-- testing here
+____zzz__ = [[
+function hasStrKey(T)
+ --checks if a Table contains a string-type key
+ local hasaStrKey = false
+ for k, v in pairs(T) do --look through table pairs
+ if type(k) == "string" then --if any string found, return true
+ hasaStrKey = true
+ break
+ end
+ end
+ return hasaStrKey
+end
+
+
+function any()
+ --todo go through table or list of args and return true of anything is something
+end
+
+function all()
+ --todo go through table or list of args and return true of anything is something
+end
+
+
+ --elseif x == {} then
+ -- return false
+
+n = nil
+b = false
+z = 0
+e = ''
+t = {}
+
+--s = 'a'
+--n = 1
+
+
+if is(nil) then
+ print('TRUE')
+else
+ print('FALSE')
+end
+
+--print({}=={})
+--print(0.0==0)
+
+
+]]
+
+
+
+
+
+
+
+
+
+
+
+-- Some simple and helpful LaTeX functions
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+
+--Generic LuaLaTeX utilities for print commands or environments
+-- http://lua-users.org/wiki/SplitJoin -- todo read me!!
+__SKIP_TEX__ = __SKIP_TEX__ or false --if declared true before here, it will use regular print functions
+-- (for troubleshooting with texlua)
+
+-- xparse defaults
+_xTrue = '\\BooleanTrue '
+_xFalse = '\\BooleanFalse '
+_xNoValue = '-NoValue-'
+
+
+if not __SKIP_TEX__ then
+ prt = tex.sprint --can print lists, but will NOT put new line between them
+ prtn = tex.print --can print lists and will put new line. C-function not in Lua. think P rint R return
+ wrt = texio.write
+ wrtn = texio.write_nl
+else
+ prt = io.write
+ prtn = print --print with new line
+ wrt = io.write
+ wrtn = io.write_nl
+end
+
+function prtl(str) -- prints a literal string to latex, adds new line between them
+ for line in str:gmatch"[^\n]*" do -- gets all characters up to a new line
+ prtn(line)
+ end
+end
+
+
+
+_NumBkts = 0
+function reset_bkt_cnt(n)
+ n = n or 0
+ _NumBkts = n
+end
+
+function add_bkt_cnt(n)
+ n = n or 1
+ _NumBkts = _NumBkts + n
+end
+
+function close_bkt_cnt()
+ local s = ('}'):rep(_NumBkts)
+ reset_bkt_cnt()
+ return s
+end
+
+
+
+
+
+
+
+
+--definition helpers -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
+-- todo add this
+
+local function defcmd_nest(cs) -- for option if you'd like your commands under a parent ex. \csparent{var}
+ tex.print('\\gdef\\'..cs..'#1{\\csname '..var..'--#1--\\endcsname}')
+end
+
+
+local function defcmd(cs, val, nargs)
+ if (nargs == nil) or (args == 0) then
+ token.set_macro(cs, tostring(val), 'global')
+ else
+ local args = '#1'
+ tex.print('\\gdef\\'..cs..args..'{'..val..'}')
+ -- todo https://tex.stackexchange.com/questions/57551/create-a-capitalized-macro-token-using-csname
+ -- \expandafter\gdef\csname Two\endcsname#1#2{1:#1, two:#2} --todo do it like this
+ end
+end
+
+
+local function prvcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ tex.print('\\PackageWarning{YAMLvars}{Variable '..cs..' already defined, could not declare}{}')
+ else
+ defcmd(cs, val)
+ end
+end
+
+
+local function newcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ tex.print('\\PackageError{luadefs}{Command '..cs..' already defined}{}')
+ else
+ defcmd(cs, val)
+ end
+end
+
+local function renewcmd(cs, val) -- provide command via lua
+ if token.is_defined(cs) then
+ defcmd(cs, val)
+ else
+ tex.print('\\PackageError{luadefs}{Command '..cs..' already defined}{}')
+ end
+end
+
+local function deccmd(cs, def)
+ if def == nil then
+ prvcmd(cs, '\\PackageError{luadefs}{Command "'..cs..'" was declared and used but, not set}{}')
+ else
+ prvcmd(cs, def)
+ end
+end
+
+
+-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
diff --git a/macros/luatex/generic/penlight/pl/Date.lua b/macros/luatex/generic/penlight/pl/Date.lua
deleted file mode 100644
index 039dbaf7fd..0000000000
--- a/macros/luatex/generic/penlight/pl/Date.lua
+++ /dev/null
@@ -1,675 +0,0 @@
---- 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
deleted file mode 100644
index 111b2e109d..0000000000
--- a/macros/luatex/generic/penlight/pl/LICENSE-pl.md
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-# "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
deleted file mode 100644
index b66c251eaf..0000000000
--- a/macros/luatex/generic/penlight/pl/List.lua
+++ /dev/null
@@ -1,566 +0,0 @@
---- 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
deleted file mode 100644
index 572b22ad0b..0000000000
--- a/macros/luatex/generic/penlight/pl/Map.lua
+++ /dev/null
@@ -1,120 +0,0 @@
---- 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
deleted file mode 100644
index 0abe84de02..0000000000
--- a/macros/luatex/generic/penlight/pl/MultiMap.lua
+++ /dev/null
@@ -1,54 +0,0 @@
---- 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
deleted file mode 100644
index 379c44fde0..0000000000
--- a/macros/luatex/generic/penlight/pl/OrderedMap.lua
+++ /dev/null
@@ -1,167 +0,0 @@
---- 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
deleted file mode 100644
index 91699d03ce..0000000000
--- a/macros/luatex/generic/penlight/pl/README-pl.md
+++ /dev/null
@@ -1,127 +0,0 @@
-# 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
deleted file mode 100644
index ce428f0a0d..0000000000
--- a/macros/luatex/generic/penlight/pl/Set.lua
+++ /dev/null
@@ -1,222 +0,0 @@
---- 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
deleted file mode 100644
index a06305da4f..0000000000
--- a/macros/luatex/generic/penlight/pl/app.lua
+++ /dev/null
@@ -1,297 +0,0 @@
---- 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
deleted file mode 100644
index 2bc13b9cd4..0000000000
--- a/macros/luatex/generic/penlight/pl/array2d.lua
+++ /dev/null
@@ -1,579 +0,0 @@
---- 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
deleted file mode 100644
index 49246ee32b..0000000000
--- a/macros/luatex/generic/penlight/pl/class.lua
+++ /dev/null
@@ -1,265 +0,0 @@
---- 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
deleted file mode 100644
index 1707ef6849..0000000000
--- a/macros/luatex/generic/penlight/pl/compat.lua
+++ /dev/null
@@ -1,250 +0,0 @@
-----------------
---- 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
deleted file mode 100644
index 39be7c5260..0000000000
--- a/macros/luatex/generic/penlight/pl/comprehension.lua
+++ /dev/null
@@ -1,285 +0,0 @@
---- 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
deleted file mode 100644
index 2e6db0cb63..0000000000
--- a/macros/luatex/generic/penlight/pl/config.lua
+++ /dev/null
@@ -1,207 +0,0 @@
---- 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
deleted file mode 100644
index a565ebc9a1..0000000000
--- a/macros/luatex/generic/penlight/pl/data.lua
+++ /dev/null
@@ -1,654 +0,0 @@
---- 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
deleted file mode 100644
index b19c8fc440..0000000000
--- a/macros/luatex/generic/penlight/pl/dir.lua
+++ /dev/null
@@ -1,524 +0,0 @@
---- 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
deleted file mode 100644
index b8058c412c..0000000000
--- a/macros/luatex/generic/penlight/pl/file.lua
+++ /dev/null
@@ -1,55 +0,0 @@
---- 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
deleted file mode 100644
index daac0fce3c..0000000000
--- a/macros/luatex/generic/penlight/pl/func.lua
+++ /dev/null
@@ -1,393 +0,0 @@
---- 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
deleted file mode 100644
index 6dd274130a..0000000000
--- a/macros/luatex/generic/penlight/pl/import_into.lua
+++ /dev/null
@@ -1,91 +0,0 @@
---------------
--- 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
deleted file mode 100644
index c27a890b56..0000000000
--- a/macros/luatex/generic/penlight/pl/init.lua
+++ /dev/null
@@ -1,11 +0,0 @@
---------------
--- 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
deleted file mode 100644
index f81de87c4d..0000000000
--- a/macros/luatex/generic/penlight/pl/input.lua
+++ /dev/null
@@ -1,171 +0,0 @@
---- 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
deleted file mode 100644
index 56d1d9d72f..0000000000
--- a/macros/luatex/generic/penlight/pl/lapp.lua
+++ /dev/null
@@ -1,451 +0,0 @@
---- 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
deleted file mode 100644
index 9219716507..0000000000
--- a/macros/luatex/generic/penlight/pl/lexer.lua
+++ /dev/null
@@ -1,515 +0,0 @@
---- 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
deleted file mode 100644
index a1f7dc67d7..0000000000
--- a/macros/luatex/generic/penlight/pl/luabalanced.lua
+++ /dev/null
@@ -1,264 +0,0 @@
---- 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
deleted file mode 100644
index 60eaffd10a..0000000000
--- a/macros/luatex/generic/penlight/pl/operator.lua
+++ /dev/null
@@ -1,209 +0,0 @@
---- 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
deleted file mode 100644
index a438a9df87..0000000000
--- a/macros/luatex/generic/penlight/pl/path.lua
+++ /dev/null
@@ -1,575 +0,0 @@
---- 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
deleted file mode 100644
index ce646f4d52..0000000000
--- a/macros/luatex/generic/penlight/pl/permute.lua
+++ /dev/null
@@ -1,196 +0,0 @@
---- 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
deleted file mode 100644
index 09add30e24..0000000000
--- a/macros/luatex/generic/penlight/pl/pretty.lua
+++ /dev/null
@@ -1,437 +0,0 @@
---- 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
deleted file mode 100644
index 1c08d2059b..0000000000
--- a/macros/luatex/generic/penlight/pl/seq.lua
+++ /dev/null
@@ -1,544 +0,0 @@
---- 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
deleted file mode 100644
index 5d1f2aa0cc..0000000000
--- a/macros/luatex/generic/penlight/pl/sip.lua
+++ /dev/null
@@ -1,337 +0,0 @@
---- 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
deleted file mode 100644
index 67cbf5b7f2..0000000000
--- a/macros/luatex/generic/penlight/pl/strict.lua
+++ /dev/null
@@ -1,138 +0,0 @@
---- 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
deleted file mode 100644
index 666f415818..0000000000
--- a/macros/luatex/generic/penlight/pl/stringio.lua
+++ /dev/null
@@ -1,158 +0,0 @@
---- 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
deleted file mode 100644
index 627611316b..0000000000
--- a/macros/luatex/generic/penlight/pl/stringx.lua
+++ /dev/null
@@ -1,594 +0,0 @@
---- 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
deleted file mode 100644
index 81f739529e..0000000000
--- a/macros/luatex/generic/penlight/pl/tablex.lua
+++ /dev/null
@@ -1,999 +0,0 @@
---- 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
deleted file mode 100644
index 9961a1bdfe..0000000000
--- a/macros/luatex/generic/penlight/pl/template.lua
+++ /dev/null
@@ -1,202 +0,0 @@
---- 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
deleted file mode 100644
index 694bbc531a..0000000000
--- a/macros/luatex/generic/penlight/pl/test.lua
+++ /dev/null
@@ -1,164 +0,0 @@
---- 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
deleted file mode 100644
index e626e86cdf..0000000000
--- a/macros/luatex/generic/penlight/pl/text.lua
+++ /dev/null
@@ -1,246 +0,0 @@
---- 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
deleted file mode 100644
index 35b0ccb54a..0000000000
--- a/macros/luatex/generic/penlight/pl/types.lua
+++ /dev/null
@@ -1,183 +0,0 @@
----- 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
deleted file mode 100644
index 8c7cfeb7aa..0000000000
--- a/macros/luatex/generic/penlight/pl/url.lua
+++ /dev/null
@@ -1,51 +0,0 @@
---- 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
deleted file mode 100644
index 66edda6db6..0000000000
--- a/macros/luatex/generic/penlight/pl/utils.lua
+++ /dev/null
@@ -1,777 +0,0 @@
---- 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
deleted file mode 100644
index 4650a32099..0000000000
--- a/macros/luatex/generic/penlight/pl/xml.lua
+++ /dev/null
@@ -1,776 +0,0 @@
---- 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/macros/xetex/latex/hanzibox/README.md b/macros/xetex/latex/hanzibox/README.md
new file mode 100644
index 0000000000..58818c5ab5
--- /dev/null
+++ b/macros/xetex/latex/hanzibox/README.md
@@ -0,0 +1,52 @@
+hanzibox
+LaTeX3 Chinese Character package, v1.01
+=======
+
+`hanzibox` is a LaTeX package written to simplify the input of Chinese
+with Hanyu Pinyin and translation. Hanyu Pinyin was placed above Chinese with
+xpinyin package and the translation was placed below the Chinese.
+
+`hanzibox` is a utility for creating practise sheets, dialogs, character explanations and more.
+There are PDFs included which may be useful to regular users.
+
+It was inspired by hanzibox.sty from https://github.com/janvorisek/chinese-latex-utilities.
+
+In this new package, the commands were kept, but redesign its options and mandatories.
+
+At present, hanzibox.sty only supports XeTeX engines and only allows UTF-8 encoding tex source file.
+
+You can read the package manual (in Chinese) for more detailed explanations.
+
+Gitee repository: https://gitee.com/nwafu_nan/hanzibox-l3.
+
+Contributing
+------------
+
+Issues and pull requests are welcome.
+
+Copyright and Licence
+---------------------
+
+ Copyright (C) 2020-2021 by Nan Geng <nangeng@nwafu.edu.cn>
+ ----------------------------------------------------------------------
+
+ This work 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. This version of this license is in
+ http://www.latex-project.org/lppl/lppl-1-3c.txt
+ and the latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of
+ LaTeX version 2005/12/01 or later.
+
+ This work has the LPPL maintenance status "maintained".
+
+ The Current Maintainer of this work is Nan Geng.
+
+ This package consists of the file hanzibox.dtx,
+ and the derived files hanzibox.sty,
+ hanzibox.pdf,
+ hanzibox.ins,
+ README.md (this file).
+
diff --git a/macros/xetex/latex/hanzibox/build.sh b/macros/xetex/latex/hanzibox/build.sh
new file mode 100755
index 0000000000..f742d62e76
--- /dev/null
+++ b/macros/xetex/latex/hanzibox/build.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+xetex hanzibox.dtx &&\
+xelatex hanzibox.dtx &&\
+makeindex -s gind.ist -o hanzibox.ind hanzibox.idx &&\
+makeindex -s gglo.ist -o hanzibox.gls hanzibox.glo &&\
+xelatex hanzibox.dtx &&\
+xelatex hanzibox.dtx &&\
+xelatex hanzibox.dtx
diff --git a/macros/xetex/latex/hanzibox/hanzibox.dtx b/macros/xetex/latex/hanzibox/hanzibox.dtx
new file mode 100644
index 0000000000..f42af799ff
--- /dev/null
+++ b/macros/xetex/latex/hanzibox/hanzibox.dtx
@@ -0,0 +1,832 @@
+% \iffalse meta-comment
+% !TEX program = XeLaTeX
+%<*internal>
+\iffalse
+%</internal>
+%<*readme>
+hanzibox
+LaTeX3 Chinese Character package, v1.01
+=======
+
+`hanzibox` is a LaTeX package written to simplify the input of Chinese
+with Hanyu Pinyin and translation. Hanyu Pinyin was placed above Chinese with
+xpinyin package and the translation was placed below the Chinese.
+
+`hanzibox` is a utility for creating practise sheets, dialogs, character explanations and more.
+There are PDFs included which may be useful to regular users.
+
+It was inspired by hanzibox.sty from https://github.com/janvorisek/chinese-latex-utilities.
+
+In this new package, the commands were kept, but redesign its options and mandatories.
+
+At present, hanzibox.sty only supports XeTeX engines and only allows UTF-8 encoding tex source file.
+
+You can read the package manual (in Chinese) for more detailed explanations.
+
+Gitee repository: https://gitee.com/nwafu_nan/hanzibox-l3.
+
+Contributing
+------------
+
+Issues and pull requests are welcome.
+
+Copyright and Licence
+---------------------
+
+ Copyright (C) 2020-2021 by Nan Geng <nangeng@nwafu.edu.cn>
+ ----------------------------------------------------------------------
+
+ This work 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. This version of this license is in
+ http://www.latex-project.org/lppl/lppl-1-3c.txt
+ and the latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of
+ LaTeX version 2005/12/01 or later.
+
+ This work has the LPPL maintenance status "maintained".
+
+ The Current Maintainer of this work is Nan Geng.
+
+ This package consists of the file hanzibox.dtx,
+ and the derived files hanzibox.sty,
+ hanzibox.pdf,
+ hanzibox.ins,
+ README.md (this file).
+
+%</readme>
+%<*internal>
+\fi
+\begingroup
+ \def\temp{LaTeX2e}
+\expandafter\endgroup\ifx\temp\fmtname\else
+\csname fi\endcsname
+%</internal>
+%<*install>
+
+\input ctxdocstrip %
+
+\let\MetaPrefix\relax
+
+\preamble
+
+ Copyright (C) 2020-2021 by Nan Geng <nangeng@nwafu.edu.cn>
+--------------------------------------------------------------------------
+
+ This work 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. This version of this license is in
+ http://www.latex-project.org/lppl/lppl-1-3c.txt
+ and the latest version of this license is in
+ http://www.latex-project.org/lppl.txt
+ and version 1.3 or later is part of all distributions of
+ LaTeX version 2005/12/01 or later.
+
+ This work has the LPPL maintenance status "maintained".
+
+ The Current Maintainer of this work is Nan Geng Nan Geng.
+
+--------------------------------------------------------------------------
+
+\endpreamble
+
+\postamble
+
+ This package consists of the file hanzibox.dtx,
+ and the derived files hanzibox.sty,
+ hanzibox.pdf,
+ hanzibox.ins,
+ README.md.
+\endpostamble
+
+\declarepostamble\emptypostamble
+\endpostamble
+
+\def\MetaPrefix{-- }
+
+
+\let\MetaPrefix\DoubleperCent
+
+\generate
+ {
+%</install>
+%<*internal>
+ \usedir{source/latex/hanzibox}
+ \file{hanzibox.ins} {\from{\jobname.dtx}{install}}
+%</internal>
+%<*install>
+ \usedir{tex/latex/hanzibox}
+ \file{hanzibox.sty} {\from{\jobname.dtx}{package}}
+ \nopreamble\nopostamble
+ \usedir{doc/latex/hanzibox}
+ \file{README.md} {\from{\jobname.dtx}{readme}}
+ }
+
+\endbatchfile
+%</install>
+%<*internal>
+\fi
+%</internal>
+%<package>\NeedsTeXFormat{LaTeX2e}[2020/10/01]
+%<package>\RequirePackage{expl3}
+%<+package>\GetIdInfo$Id: hanzibox.dtx 1.0.0 2021-09-18 20:00:00Z +0800 Nan Geng <nangeng@nwafu.edu.cn> $
+%<package> {Boxed Chinese characters with Pinyin above and translation below.}
+%<package>\ProvidesExplPackage{\ExplFileName}
+%<package> {\ExplFileDate}{\ExplFileVersion}{\ExplFileDescription}
+%<*driver>
+\documentclass{ctxdoc}
+\usepackage{listings}
+\usepackage{xcolor}
+\usepackage{xcolor-material}
+\usepackage{xpinyin}
+\usepackage{hanzibox}
+\renewcommand*\marg[1]{\{\meta{#1}\}}
+\renewcommand*\oarg[1]{[\meta{#1}]}
+\renewcommand*\parg[1]{(\meta{#1})}
+\DeclareDocumentCommand\opt{m}{\texttt{#1}}
+\DeclareDocumentCommand\kvopt{mm}
+ {\texttt{#1\breakablethinspace=\breakablethinspace#2}}
+\def\breakablethinspace{\hskip 0.16667em\relax}
+\begin{document}
+ \DocInput{\jobname.dtx}
+ \IndexLayout
+ \PrintChanges
+ \PrintIndex
+\end{document}
+%</driver>
+% \fi
+%
+% \changes{v1.0.0}{2021/09/18}{开始编写模板。}
+%
+% \CheckSum{0}
+%
+% \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
+% Lower-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
+% Digits \0\1\2\3\4\5\6\7\8\9
+% Exclamation \! Double quote \" Hash (number) \#
+% Dollar \$ Percent \% Ampersand \&
+% Acute accent \' Left paren \( Right paren \)
+% Asterisk \* Plus \+ Comma \,
+% Minus \- Point \. Solidus \/
+% Colon \: Semicolon \; Less than \<
+% Equals \= Greater than \> Question mark \?
+% Commercial at \@ Left bracket \[ Backslash \\
+% Right bracket \] Circumflex \^ Underscore \_
+% Grave accent \` Left brace \{ Vertical bar \|
+% Right brace \} Tilde \~}
+%
+% \GetFileId{hanzibox.sty}
+%
+% \title{\bfseries\pkg{hanzibox}:田字格-米字格汉字练习宏包}
+% \author{耿楠\\ \path{nangeng@nwafu.edu.cn}}
+% \date{\filedate\qquad\fileversion\thanks{\url{https://gitee.com/nwafu_nan/hanzibox-l3}.}}
+% \maketitle
+%
+% \begin{documentation}
+%
+% \begin{abstract}
+%
+% \pkg{hanzibox} 是一个用\LaTeX3开发的 \LaTeX 宏包,提供了汉字学习中的田字格
+% 或米字格功能,并可以根据需要在汉字正上方显示拼音,在正下方显示需要的译文。
+%
+% \end{abstract}
+%
+% \section{简要说明}
+%
+% 建议使用\pkg{hanzibox}宏包的\LaTeX 源文件总是采用 \texttt{UTF-8}。
+%
+% \pkg{hanzibox} 还依赖 \package{l3kernel} 和 \package{l3packages}。
+%
+% \section{用户接口}
+%
+% \begin{function}[added=2021-09-18,updated=2021-09-19]{\hanzidialog}
+% \begin{syntax}
+% \tn{hanzidialog} \Arg{汉字} \oarg{拼音选项} \oarg{译文选项}
+% \end{syntax}
+% 排版汉字,并根据选项内容在顶部排版拼音,在底部排版译文。\\
+% 其中,\oarg{拼音选项}可以是任意文本,如果是汉语拼音则需要使用\pkg{xpinyin}宏包的
+% \tn{pinyin}命令进行处理;\oarg{译文选项}可以是任意文本。
+% \begin{SideBySideExample}[frame=single,numbers=left,xrightmargin=.3\linewidth,gobble=5]
+% \hanzidialog{王老师}[Wang \pinyin{lao3shi1}][teacher wang]
+% \hanzidialog{王老师}[Wang \pinyin{lao3shi1}][王先生]
+% \end{SideBySideExample}
+% \end{function}
+%
+% \begin{function}[added=2021-09-18,updated=2021-09-20]{\hanzibox,\hanzibox*}
+% \begin{syntax}
+% \tn{hanzibox} \oarg{外观选项} \Arg{汉字} \oarg{拼音选项} \oarg{译文选项}
+% \tn{hanzibox*} \oarg{外观选项} \Arg{汉字} \oarg{拼音选项} \oarg{译文选项}
+% \end{syntax}
+% 排版汉字,并根据选项内容在顶部排版拼音,在底部排版译文。\\
+% 其中,\oarg{外观选项}用于设置盒子外观;\oarg{拼音选项}用于设置拼音,
+% 要求其拼音格式符合\pkg{xpinyin}宏包的规定;\oarg{译文选项}可以是任意文本。
+% \begin{SideBySideExample}[frame=single,numbers=left,xrightmargin=.43\linewidth,gobble=5]
+% \hanzibox[inner=none,border=false]{我}[wo3][me]
+% \hanzibox[inner=cross,border=true]{我}[wo3][吾]
+% \hanzibox[inner=star]{我}[wo3][俺]
+% \hanzibox[borderwidth=1.5pt]{我}[wo3][爷]
+% \hanzibox[bordercolor=red]{我}[wo3][山人]
+% \end{SideBySideExample}
+% 星号命令用于根据汉字自动在顶部排版拼音,在底部排版译文。\\
+% 此时,\oarg{拼音选项}中的拼音无效,可以留空,但若有译文则该选项不可省略;
+% \oarg{译文选项}可以是任意文本。\\
+% 注意:此时的注意由\pkg{xpinyin}宏包实现,如需要设置多音字的拼音,请参阅其宏包说明。
+% \begin{SideBySideExample}[frame=single,numbers=left,xrightmargin=.43\linewidth,gobble=5]
+% \hanzibox*[inner=none,border=false]{我}[][me]
+% \hanzibox*[inner=cross,border=true]{我}[][吾]
+% \hanzibox*[inner=star]{我}[wo3][不才]
+% \hanzibox*[borderwidth=1.5pt]{我}[wo2][本人]
+% \hanzibox*[bordercolor=red]{我}[ni3][孤家]
+% \end{SideBySideExample}
+% \end{function}
+%
+% \section{选项说明}
+%
+% \begin{function}[added=2021-09-20]{\hanziboxset}
+% \begin{syntax}
+% \tn{hanziboxset} \marg{键值列表}
+% \end{syntax}
+% 本宏包提供了一系列选项,以实现汉字盒子外观样式设置。
+% 载入\pkg{hanzibox}宏包后,以下选项均可通过统一的\tn{hanziboxset}命令进行设置。
+% 同时,这些选项也可以通过\tn{hanzibox}命令的外观选项进行设置。
+% \end{function}
+%
+% \tn{hanziboxset} 的参数是一组由(英文)逗号隔开的选项列表,
+% 列表中的选项通常是 \kvopt{\meta{key}}{\meta{value}} 形式。
+% 部分选项的\meta{value} 可以省略。对于同一项,后续设置会覆盖以前的设置。
+% 在下文的说明中,将用\textbf{粗体}表示默认值。
+%
+% \tn{hanziboxset} 采用 \LaTeX3 风格的键值设置,支持不同类型以及多种
+% 层次的选项设定。键值列表中,“|=|”左右的空格不影响设置;但需注意,
+% 参数列表中\textbf{不可以出现空行}。
+%
+% 布尔型的参数 \kvopt{\meta{选项}}{true} 中的“|= true|”可以省略。
+%
+% 注:各选项也可以以相同的键值列表在汉字盒子命令的外观选项中进行设置。
+%
+% \subsection{缩放比例}
+%
+% \begin{function}[added=2021-09-20]{ratio}
+% \begin{syntax}
+% ratio = \Arg{number}
+% \end{syntax}
+% 设置\tn{hanzibox}命令绘制汉字盒子及其拼音和译文的整体缩放比例,默认值为\textbf{1.0}。
+% \end{function}
+%
+%
+% \subsection{汉字边框}
+%
+% \begin{function}[added=2021-09-20]{border}
+% \begin{syntax}
+% border = <\TTF>
+% \end{syntax}
+% 为\tn{hanzibox}命令选择是否绘制汉字的边框,默认值为\textbf{true}。该选项对\tn{hanzidialog}命令无效。
+% \end{function}
+%
+% \begin{function}[added=2021-09-20]{borderwidth}
+% \begin{syntax}
+% borderwidth = \Arg{dimen}
+% \end{syntax}
+% 为\tn{hanzibox}命令设置汉字的边框宽度,默认值为\textbf{0.4pt}。该选项对\tn{hanzidialog}命令无效。
+% \end{function}
+%
+% \begin{function}[added=2021-09-20]{bordercolor}
+% \begin{syntax}
+% bordercolor = \Arg{color}
+% \end{syntax}
+% 为\tn{hanzibox}命令设置汉字的边框颜色,默认值为\textbf{black}。该选项对\tn{hanzidialog}命令无效。
+% \end{function}
+%
+% \subsection{格子样式}
+%
+% \begin{function}[added=2021-09-20]{inner}
+% \begin{syntax}
+% inner = <(none)|star|cross>
+% \end{syntax}
+% 为\tn{hanzibox}命令设置汉字盒子内部装饰为无(none)、米字格(star)或田字格(cross),
+% 默认值为\textbf{none}。该选项对\tn{hanzidialog}命令无效。
+% \end{function}
+%
+% \end{documentation}
+%
+% \StopEventually{}
+%
+% \begin{implementation}
+%
+% \section{代码实现}
+%
+% 本宏包使用 \LaTeX3 语法编写,依赖 \pkg{expl3} 环境,
+% 并需调用 \pkg{l3packages} 中的相关宏包。
+%
+% 按照 \LaTeX3 语法,代码中的空格、换行、回车与制表符完全被忽略,
+% 而下划线“|_|”和冒号“|:|”则可作为一般字母使用。
+% 正常的空格可以使用“|~|”代替;至于 |~| 原来所表示的“带子”,
+% 则要用 \LaTeXe{} 的原始命令 \tn{nobreakspace} 代替。
+%
+% 以下代码中有一些形如 \textcolor[HTML]{2E3191}{\textsf{<*package>}}
+% 的标记,这是 \pkg{DocStrip} 中的“guard”,用来选择性地提取文件。
+% “\textsf{*}”和“\textsf{/}”分别表示该部分的开始和结束。不含
+% “\textsf{*}”和“\textsf{/}”的 guard 出现在行号右侧,它们用来确定
+% 单独一行代码的归属。这些 guard 的颜色深浅不一,用以明确嵌套关系。
+%
+% 另有若干形如 \textcolor[HTML]{AD1457}{\textsf{<@@=hanzibox>}} 的 guard,
+% 它们由 \pkg{l3docstrip} 定义,用来标识名字空间(模块)。
+%
+% \subsection{环境检测与准备}
+%
+% \begin{macrocode}
+%<*package>
+%<@@=hanzibox>
+% \end{macrocode}
+%
+% 载入必要的宏包
+%
+% \begin{macrocode}
+
+\RequirePackage { xtemplate, l3keys2e, xparse }
+
+% \end{macrocode}
+%
+% 检查LaTeX3宏包版本
+%
+% \begin{macrocode}
+% \clist_map_inline:nn { xtemplate, l3keys2e }
+% {
+% \@ifpackagelater {#1} { 2020/07/17 }
+% { } { \msg_error:nnn { hanzibox } { l3-too-old } {#1} }
+% }
+% \msg_new:nnn { hanzibox } { l3-too-old }
+% {
+% Package~ "#1"~ is~ too~ old. \\\\
+% Please~ update~ an~ up-to-date~ version~ of~ the~ bundles \\
+% "l3kernel"~ and~ "l3packages"~ using~ your~ TeX~ package \\
+% manager~ or~ from~ CTAN.
+% }
+%
+% \end{macrocode}
+%
+% 检查编译引擎
+%
+% \begin{macrocode}
+\sys_if_engine_xetex:F
+ {
+ \sys_if_engine_luatex:F
+ {
+ \msg_fatal:nnx { hanzibox } { unsupported-engine }
+ { \c_sys_engine_str }
+ }
+ }
+\msg_new:nnn { hanzibox } { unsupported-engine }
+ {
+ The~ hanzibox~ packages~ requires~ either~ XeTeX~ or~ LuaTeX. \\\\
+ "#1"~ is~ not~ supported~ at~ present.~ You~ must~ change \\
+ your~ typesetting~ engine~ to~ "xelatex"~ or~ "lualatex".
+ }
+
+% \end{macrocode}
+%
+% 载入TikZ, xpinyin宏包
+%
+% \begin{macrocode}
+\RequirePackage { tikz, xpinyin }
+
+% \end{macrocode}
+%
+% 在Expl3中使用TikZ库的补丁
+%
+% \begin{macrocode}
+\RequirePackage{etoolbox}
+\ExplSyntaxOff
+\patchcmd
+ {\pgfutil@InputIfFileExists}
+ {\input #1}
+ {%
+ \@pushfilename
+ \xdef\@currname{#1}%
+ \input #1 %
+ \@popfilename
+ }
+ {}{}
+\ExplSyntaxOn
+\usetikzlibrary{decorations}
+\usetikzlibrary{shapes.geometric}
+\usetikzlibrary{positioning}
+
+% \end{macrocode}
+%
+% TikZ样式设置
+%
+% \begin{macrocode}
+\tikzset{%
+ stretch~dash/.code~args={on~#1~off~#2}{%
+ \tikz@addoption{%
+ \pgfgetpath\currentpath%
+ \pgfprocessround{\currentpath}{\currentpath}%
+ \pgf@decorate@parsesoftpath{\currentpath}{\currentpath}%
+ \pgfmathparse{max(round((\pgf@decorate@totalpathlength-#1)/(#1+#2)),0)}%
+ \let\npattern=\pgfmathresult%
+ \pgfmathparse{\pgf@decorate@totalpathlength/(\npattern*(#1+#2)+#1)}%
+ \let\spattern=\pgfmathresult%
+ \pgfsetdash{{\spattern*#1}{\spattern*#2}}{0pt}%
+ }%
+ }%
+}
+
+% \end{macrocode}
+%
+% \subsection{用户接口}
+%
+% \begin{macro}{\hanzidialog}
+% 手动汉字盒子命令。
+% \begin{macrocode}
+\NewDocumentCommand{\hanzidialog}{ m O{} O{} }
+ {
+ \@@_dialog:nnnn { #1 } { #2 } { #3 }
+ }
+
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\hanzibox}
+% 自动拼音汉字盒子命令。
+% \begin{macrocode}
+\NewDocumentCommand{\hanzibox}{ s O{} m O{} O{} }
+ {
+ \IfBooleanTF{#1}
+ {
+ \@@_star_draw:nnnn { #2 } { #3 } { #4 } { #5 }
+ }{
+ \@@_draw:nnnn { #2 } { #3 } { #4 } { #5 }
+ }
+ }
+
+% \end{macrocode}
+% \end{macro}
+%
+% \subsection{内部变量声明}
+%
+% \begin{variable}{\l_@@_inner_int,
+% \l_@@_border_bool,
+% \l_@@_borderwidth_dim,
+% \l_@@_bordercolor_tl,
+% \l_@@_pinyin_tl,
+% \l_@@_character_tl,
+% \l_@@_translation_tl,
+% \l_@@_pinyin_int,
+% \l_@@_character_int,
+% \l_@@_translation_int
+% }
+% 临时变量。
+% \begin{macrocode}
+\fp_new:N \l_@@_ratio_fp
+\int_new:N \l_@@_inner_int
+\bool_new:N \l_@@_border_bool
+\dim_new:N \l_@@_borderwidth_dim
+\tl_new:N \l_@@_bordercolor_tl
+\tl_new:N \l_@@_pinyin_tl
+\tl_new:N \l_@@_character_tl
+\tl_new:N \l_@@_translation_tl
+\int_new:N \l_@@_pinyin_int
+\int_new:N \l_@@_character_int
+\int_new:N \l_@@_translation_int
+
+% \end{macrocode}
+% \end{variable}
+%
+%
+% \subsection{选项处理}
+%
+% 定义 |hanzibox| 键值类。
+% \begin{macrocode}
+\keys_define:nn { hanzibox }
+ {
+% \end{macrocode}
+%
+% \begin{macro}{ratio}
+% 设置汉字及其拼音和译文的整体缩放比例。
+% \begin{macrocode}
+ ratio .fp_set:N = \l_@@_ratio_fp,
+ ratio .initial:n = 1.0,
+
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{inner}
+% 设置汉字格子内部样式,有空白、米字和十字3种样式,默认为空白样式。
+% \begin{macrocode}
+ inner .choice:,
+ inner .value_required:n = true,
+ inner .choices:nn =
+ { none,star,cross }
+ { \int_set_eq:NN \l_@@_inner_int \l_keys_choice_int },
+ inner .initial:n = none,
+
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{border}
+% 是否绘制汉字盒子边框,默认为空true。
+% \begin{macrocode}
+ border .bool_set:N = \l_@@_border_bool,
+ border .default:n = true,
+ border .initial:n = true,
+
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{borderwidth}
+% 汉字盒子边框宽度,默认为空3 pt。
+% \begin{macrocode}
+ borderwidth .dim_set:N = \l_@@_borderwidth_dim,
+ borderwidth .initial:n = 0.4 pt,
+
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{bordercolor}
+% 汉字盒子边框颜色,默认为black。
+% \begin{macrocode}
+ bordercolor .tl_set:N = \l_@@_bordercolor_tl,
+ bordercolor .initial:n = black,
+
+% \end{macrocode}
+% \end{macro}
+%
+% 处理未知选项。
+% \begin{macrocode}
+ unknown .code:n = { \@@_error:n { unknown-option } }
+ }
+\msg_new:nnn { hanzibox } { unknown-option }
+ { package~ option~ "\l_keys_key_tl"~ is~ unknown. }
+
+% \end{macrocode}
+%
+% \subsection{选项用户接口}
+%
+% \begin{macro}{\hanziboxset}
+% 选项设置用户接口。
+% \begin{macrocode}
+\NewDocumentCommand \hanziboxset { m }
+ { \keys_set:nn { hanzibox } {#1} }
+% \end{macrocode}
+% \end{macro}
+%
+% \subsection{内部函数}
+%
+% \begin{macro}{\@@_dialog:nnnn}
+% 手动汉字盒子排版内部命令,根据选项用TikZ实现绘制。
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_dialog:nnnn #1#2#3
+ {
+ \tl_set:Nx \l_@@_character_tl {#1}
+ \tl_set:Nx \l_@@_pinyin_tl {#2}
+ \tl_set:Nx \l_@@_translation_tl {#3}
+ \scalebox{\fp_use:N \l_@@_ratio_fp}{
+ \tikz[scale=0.5, every~node/.style={inner~sep=0,outer~sep=0}]{%
+ {\node[] at (1,1){\huge{\tl_use:N \l_@@_character_tl}}};
+ {\node[label={\strut\small\textbf{\tl_use:N \l_@@_pinyin_tl}},
+ text~height=1cm] (charr) at (1,1){};}%
+ {\node[align=center] at (1,-0.5)
+ {\strut\small\textit{\tl_use:N \l_@@_translation_tl}};}%
+ }%
+ }%
+ }
+
+% \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\@@_pinyinaux:n}
+% \begin{macro}{\@@_pinyinaux:x}
+% \pkg{xpyinyin}宏包\tn{pinyin} 命令的\LaTeX3{}版本函数及其函数变体。
+% \begin{macrocode}
+\cs_set_eq:NN \@@_pinyinaux:n \pinyin
+\cs_generate_variant:Nn \@@_pinyinaux:n { x }
+% \end{macrocode}
+% \end{macro}
+% \end{macro}
+%
+% \begin{macro}{\@@_draw:nnnn}
+% 自动汉字盒子排版内部命令,根据选项用TikZ实现绘制。
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_draw:nnnn #1#2#3#4
+ {
+% \end{macrocode}
+%
+% 设置盒子外观选项
+%
+% \begin{macrocode}
+ \keys_set:nn { hanzibox } { #1 }
+% \end{macrocode}
+%
+% 将各参数存入token list变量,注意使用x递归展开
+%
+% \begin{macrocode}
+ \tl_set:Nx \l_@@_character_tl {#2}
+ \tl_set:Nx \l_@@_pinyin_tl {#3}
+ \tl_set:Nx \l_@@_translation_tl {#4}
+% \end{macrocode}
+%
+% 计算token list中的内容长度(个数)
+%
+% \begin{macrocode}
+ \int_set:Nn \l_@@_character_int
+ {
+ \tl_count:V \l_@@_character_tl
+ }
+ \int_set:Nn \l_@@_pinyin_int
+ {
+ \tl_count:V \l_@@_pinyin_tl
+ }
+ \int_set:Nn \l_@@_translation_int
+ {
+ \tl_count:V \l_@@_translation_tl
+ }
+
+ \scalebox{\fp_use:N \l_@@_ratio_fp}{
+ \tikz[scale=0.5, every~node/.style={inner~sep=0,outer~sep=0}]{%
+ \int_compare:nNnT { \l_@@_character_int } < {2}
+ {
+ \int_case:nn {\l_@@_inner_int}
+ {
+ {1}{
+ }
+ {2}{
+ \draw[stretch~dash=on~2pt~off~2pt,thin,
+ \tl_use:N \l_@@_bordercolor_tl!50](0,0)--(2,2);%
+ \draw[stretch~dash=on~2pt~off~2pt,thin,
+ \tl_use:N \l_@@_bordercolor_tl!50](0,2)--(2,0);%
+
+ \draw[stretch~dash=on~2pt~off~2pt,thin,
+ \tl_use:N \l_@@_bordercolor_tl!50](0,1)--(2,1);%
+ \draw[stretch~dash=on~2pt~off~2pt,thin,
+ \tl_use:N \l_@@_bordercolor_tl!50](1,0)--(1,2);%
+ }
+ {3}{
+ \draw[stretch~dash=on~2pt~off~2pt,thin,
+ \tl_use:N \l_@@_bordercolor_tl!50](0,1)--(2,1);%
+ \draw[stretch~dash=on~2pt~off~2pt,thin,
+ \tl_use:N \l_@@_bordercolor_tl!50](1,0)--(1,2);%
+ }
+ }
+ }
+ \int_compare:nNnTF { \l_@@_character_int } = {0}
+ {
+ \draw[line~width=\dim_use:N \l_@@_borderwidth_dim,
+ \tl_use:N \l_@@_bordercolor_tl] (0,0) rectangle(2,2);
+ \node[] at (1,1){\huge{\tl_use:N \l_@@_character_tl}};
+ \int_compare:nNnTF { \l_@@_pinyin_int } = {0}
+ { }
+ {
+ \node[label={\strut\small\@@_pinyinaux:x {\l_@@_pinyin_tl}},
+ text~height=1cm] (charr) at (1,1){};
+ }
+ \int_compare:nNnTF { \l_@@_translation_int } = {0}
+ { }
+ {
+ \node[align=center] at (1,-0.5)
+ {\strut\small\textit{\tl_use:N \l_@@_translation_tl}};
+ }
+ }
+ {
+ \bool_if:NTF \l_@@_border_bool
+ {
+ \draw[line~width=\dim_use:N \l_@@_borderwidth_dim,
+ \tl_use:N \l_@@_bordercolor_tl] (0,0) rectangle(2*\l_@@_character_int,2);
+ }{}
+
+ \node[] at (1*\l_@@_character_int,1)
+ {\huge\tl_use:N \l_@@_character_tl};
+
+ \int_compare:nNnTF { \l_@@_pinyin_int } = {0}
+ { }
+ {
+ \node[label={\strut\small\@@_pinyinaux:x {\l_@@_pinyin_tl}},
+ text~height=1cm] (charr) at (1*\l_@@_character_int,1){};
+ }
+
+ \int_compare:nNnTF { \l_@@_translation_int } = {0}
+ { }
+ {
+ \node[align=center] at (1*\l_@@_character_int,-0.5)
+ {\strut\small\textit{\tl_use:N \l_@@_translation_tl}};
+ }
+ }
+ }%
+ }%
+ }
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\@@_star_draw:nnnn}
+% 自动汉字拼音盒子排版内部命令,根据选项用TikZ实现绘制。
+% \begin{macrocode}
+\cs_new_protected:Npn \@@_star_draw:nnnn #1#2#3#4
+ {
+% \end{macrocode}
+%
+% 设置盒子外观选项
+%
+% \begin{macrocode}
+ \keys_set:nn { hanzibox } { #1 }
+% \end{macrocode}
+%
+% 将各参数存入token list变量,注意使用x递归展开
+%
+% \begin{macrocode}
+ \tl_set:Nx \l_@@_character_tl {#2}
+ \tl_set:Nx \l_@@_pinyin_tl {#3}
+ \tl_set:Nx \l_@@_translation_tl {#4}
+% \end{macrocode}
+%
+% 计算token list中的内容长度(个数)
+%
+% \begin{macrocode}
+ \int_set:Nn \l_@@_character_int
+ {
+ \tl_count:V \l_@@_character_tl
+ }
+ \int_set:Nn \l_@@_pinyin_int
+ {
+ \tl_count:V \l_@@_pinyin_tl
+ }
+ \int_set:Nn \l_@@_translation_int
+ {
+ \tl_count:V \l_@@_translation_tl
+ }
+
+ \scalebox{\fp_use:N \l_@@_ratio_fp}{
+ \tikz[scale=0.5, every~node/.style={inner~sep=0,outer~sep=0}]{%
+ \int_compare:nNnT { \l_@@_character_int } < {2}
+ {
+ \int_case:nn {\l_@@_inner_int}
+ {
+ {1}{
+ }
+ {2}{
+ \draw[stretch~dash=on~2pt~off~2pt,thin, yshift=-0.5cm,
+ \tl_use:N \l_@@_bordercolor_tl!50](0,0)--(2,2);%
+ \draw[stretch~dash=on~2pt~off~2pt,thin, yshift=-0.5cm,
+ \tl_use:N \l_@@_bordercolor_tl!50](0,2)--(2,0);%
+
+ \draw[stretch~dash=on~2pt~off~2pt,thin, yshift=-0.5cm,
+ \tl_use:N \l_@@_bordercolor_tl!50](0,1)--(2,1);%
+ \draw[stretch~dash=on~2pt~off~2pt,thin, yshift=-0.5cm,
+ \tl_use:N \l_@@_bordercolor_tl!50](1,0)--(1,2);%
+ }
+ {3}{
+ \draw[stretch~dash=on~2pt~off~2pt,thin, yshift=-0.5cm,
+ \tl_use:N \l_@@_bordercolor_tl!50](0,1)--(2,1);%
+ \draw[stretch~dash=on~2pt~off~2pt,thin, yshift=-0.5cm,
+ \tl_use:N \l_@@_bordercolor_tl!50](1,0)--(1,2);%
+ }
+ }
+ }
+ \int_compare:nNnTF { \l_@@_character_int } = {0}
+ {
+ \draw[line~width=\dim_use:N \l_@@_borderwidth_dim, yshift=-0.5cm,
+ \tl_use:N \l_@@_bordercolor_tl] (0,0) rectangle(2,2);
+ \node[] at (1,1){\huge{\xpinyin*[vsep=0.85 cm]{\tl_use:N \l_@@_character_tl}}};
+ \int_compare:nNnTF { \l_@@_translation_int } = {0}
+ { }
+ {
+ \node[align=center] at (1,-1.0)
+ {\strut\small\textit{\tl_use:N \l_@@_translation_tl}};
+ }
+ }
+ {
+ \bool_if:NTF \l_@@_border_bool
+ {
+ \draw[line~width=\dim_use:N \l_@@_borderwidth_dim, yshift=-0.5cm,
+ \tl_use:N \l_@@_bordercolor_tl] (0,0) rectangle(2*\l_@@_character_int,2);
+ }{}
+
+ \node[] at (1*\l_@@_character_int,1)
+ {\huge{\xpinyin*[vsep=0.90 cm]{\tl_use:N \l_@@_character_tl}}};
+
+ \int_compare:nNnTF { \l_@@_translation_int } = {0}
+ { }
+ {
+ \node[align=center] at (1*\l_@@_character_int,-1.0)
+ {\strut\small\textit{\tl_use:N \l_@@_translation_tl}};
+ }
+ }
+ }%
+ }%
+ }
+% \end{macrocode}
+% \end{macro}
+%
+% \begin{macrocode}
+%</package>
+% \end{macrocode}
+%
+% \end{implementation}
+%
+% \Finale
+%
+\endinput
diff --git a/macros/xetex/latex/hanzibox/hanzibox.pdf b/macros/xetex/latex/hanzibox/hanzibox.pdf
new file mode 100644
index 0000000000..23e93b20dc
--- /dev/null
+++ b/macros/xetex/latex/hanzibox/hanzibox.pdf
Binary files differ