From fb4909f4112d8b778570cf1c2e25ca18c63bc1ca Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Fri, 10 Jun 2022 03:02:43 +0000 Subject: CTAN sync 202206100302 --- macros/luatex/generic/luakeys/README.md | 114 ++ macros/luatex/generic/luakeys/luakeys-debug.sty | 2 +- macros/luatex/generic/luakeys/luakeys-debug.tex | 7 +- macros/luatex/generic/luakeys/luakeys-doc.pdf | Bin 169502 -> 254353 bytes macros/luatex/generic/luakeys/luakeys-doc.tex | 1398 +++++++++++++++++------ macros/luatex/generic/luakeys/luakeys.lua | 1269 +++++++++++++------- macros/luatex/generic/luakeys/luakeys.sty | 2 +- 7 files changed, 2044 insertions(+), 748 deletions(-) (limited to 'macros/luatex/generic/luakeys') diff --git a/macros/luatex/generic/luakeys/README.md b/macros/luatex/generic/luakeys/README.md index 10793d353e..ca73100b64 100644 --- a/macros/luatex/generic/luakeys/README.md +++ b/macros/luatex/generic/luakeys/README.md @@ -30,6 +30,120 @@ This work has the LPPL maintenance status `maintained`. The Current Maintainer of this work is Josef Friedrich. +## Documentation + +### Key-value pair definitions + +```lua +local defs = { + key = { + -- Allow different key names. + -- or a single string: alias = 'k' + alias = { 'k', 'ke' }, + + -- The key is always included in the result. If no default value is + -- definied, true is taken as the value. + always_present = false, + + -- Only values listed in the array table are allowed. + choices = { 'one', 'two', 'three' }, + + -- Possible data types: boolean, dimension, integer, number, string + data_type = 'string', + + default = true, + + -- The key belongs to a mutually exclusive group of keys. + exclusive_group = 'name', + + -- > \MacroName + macro = 'MacroName', -- > \MacroName + + -- See http://www.lua.org/manual/5.3/manual.html#6.4.1 + match = '^%d%d%d%d%-%d%d%-%d%d$', + + -- The name of the key, can be omitted + name = 'key', + opposite_keys = { [true] = 'show', [false] = 'hide' }, + process = function(value, input, result, unknown) + return value + end, + required = true, + sub_keys = { key_level_2 = { } }, + } +} +``` + +### Parser options (opts) + +```lua +local opts = { + -- Automatically convert dimensions into scaled points (1cm -> 1864679). + convert_dimensions = false, + + -- Print the result table to the console. + debug = false, + + -- The default value for naked keys (keys without a value). + default = true, + + -- A table with some default values. The result table is merged with + -- this table. + defaults = { key = 'value' }, + + -- Key-value pair definitions. + defs = { key = { default = 'value' } }, + + -- lower, snake, upper + format_keys = { 'snake' }, + + -- Listed in the order of execution + hooks = { + kv_string = function(kv_string) + return kv_string + end, + + -- Visit all key-value pairs recursively. + keys_before_opts = function(key, value, depth, current, result) + return key, value + end, + + -- Visit the result table. + result_before_opts = function(result) + end, + + -- Visit all key-value pairs recursively. + keys_before_def = function(key, value, depth, current, result) + return key, value + end, + + -- Visit the result table. + result_before_def = function(result) + end, + + -- Visit all key-value pairs recursively. + keys = function(key, value, depth, current, result) + return key, value + end, + + -- Visit the result table. + result = function(result) + end, + }, + + -- If true, naked keys are converted to values: + -- { one = true, two = true, three = true } -> { 'one', 'two', 'three' } + naked_as_value = false, + + -- Throw no error if there are unknown keys. + no_error = false, + + -- { key = { 'value' } } -> { key = 'value' } + unpack = false, +} +local result = luakeys.parse('one,two,three', opts) +``` + ## Tasks ### Installing diff --git a/macros/luatex/generic/luakeys/luakeys-debug.sty b/macros/luatex/generic/luakeys/luakeys-debug.sty index 83ae86b08b..7fcb483741 100644 --- a/macros/luatex/generic/luakeys/luakeys-debug.sty +++ b/macros/luatex/generic/luakeys/luakeys-debug.sty @@ -17,6 +17,6 @@ % luakeys-debug.sty and luakeys-debug.tex. \NeedsTeXFormat{LaTeX2e} -\ProvidesPackage{luakeys-debug}[2022/04/04 v0.5 Debug package for luakeys.] +\ProvidesPackage{luakeys-debug}[2022/06/09 v0.6 Debug package for luakeys.] \input luakeys-debug.tex diff --git a/macros/luatex/generic/luakeys/luakeys-debug.tex b/macros/luatex/generic/luakeys/luakeys-debug.tex index ffc449ac9b..e7758cce0a 100644 --- a/macros/luatex/generic/luakeys/luakeys-debug.tex +++ b/macros/luatex/generic/luakeys/luakeys-debug.tex @@ -37,7 +37,10 @@ }% \def\luakeysdebug@parse@options#1{ \directlua{ - luakeys.save('debug_options', luakeys.parse('#1')) + luakeys.save( + 'debug_options', + luakeys.parse('#1', { format_keys = { 'snake', 'lower' } }) + ) } }% \def\luakeysdebug@output#1{ @@ -47,7 +50,7 @@ \directlua{ local result = luakeys.parse('\luaescapestring{\unexpanded{#1}}', luakeys.get('debug_options')) tex.print(luakeys.stringify(result, true)) - luakeys.print(result) + luakeys.debug(result) } } }% diff --git a/macros/luatex/generic/luakeys/luakeys-doc.pdf b/macros/luatex/generic/luakeys/luakeys-doc.pdf index 07f14d206b..6fe42295c4 100644 Binary files a/macros/luatex/generic/luakeys/luakeys-doc.pdf and b/macros/luatex/generic/luakeys/luakeys-doc.pdf differ diff --git a/macros/luatex/generic/luakeys/luakeys-doc.tex b/macros/luatex/generic/luakeys/luakeys-doc.tex index 3f7d1b0ea1..bc9a55f632 100644 --- a/macros/luatex/generic/luakeys/luakeys-doc.tex +++ b/macros/luatex/generic/luakeys/luakeys-doc.tex @@ -1,6 +1,6 @@ \documentclass{ltxdoc} -\usepackage{hyperref} +\usepackage[hidelinks]{hyperref} \EnableCrossrefs \CodelineIndex \RecordChanges @@ -17,8 +17,24 @@ \setminted{ breaklines=true, fontsize=\footnotesize, + style=manni, } \def\lua#1{\mintinline{lua}|#1|} +\def\latex#1{\mintinline{latex}|#1|} + +\NewDocumentCommand { \InputLatexExample } { O{} m } { + \begin{mdframed} + \inputminted[linenos=false,#1]{latex}{examples/#2} + \end{mdframed} +} + +\NewDocumentCommand { \InputLuaExample } { O{} m } { + \begin{mdframed} + \inputminted[linenos=false,#1]{lua}{examples/#2} + \end{mdframed} +} + +\catcode`_=12 \begin{document} @@ -30,19 +46,13 @@ \url{josef@friedrich.rocks}\\% \href{https://github.com/Josef-Friedrich/luakeys}{github.com/Josef-Friedrich/luakeys}% } -\date{v0.5 from 2022/04/04} +\date{v0.6 from 2022/06/09} \maketitle \vfill -%level1={level2={level3={dim=1cm,bool=true,num=-0.001,str=lua}}} - -\begin{minted}{lua} -local luakeys = require('luakeys') -local kv = luakeys.parse('level1={level2={level3={dim=1cm,bool=true,num=-0.001,str=lua}}}') -luakeys.print(kv) -\end{minted} +\InputLuaExample[firstline=4,lastline=7]{first-page.lua} \noindent Result: @@ -52,149 +62,1016 @@ Result: { ['level1'] = { ['level2'] = { - ['level3'] = { - ['dim'] = 1864679, - ['bool'] = true, - ['num'] = -0.001 - ['str'] = 'lua', - } + ['naked'] = true, + ['dim'] = 1864679, + ['bool'] = false, + ['num'] = -0.001, + ['str'] = 'lua,{}', } } } \end{minted} \end{center} -\vfill +\vfill + +\strut + +\newpage + +\tableofcontents + +\newpage + +\section{Introduction} + +\noindent +% |luakeys| ist ein Lua-Modul / Lua\TeX package, das wie die Pakete ... +% Schlüssel-Wert-Optionen analysieren kann. +|luakeys| is a Lua module / Lua\TeX package that can parse key-value +options like the \TeX{} packages +\href{https://www.ctan.org/pkg/keyval}{keyval}, +\href{https://www.ctan.org/pkg/kvsetkeys}{kvsetkeys}, +\href{https://www.ctan.org/pkg/kvoptions}{kvoptions}, +\href{https://www.ctan.org/pkg/xkeyval}{xkeyval}, +\href{https://www.ctan.org/pkg/pgfkeys}{pgfkeys} etc. +% +% |luakeys|, erfüllt diese Aufgabe jedoch, indem es die Sprache Lua +% verwendet und nicht auf \TeX{} angewiesen ist. +|luakeys|, however, accomplishes this task by using the Lua language and +doesn’t rely on \TeX{}. +% +% Deshalb kann dieses Paket nur mit der \TeX{}-Engine Lua\TeX{} +% verwendet werden. +Therefore this package can only be used with the +\TeX{} engine Lua\TeX{}. +% +% Da |luakeys| \href{http://www.inf.puc-rio.br/~roberto/lpeg/}{LPeg} +% verwendet, sollte der Parsing-Mechanismus ziemlich robust sein. +Since |luakeys| uses +\href{http://www.inf.puc-rio.br/~roberto/lpeg/}{LPeg}, the parsing +mechanism should be pretty robust. + +The TUGboat article +\href{http://www.tug.org/tugboat/tb30-1/tb94wright-keyval.pdf} +{“Implementing key–value input: An introduction” (Volume 30 (2009), No. 1)} +by \emph{Joseph Wright} and \emph{Christian Feuersänger} gives a good +overview of the available key-value packages. + +This package would not be possible without the article +\href{https://tug.org/TUGboat/tb40-2/tb125menke-lpeg.pdf} +{“Parsing complex data formats in LuaTEX with LPEG” (Volume 40 (2019), No. 2)}. + +%----------------------------------------------------------------------- +% +%----------------------------------------------------------------------- + +\section{How the package is loaded} + +%% +% +%% + +\subsection{Using the Lua module \texttt{luakeys.lua}} + +% Die Kernfunktionalität dieses Pakets ist in Lua realisiert. +The core functionality of this package is realized in Lua. +% +% Sie können also |luakeys| auch ohne die Wrapper-Dateien +% \texttt{luakeys.sty} und \texttt{luakeys.tex} verwenden. +So you can use |luakeys| even without using the wrapper files +|luakeys.sty| and |luakeys.tex|. + +\InputLatexExample{loading/lua.tex} + +%% +% +%% + +\subsection{Using the Lua\LaTeX{} wrapper \texttt{luakeys.sty}} + +% Der Paketmanager MikTeX lädt beispielsweise Pakete erst bei Bedarf +% herunter. +For example, the MiK\TeX{} package manager downloads packages only when +needed. +% +% Es wurde berichtet, dass dieser automatische Download nur mit Hilfe der +% Hüll-Dateien funktioniert. +It has been reported that this automatic download only works with this +wrapper files. +% +% Wahrscheinlich hält MiK\TeX nach einem Auftreten des LaTeX macros +% “\latex{\usepackage{luakeys}} ”Ausschau. +Probably MiK\TeX{} is searching for an occurrence of the \LaTeX{} macro +“\latex{\usepackage{luakeys}}”. + +% Die mitgelieferte Lua\LaTeX{}-Datei ist recht klein: +The supplied Lua\LaTeX{} file is quite small: + +\begin{minted}{latex} +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage{luakeys} +\directlua{luakeys = require('luakeys')} +\end{minted} + +\noindent +It loads the Lua module into the global variable \texttt{luakeys}. + +\InputLatexExample{loading/latex.tex} + +%% +% +%% + +\subsection{Using the plain Lua\TeX{} wrapper \texttt{luakeys.tex}} + +Even smaller is the file \texttt{luakeys.tex}. It consists of only one +line: + +\begin{minted}{latex} +\directlua{luakeys = require('luakeys')} +\end{minted} + +\noindent +It does the same as the Lua\LaTeX{} wrapper and loads the Lua module +\texttt{luakeys.lua} into the global variable \texttt{luakeys}. + +\InputLatexExample{loading/tex.tex} + +\section{Lua interface / API} + +To learn more about the individual functions (local functions), please +read the \href{https://josef-friedrich.github.io/luakeys/}{source code +documentation}, which was created with +\href{http://stevedonovan.github.io/ldoc/}{LDoc}. The Lua module exports +this functions and tables: + +\InputLuaExample[firstline=3,lastline=12]{export.lua} + +\subsection{Lua indentifier names} + +% Das Projekt verwendet einige wenige Abkürzungen für Variablennamen, +% die hoffentlich für externe Leser eindeutig und bekannt sind. +The project uses a few abbreviations for variable names that are +hopefully unambiguous and familiar to external readers. + +\begin{center} +\begin{tabular}{lll} +Abbreviation & spelled out & Example \\\hline +\lua{kv_string} & Key-value string & \lua{'key=value'} \\ +\lua{opts} & Options (for the parse function) & \lua{ { no_error = false } } \\ +\lua{defs} & Definitions \\ +\lua{def} & Definition \\ +\lua{attr} & Attributes (of a definition) \\ +\end{tabular} +\end{center} + +\noindent +% Diese nicht abgekürzten Variablennamen werden häufig verwendet. +These unabbreviated variable names are commonly used. +\begin{center} +\begin{tabular}{ll} + +\lua{result} & +% Das Endergebnis aller einzelnen Übersetzungs- und Normalisierungsschritte +The final result of all individual parsing and normalization steps. \\ + +\lua{unknown} & +% Ein Tabelle mit unbekannten, nicht definierten Schlüssel-Wert-Paaren +A table with unknown, undefinied key-value pairs. \\ + +\lua{raw} & +% Das unbearbeitete, rohe Ergebnis der LPeg-Syntaxanalyse. +The raw result of the Lpeg grammar parser. \\ +\end{tabular} +\end{center} + +%% +% +%% + +\subsection{Function \texttt{parse(kv_string, opts): result, unknown, raw}} +\label{parse} + +% Die Function parse ist die wichtigste Funktion des Pakets. +The function \lua{parse(kv_string, opts)} is the most important function +of the package. +% Sie konvertiert eine Schlüssel-Wert-Zeichenkette in eine Lua Tabelle. +It converts a key-value string into a Lua table. + +\begin{minted}{latex} +\newcommand{\mykeyvalcmd}[2][]{ + \directlua{ + result = luakeys.parse('#1') + luakeys.debug(result) + } + #2 +} +\mykeyvalcmd[one=1]{test} +\end{minted} + +\noindent +In plain \TeX: + +\begin{minted}{latex} +\def\mykeyvalcommand#1{ + \directlua{ + result = luakeys.parse('#1') + luakeys.debug(result) + } +} +\mykeyvalcmd{one=1} +\end{minted} + +\subsection{Options to configure the \texttt{parse} function} + +\noindent +The \lua{parse} function can be called with an options table. This +options are supported: + +\InputLuaExample[firstline=5,lastline=69]{opts/all-opts.lua} + +\noindent +The options can also be set globally using the exported table +|opts|: + +\InputLuaExample[firstline=4,lastline=4]{opts/exported-default-opts.lua} + +\InputLuaExample[firstline=10,lastline=11]{opts/exported-default-opts.lua} + +%% +% +%% + +\subsubsection{Option “\texttt{convert_dimensions}”} + +If you set the option \lua{convert_dimensions} to \lua{true}, |luakeys| +detects the \TeX{} dimensions and converts them into scaled points using +the function \lua{tex.sp(dim)}. + +\InputLuaExample[firstline=4,lastline=7]{opts/convert-dimensions.lua} + +\noindent +By default the dimensions are not converted into scaled points. + +\InputLuaExample[firstline=13,lastline=18]{opts/convert-dimensions.lua} + +\noindent +If you want to convert a scale point into a unit string you can use the module +\href{https://raw.githubusercontent.com/latex3/lualibs/master/lualibs-util-dim.lua}{lualibs-util-dim.lua}. + +\begin{minted}{lua} +require('lualibs') +tex.print(number.todimen(tex.sp('1cm'), 'cm', '%0.0F%s')) +\end{minted} + +%% +% +%% + +\subsubsection{Option “\texttt{debug}”} + +If the option “debug” is set to ture, the result table is printed to the +console. + +\begin{minted}{latex} +\documentclass{article} +\usepackage{luakeys} +\begin{document} +\directlua{ + luakeys.parse('one,two,three', { debug = true }) +} +debug +\end{document} +\end{minted} + +\begin{verbatim} +This is LuaHBTeX, Version 1.15.0 (TeX Live 2022) +... +(./debug.aux) (/usr/local/texlive/texmf-dist/tex/latex/base/ts1cmr.fd) +{ + ['three'] = true, + ['two'] = true, + ['one'] = true, +} + [1{/usr/ +local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] (./debug.aux) +) +... +Transcript written on debug.log. +\end{verbatim} + +%% +% +%% + +\subsubsection{Option “\texttt{default}”} +\label{option-default} + +% Mit der Option \lua{default} kann angegeben werden, welchen Wert +% nackte Schlüssel (Schlüssel ohne Wert) erhalten. Diese Option hat +% keinen Einfluss auf Schlüssel mit Werten. +The option \lua{default} can be used to specify which value naked keys +(keys without a value) get. This option has no influence on keys with +values. + +\InputLuaExample[firstline=4,lastline=5]{opts/default.lua} + +\noindent +% Standardmäßig erhalten nackte Schlüssel den Wert \lua{true}. +By default, naked keys get the value \lua{true}. + +\InputLuaExample[firstline=11,lastline=12]{opts/default.lua} + +%% +% +%% + +\subsubsection{Option “\texttt{defaults}”} +\label{options-defaults} + +% Mit der Attribut „defaults“ kann nicht nur ein einiger Standardwert +% angegeben werden, sondern eine ganze Tabelle mit Standardwerten. +The option “defaults” can be used to specify not only one default value, +but a whole table of default values. +% Die Ergebnistabelle wird mit der Tabelle bestehend aus Standardwerten +% vereinigt. +The result table is merged into the defaults table. +% Werte aus der Tabelle mit Standardwerten werden von Werten der +% Ergebnistabelle überschrieben. +Values in the defaults table are +overwritten by values in the result table. + +\InputLuaExample[firstline=4,lastline=7]{opts/defaults.lua} + +%% +% +%% + +\subsubsection{Option “\texttt{defs}”} + +% Für mehr Information wie Schlüssel definiert werden, lesen sie das +% kapitel 3.2 +For more informations on how keys are defined, see section \ref{define}. +% Wenn sie die Option \lua{defs} verwenden, können sie auf den +% Aufruf der Funktion \lua{define} verzichten. +If you use the \lua{defs} option, you don't need to call the +\lua{define} function. +% +% Anstatt +Instead of ... + +\InputLuaExample[firstline=4,lastline=5]{opts/defs.lua} + +\noindent +% können wir schreiben .. +we can write ... + +\InputLuaExample[firstline=11,lastline=13]{opts/defs.lua} + +%% +% +%% + +\subsubsection{Option “\texttt{format_keys}”} + +\begin{description} +\item[lower] \strut + +\InputLuaExample[firstline=4,lastline=5]{opts/format-keys.lua} + +\item[snake] \strut + +\InputLuaExample[firstline=11,lastline=12]{opts/format-keys.lua} + +\item[upper] \strut + +\InputLuaExample[firstline=18,lastline=19]{opts/format-keys.lua} +\end{description} + +%% +% +%% + +\subsubsection{Option “\texttt{hooks}”} + +% Die folgenden Hooks bzw. Callback-Funktionen ermöglichen es in den +% Verarbeitungsprozess der \lua{parse}-Function einzugreifen. +The following hooks or callback functions allow to intervene in the +processing of the \lua{parse} function. +% +% Die Funktionen sind in der Verarbeitungsreihenfolge aufgelistet. +The functions are listed in processing order. +% +% \lua{*_before_opts} bedeutet, dass die Hooks nach der LPeg Syntaxanalyse +% und vor dem Anwenden der Optionen ausgeführt +\lua{*_before_opts} means that the hooks are executed after the LPeg +syntax analysis and before the options are applied. +% +% Die Hooks \lua{*_before_defs} werden vor dem Anwenden der +% Schlüssel-Wert-Definitionen ausgeführt +The \lua{*_before_defs} hooks are executed before applying the key value +definitions. + +\def\TmpSignature#1{ + { + \scriptsize\texttt{ = #1} + } +} + +\def\TmpKeySignature{ + \TmpSignature{function(key, value, depth, current, result): key, value} +} +\def\TmpResultSignature{ + \TmpSignature{function(result): void} +} + +\begin{enumerate} +\item \lua{kv_string} \TmpSignature{function(kv_string): kv_string} +\item \lua{keys_before_opts} \TmpKeySignature +\item \lua{result_before_opts} \TmpResultSignature +\item \lua{keys_before_def} \TmpKeySignature +\item \lua{result_before_def} \TmpResultSignature +\item (\lua{process}) (has to be definied using defs, see \ref{attr-process}) +\item \lua{keys} \TmpKeySignature +\item \lua{result} \TmpResultSignature +\end{enumerate} + +\paragraph{\texttt{kv_string}} + +% Der Hook \lua{kv_string} wird als erste +% der Hook-Funktionen noch vor der LPeg-Syntax-Analyse aufgerufen. +The \lua{kv_string} hook is called as the first of the hook functions +before the LPeg syntax parser is executed. + +\InputLuaExample[firstline=4,lastline=11]{hooks/kv-string.lua} + +\paragraph{\texttt{keys_*}} + +% Die Hooks \lua{keys_*} werden rekursiv auf jeden Schlüssel in der +% aktuellen Ergebnistabelle aufgerufen. +The hooks \lua{keys_*} are called recursively on each key in the current +result table. +% Die Hook-Funktion muss zwei Werte zurückgeben: \lua{key, value} +The hook function must return two values: \lua{key, value}. +% +% Das folgende Beispiel gibt \lua{key} und \lua{value} unverändert zurück, +% sodass die Ergebnistabelle nicht verändert wird. +The following example returns \lua{key} and \lua{value} unchanged, so +the result table is not changed. + +\InputLuaExample[firstline=4,lastline=11]{hooks/keys-unchanged.lua} + +\noindent +% Das nächste Beispiel demonstriert den dritten Parameter \lua{depth} +% der Hook-Funktion. +The next example demonstrates the third parameter \lua{depth} of the +hook function. + +\InputLuaExample[firstline=4,lastline=16]{hooks/keys-depth.lua} + +\paragraph{\texttt{result_*}} + +% Die Hooks \lua{result_*} werden einmal mit der aktuellen +% Ergebnistabelle als Parameter aufgerufen. +The hooks \lua{result_*} are called once with the current result table +as a parameter. + +%% +% +%% + +\subsubsection{Option “\texttt{naked_as_value}”} + +% Mit Hilfe der Option \lua{naked_as_value} werden nackte Schlüssel +% nicht mit einem Standardwert versehen, sondern als Werte in die +% Lua-Tabelle abgelegt. +With the help of the option \lua{naked_as_value}, naked keys are not +given a default value, but are stored as values in a Lua table. + +\InputLuaExample[firstline=4,lastline=5]{opts/naked-as-value.lua} + +\noindent +If we set the option \lua{naked_as_value} to \lua{true}: + +\InputLuaExample[firstline=11,lastline=14]{opts/naked-as-value.lua} + +%% +% +%% + +\subsubsection{Option “\texttt{no_error}”} + +% Standardmaßig wirft parse-Funktion einen Fehler, wenn es unbekannte +% Schlüssel gibt. +By default the parse function throws an error if there are unknown keys. +% Mit Hilfe der Option \lua{no_error} kann dies unterbunden werden. +This can be prevented with the help of the \lua{no_error} option. + +\InputLuaExample[firstline=5,lastline=6]{opts/no-error.lua} + +\noindent +If we set the option \lua{no_error} to \lua{true}: + +\InputLuaExample[firstline=9,lastline=10]{opts/no-error.lua} + +%% +% +%% + +\subsubsection{Option “\texttt{unpack}”} + +% Mit Hilfe der Option \lua{unpack} werden alle Tabellen, die nur aus +% einem einzigen nackten Schlüssel bzw. einen einzigen alleinstehenden +% Wert bestehen, aufgelöst. +With the help of the option \lua{unpack}, all tables that consist of +only one a single naked key or a single standalone value are unpacked. + +\InputLuaExample[firstline=4,lastline=5]{opts/unpack.lua} + +\InputLuaExample[firstline=11,lastline=12]{opts/unpack.lua} + +%% +% +%% + +\subsection{Function \texttt{define(defs, opts): parse}} +\label{define} + +The \lua{define} function returns a \lua{parse} function (see +\ref{parse}). +The name of a key can be specified in three ways: + +\begin{enumerate} +\item as a string. +\item as a key in a Lua table. The definition of the corresponding +key-value pair is then stored under this key. +\item by the “name” attribute. +\end{enumerate} + +\InputLuaExample[firstline=4,lastline=16]{functions/define.lua} + +\noindent +For nested definitions, only the last two ways of specifying the key +names can be used. + +\InputLuaExample[firstline=26,lastline=33]{functions/define.lua} + +%----------------------------------------------------------------------- +% +%----------------------------------------------------------------------- + +\subsection{Attributes to define a key-value pair} + +% Die Definition eines Schlüssel-Wert-Paares kann mit Hilfe von +% verschiedenen Attributen vorgenommen werden. +The definition of a key-value pair can be made with the help of various +attributes. +% +% Der Name „Attribut“ für eine Option, einen Schlüssel, eine Eigenschaft +% (um nur einige Benennungsmöglichkeiten aufzuzählen) zur +% Schlüssel-Definition, wurde bewusst gewählt, um sie von den Optionen +% der Funktion \lua{parse} zu unterscheiden. +The name \emph{“attribute”} for an option, a key, a property ... (to +list just a few naming possibilities) to define keys, was deliberately +chosen to distinguish them from the options of the \lua{parse} function. +% +% Das folgende Codebeispiel listet alle Attribute auf, die verwendet +% werden können, um Schlüssel-Wert-Paare zu definieren. +The code example below lists all the attributes that can be used to +define key-value pairs. + +\InputLuaExample[firstline=5,lastline=41]{defs/all-attrs.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{alias}”} + +With the help of the \lua{alias} attribute, other key names can be used. +The value is always stored under the original key name. A single alias +name can be specified by a string ... + +\InputLuaExample[firstline=4,lastline=7]{defs/attrs/alias.lua} + +\noindent +multiple aliases by a list of strings. + +\InputLuaExample[firstline=13,lastline=16]{defs/attrs/alias.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{always_present}”} + +% Die Option \lua{default} wird nur bei nackten Schlüsseln verwendet. +The \lua{default} attribute is used only for naked keys. + +\InputLuaExample[firstline=4,lastline=5]{defs/attrs/always-present.lua} + +\noindent +% Wird die Option \lua{always_present} auf wahr gesetzt, wird der +% Schlüssel immer ins Ergebnis mit übernommen. +If the attribute \lua{always_present} is set to true, the key is always +included in the result. If no default value is definied, true is taken +as the value. + +\InputLuaExample[firstline=11,lastline=12]{defs/attrs/always-present.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{choices}”} + +% source: Python argparse documentation. +Some key values should be selected from a restricted set of choices. +These can be handled by passing an array table containing choices. + +\InputLuaExample[firstline=4,lastline=5]{defs/attrs/choices.lua} + +\noindent +When the key-value pair is parsed, values will be checked, and an error +message will be displayed if the value was not one of the acceptable +choices: + +\InputLuaExample[firstline=13,lastline=15]{defs/attrs/choices.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{data_type}”} + +% source: Python argparse documentation. +The \lua{data_type} attribute allows type-checking and type conversions to +be performed. +% +% Folgende Datentypen werden unterstützt: +The following data types are supported: +\lua{'boolean'}, +\lua{'dimension'}, +\lua{'integer'}, +\lua{'number'}, +\lua{'string'}. +% +% Bei den drei Datentypen integer, number, dimension kann eine +% Typenumwandlung scheitern. +A type conversion can fail with the three data types +\lua{'dimension'}, +\lua{'integer'}, +\lua{'number'}. +% +% Dann wird eine Fehlermeldung ausgegeben. +Then an error message is displayed. + +\InputLuaExample[firstline=4,lastline=8]{defs/attrs/data-type.lua} +\InputLuaExample[firstline=11,lastline=15]{defs/attrs/data-type.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{default}”} + +% Verwenden Sie das Attribut „\lua{default}“, um für jeden nackten Schlüssel +% einzeln einen Standardwert bereit zu stellen. +Use the \lua{default} attribute to provide a default value for each naked +key individually. +% +% Mit der globalen \lua{default} Option kann für alle nackten Schlüssel ein +% Standardwert vorgegeben werden. +With the global \lua{default} attribute (\ref{option-default}) a default +value can be specified for all naked keys. + +\InputLuaExample[firstline=4,lastline=9]{defs/attrs/default.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{exclusive_group}”} + +% Alle Schlüssel, die der gleichen ausschließenden Gruppe angehören, +% dürfen nicht gemeinsam angegeben werden. +All keys belonging to the same exclusive group must not be specified +together. +% +% Nur ein Schlüssel aus dieser Gruppe ist erlaubt. +Only one key from this group is allowed. +% +% Als Name für diese ausschließende Gruppe kann irgend ein beliebiger +% Wert verwendet werden. +Any value can be used as a name for this exclusive group. + +\InputLuaExample[firstline=4,lastline=9]{defs/attrs/exclusive-group.lua} + +% Werden mehrer Schlüssel der Gruppe angegeben, so wird eine +% Fehlermeldung geworfen. +If more than one key of the group is specified, an error message is +thrown. + +\InputLuaExample[firstline=21,lastline=23]{defs/attrs/exclusive-group.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{opposite_keys}”} + +% Die Option \lua{opposite_keys} ermöglicht es, gegensätzliche (nackte) +% Schlüssel in Wahrheitswerte umzuwandeln und diesen Wahrheitswert unter +% einem Zielschlüssel zu speichern. +The \lua{opposite_keys} attribute allows to convert opposite (naked) keys +into a boolean value and store this boolean under a target key. +% +% Lua erlaubt es in Tabellen Wahrheitswerte als Schlüssel zu verwenden. +% Es müssen jedoch eckige Klammern verwendet werden. +Lua allows boolean values to be used as keys in tables. +% +% Die Wahrheitswerte müssen jedoch in eckige Klammern geschrieben werden. +However, the boolean values must be written in square brackets, e. g. +\lua{{ opposite_keys = { [true] = 'show', [false] = 'hide' } }}. +% +% Beispiele für gegensätzliche Schlüssel sind: +Examples of opposing keys are: \lua{show} and \lua{hide}, \lua{dark} and +\lua{light}, \lua{question} and \lua{solution}. +% +% Das untenstehende Beispiel verwendet als gegensätzliches Schlüsselpaar +% die Schlüssel \lua{show} und \lua{hide}. +The example below uses the \lua{show} and \lua{hide} keys as the +opposite key pair. +% +% Wird der Schlüssel \lua{show} von der Funktion \lua{parse} gelesen, +% dann erhält der Zielschlüssel \lua{visibility} den Wert \lua{true}. +If the key \lua{show} is parsed by the \lua{parse} function, then the +target key \lua{visibility} receives the value \lua{true}. + +\InputLuaExample[firstline=4,lastline=7]{defs/attrs/opposite-keys.lua} + +% Wird der Schlüssel \lua{hide} gelesen, dann \lua{falsch}. +\noindent +If the key \lua{hide} is parsed, then \lua{false}. + +\InputLuaExample[firstline=13,lastline=13]{defs/attrs/opposite-keys.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{macro}”} + +The attribute \texttt{macro} stores the value in a \TeX{} macro. + +\begin{minted}{lua} +local parse = luakeys.define({ + key = { + macro = 'MyMacro' + } +}) +parse('key=value') +\end{minted} + +\begin{minted}{latex} +\MyMacro % expands to “value” +\end{minted} + +%% +% +%% + +\subsubsection{Attribute “\texttt{match}”} + +% Der Wert des Schlüssel wird der Lua Funktion übergeben +The value of the key is passed to the Lua function +\lua{string.match(value, match)} ((\url{http://www.lua.org/manual/5.3/manual.html#pdf-string.match})). +% Werfe einen Blick in das Lua-Handbuch, wie man Patterns schreibt. +Take a look at the Lua manual on how to write patterns +(\url{http://www.lua.org/manual/5.3/manual.html#6.4.1}) + +\InputLuaExample[firstline=4,lastline=6]{defs/attrs/match.lua} + +\noindent +% Kann das Pattern im Wert nicht gefunden werden, wird eine +% Fehlermeldung ausgegeben. +If the pattern cannot be found in the value, an error message is issued. + +\InputLuaExample[firstline=14,lastline=17]{defs/attrs/match.lua} + +\noindent +% Der Schlüssel erhält das Ergebnis der Funktion \lua{string.match(value, +% match)}, dass bedeutet, dass der ursprüngliche Wert unter +% Umständen nicht vollständig in den Schlüssel gespeichert wird. +The key receives the result of the function \lua{string.match(value, +match)}, which means that the original value may not be stored +completely in the key. + +\InputLuaExample[firstline=22,lastline=23]{defs/attrs/match.lua} + +\noindent +% Das Präfix “waste ” und das Suffix “ rubbisch” der Zeichenketten wird +% verworfen. +The prefix “waste ” and the suffix “ rubbisch” of the string are +discarded. + +\InputLuaExample[firstline=29,lastline=29]{defs/attrs/match.lua} + +\noindent +% Da Funktion \lua{string.match(value, match)} immer eine Zeichenkette +% zurückgibt, ist der Wert des Schlüssel auch immer eine Zeichenkette. +Since function \lua{string.match(value, match)} always returns a string, +the value of the key is also always a string. + +%% +% +%% + +\subsubsection{Attribute “\texttt{name}”} + +% Die Option \lua{name} ermöglicht eine alternative Notation von +% Schlüsselnamen. +The \lua{name} attribute allows an alternative notation of key names. +% +% Anstatt ... +Instead of ... + +\InputLuaExample[firstline=4,lastline=5]{defs/attrs/name.lua} + +\noindent +% ... können wir schreiben: +... we can write: + +\InputLuaExample[firstline=11,lastline=15]{defs/attrs/name.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{process}”} +\label{attr-process} + +The \lua{process} attribute can be used to define a function whose return +value is passed to the key. Four parameters are passed when the +function is called: + +\begin{enumerate} +\item \lua{value}: +% Der zum schlüssel gehörende aktuelle Wert. +The current value asssociated with the key. + +\item \lua{input}: +% Die Ergebnis-Tabelle, die vor dem Zeitpunkt geklont wurde, als mit dem +% Anwenden der Definitionen begonnen wurde. +The result table cloned before the time the definitions started to be applied. + +\item \lua{result}: The table in which the final result will be saved. + +\item \lua{unknown}: The table in which the unknown key-value pairs +are stored. +\end{enumerate} + +% Das folgende Beispiel demonstriert den Parameter \lua{value}. +\noindent +The following example demonstrates the \lua{value} parameter: +\InputLuaExample[firstline=4,lastline=14]{defs/attrs/process.lua} + +\noindent +The following example demonstrates the \lua{input} parameter: + +\InputLuaExample[firstline=22,lastline=34]{defs/attrs/process.lua} + +\noindent +The following example demonstrates the \lua{result} parameter: + +\InputLuaExample[firstline=42,lastline=50]{defs/attrs/process.lua} + +\noindent +The following example demonstrates the \lua{unknown} parameter: + +\InputLuaExample[firstline=58,lastline=65]{defs/attrs/process.lua} +\InputLuaExample[firstline=69,lastline=69]{defs/attrs/process.lua} + +%% +% +%% + +\subsubsection{Attribute “\texttt{required}”} -\strut +\InputLuaExample[firstline=4,lastline=5]{defs/attrs/required.lua} -\newpage +\InputLuaExample[firstline=13,lastline=14]{defs/attrs/required.lua} -\tableofcontents +\noindent +A recursive example: -\newpage +\InputLuaExample[firstline=18,lastline=23]{defs/attrs/required.lua} +\InputLuaExample[firstline=29,lastline=30]{defs/attrs/required.lua} +\InputLuaExample[firstline=38,lastline=39]{defs/attrs/required.lua} -\section{Introduction} +%% +% +%% -\noindent -|luakeys| is a Lua module that can parse key-value options like the -\TeX{} packages \href{https://www.ctan.org/pkg/keyval}{keyval}, -\href{https://www.ctan.org/pkg/kvsetkeys}{kvsetkeys}, -\href{https://www.ctan.org/pkg/kvoptions}{kvoptions}, -\href{https://www.ctan.org/pkg/xkeyval}{xkeyval}, -\href{https://www.ctan.org/pkg/pgfkeys}{pgfkeys} etc. do. |luakeys|, -however, accompilshes this task entirely, by using the Lua language and -doesn’t rely on \TeX{}. Therefore this package can only be used with the -\TeX{} engine Lua\TeX{}. Since |luakeys| uses -\href{http://www.inf.puc-rio.br/~roberto/lpeg/}{LPeg}, the parsing -mechanism should be pretty robust. +\subsubsection{Attribute “\texttt{sub_keys}”} -The TUGboat article -\href{http://www.tug.org/tugboat/tb30-1/tb94wright-keyval.pdf} -{“Implementing key–value input: An introduction” (Volume 30 (2009), No. 1)} -by Joseph Wright and Christian Feuersänger gives a good overview of the -available key-value packages. +% Mit dem Attribut \lua{sub_keys} können ineinander verschachtelte +% Schlüssel-Wert-Paar-Definitionen aufgebaut werden. +The \lua{sub_keys} attribute can be used to build nested key-value pair +definitions. -This package would not be possible without the article -\href{https://tug.org/TUGboat/tb40-2/tb125menke-lpeg.pdf} -{Parsing complex data formats in LuaTEX with LPEG (Volume 40 (2019), No. 2)}. +\InputLuaExample[firstline=4,lastline=16]{defs/attrs/sub-keys.lua} %----------------------------------------------------------------------- % %----------------------------------------------------------------------- -\clearpage +\subsection{Function \texttt{render(result): string}} + +The function \lua{render(result)} reverses the function +\lua{parse(kv_string)}. It takes a Lua table and converts this table +into a key-value string. The resulting string usually has a different +order as the input table. + +\InputLuaExample[firstline=4,lastline=10]{functions/render.lua} + +\noindent +In Lua only tables with 1-based consecutive integer keys (a.k.a. array +tables) can be parsed in order. -\section{Usage} +\InputLuaExample[firstline=16,lastline=17]{functions/render.lua} %% % %% -\subsection{Using the Lua module \texttt{luakeys.lua}} +\subsection{Function \texttt{debug(result): void}} -The core functionality of this package is realized in Lua. So you can -use \texttt{luakeys} without using the wrapper \TeX{} files -\texttt{luakeys.sty} and \texttt{luakeys.tex}. +The function \lua{debug(result)} pretty prints a Lua table to standard +output (stdout). It is a utility function that can be used to debug and +inspect the resulting Lua table of the function \lua{parse}. You have to +compile your \TeX{} document in a console to see the terminal output. -\begin{minted}{latex} -\documentclass{article} -\directlua{ - luakeys = require('luakeys') -} +\InputLuaExample[firstline=4,lastline=5]{functions/debug.lua} -\newcommand{\helloworld}[2][]{ - \directlua{ - local keys = luakeys.parse('\luaescapestring{\unexpanded{#1}}') - luakeys.print(keys) - local marg = '#2' - tex.print(keys.greeting .. ', ' .. marg .. keys.punctuation) +\noindent +The output should look like this: + +\begin{minted}{md} +{ + ['level1'] = { + ['level2'] = { + ['key'] = 'value', + }, } } -\begin{document} -\helloworld[greeting=hello,punctuation=!]{world} -\end{document} \end{minted} %% % %% -\subsection{Using the Lua\LaTeX{} wrapper \texttt{luakeys.sty}} - -The supplied Lua\LaTeX{} file is quite small: +\subsection{Function \texttt{save(identifier, result): void}} -\begin{minted}{latex} -\NeedsTeXFormat{LaTeX2e} -\ProvidesPackage{luakeys} -\directlua{luakeys = require('luakeys')} -\end{minted} +The function \lua{save(identifier, result)} saves a result (a +table from a previous run of \lua{parse}) under an identifier. +Therefore, it is not necessary to pollute the global namespace to +store results for the later usage. -\noindent -It loads the Lua module into the global variable \texttt{luakeys}. +%% +% +%% -\begin{minted}{latex} -\documentclass{article} -\usepackage{luakeys} +\subsection{Function \texttt{get(identifier): result}} -\begin{document} - \directlua{ - local keys = luakeys.parse('one,two,three') - tex.print(keys[1]) - tex.print(keys[2]) - tex.print(keys[3]) - } -\end{document} -\end{minted} +The function \lua{get(identifier)} retrieves a saved result from the +result store. %% % %% -\subsection{Using the plain Lua\TeX{} wrapper \texttt{luakeys.tex}} +\subsection{Table \texttt{is}} -Even smaller is the file \texttt{luakeys.tex}. It consists of only one -line: +\subsubsection{Function \texttt{is.boolean(value): boolean}} +\InputLuaExample[firstline=7,lastline=23]{is-table.lua} -\begin{minted}{latex} -\directlua{luakeys = require('luakeys')} -\end{minted} +\subsubsection{Function \texttt{is.dimension(value): boolean}} +\InputLuaExample[firstline=27,lastline=37]{is-table.lua} -\noindent -It does the same as the Lua\LaTeX{} wrapper and loads the Lua module -\texttt{luakeys.lua} into the global variable \texttt{luakeys}. +\subsubsection{Function \texttt{is.integer(value): boolean}} +\InputLuaExample[firstline=41,lastline=46]{is-table.lua} -\begin{minted}{latex} -\input luakeys.tex +\subsubsection{Function \texttt{is.number(value): boolean}} +\InputLuaExample[firstline=50,lastline=57]{is-table.lua} -\directlua{ - local keys = luakeys.parse('one,two,three') - tex.print(keys[1]) - tex.print(keys[2]) - tex.print(keys[3]) -} -\bye -\end{minted} +\subsubsection{Function \texttt{is.string(value): boolean}} +\InputLuaExample[firstline=61,lastline=67]{is-table.lua} + +%----------------------------------------------------------------------- +% +%----------------------------------------------------------------------- \section{Syntax of the recognized key-value format} @@ -202,13 +1079,13 @@ It does the same as the Lua\LaTeX{} wrapper and loads the Lua module % %% -\subsection{A attempt to put the syntax into words} +\subsection{An attempt to put the syntax into words} A key-value pair is definied by an equal sign (\texttt{key=value}). -Several key-value pairs or values without keys are lined up with commas -(\texttt{key=value,value}) and build a key-value list. Curly brackets -can be used to create a recursive data structure of nested key-value -lists (\texttt{level1=\{level2=\{key=value,value\}\}}). +Several key-value pairs or keys without values (naked keys) are lined up +with commas (\texttt{key=value,naked}) and build a key-value list. Curly +brackets can be used to create a recursive data structure of nested +key-value lists (\texttt{level1=\{level2=\{key=value,naked\}\}}). %% % @@ -231,9 +1108,17 @@ lists (\texttt{level1=\{level2=\{key=value,value\}\}}). \alt \alt + ::= + + ::= ( [ ] | ) + + ::= `.' + ::= `-' | `+' - ::= `0' | `1' | `2' | `3' | `4' | `5' | `6' | `7' | `8' | `9' + ::= \{ \} + + ::= `0' | `1' | `2' | `3' | `4' | `5' | `6' | `7' | `8' | `9' ::= `bp' | `BP' \alt `cc' | `CC' @@ -297,8 +1182,6 @@ into Lua’s boolean type \lua{true}, the strings \texttt{false}, % %% -\clearpage - \subsubsection{number} \begin{multicols}{2} @@ -322,35 +1205,14 @@ into Lua’s boolean type \lua{true}, the strings \texttt{false}, % %% -\clearpage - \subsubsection{dimension} -|luakeys| detects \TeX{} dimensions and automatically converts the -dimensions into scaled points using the function \lua{tex.sp(dim)}. Use -the option \lua{convert_dimensions} of the function -\lua{parse(kv_string, options)} to disalbe the automatic conversion. - -\begin{minted}{lua} -local result = parse('dim=1cm', { - convert_dimensions = false, -}) -\end{minted} - -\noindent -If you want to convert a scale point into a unit string you can use the module -\href{https://raw.githubusercontent.com/latex3/lualibs/master/lualibs-util-dim.lua}{lualibs-util-dim.lua}. - -\begin{minted}{latex} -\begin{luacode} -require('lualibs') -tex.print(number.todimen(tex.sp('1cm'), 'cm', '%0.0F%s')) -\end{luacode} -\end{minted} - +\begin{multicols}{3} +\tiny \begin{center} \begin{tabular}{rl} -\textbf{Unit name} & \textbf{Description} \\\hline +% \textbf{Unit name} +& \textbf{Description} \\\hline bp & big point \\ cc & cicero \\ cm & centimeter \\ @@ -367,7 +1229,8 @@ sp & scaledpoint \\ \end{tabular} \end{center} -\begin{multicols}{2} +\columnbreak + \begin{minted}{latex} \luakeysdebug{ bp = 1bp, @@ -385,6 +1248,9 @@ sp & scaledpoint \\ sp = 1sp, } \end{minted} + +\columnbreak + \begin{minted}{lua} { ['bp'] = 65781, @@ -410,64 +1276,41 @@ sp & scaledpoint \\ \subsubsection{string} -There are two ways to specify strings: With or without quotes. If the -text have to contain commas or equal signs, then double quotation -marks must be used. - -\begin{multicols}{2} -\begin{minted}{latex} -\luakeysdebug{ - without quotes = no commas and equal signs are allowed, - with double quotes = ", and = are allowed", -} -\end{minted} -\begin{minted}{lua} -{ - ['without quotes'] = 'no commas and equal signs are allowed', - ['with double quotes'] = ', and = are allowed', -} -\end{minted} -\end{multicols} - -\subsubsection{Standalone values} +% Es gibt zwei Möglichkeiten Zeichenketten anzugeben: +There are two ways to specify strings: +% +% Mit oder ohne doppelte Anführungszeichen. +With or without double quotes. +% +% Wenn der Text Kommas, geschweifte Klammern oder Gleichheitszeichen +% enthalten soll, müssen doppelte Anführungszeichen verwendet werden. +If the text have to contain commas, curly braces or equal signs, then +double quotes must be used. -Standalone values are values without a key. They are converted into an -array. In Lua an array is a table with numeric indexes (The first index -is 1). +\InputLuaExample[firstline=4,lastline=17]{data-types/string.lua} -\begin{multicols}{2} -\begin{minted}{latex} -\luakeysdebug{one,two,three} -\end{minted} -\columnbreak -\begin{minted}{lua} -{ 'one', 'two', 'three' } -\end{minted} +\subsubsection{Naked keys} -\noindent -is equivalent to +% Nackte Schlüssel sind Schlüssel ohne Wert. +Naked keys are keys without a value. +% +% Mit der Option \lua{naked_as_value} können sie als Werte in ein Feld +% übernommen werden. +Using the option \lua{naked_as_value} they can be converted into values +and stored into an array. +% +% In Lua ist ein Feld eine Tabelle mit numerischen Indizes (der erste +% Index ist 1). +In Lua an array is a table with numeric indexes (The first index is 1). -\begin{minted}{lua} -{ - [1] = 'one', - [2] = 'two', - [3] = 'three', -} -\end{minted} -\end{multicols} +\InputLatexExample[firstline=5,lastline=12]{luakeysdebug/naked-keys.tex} \noindent +% VAlle erkannten Datentypen können als eigenständige Werte verwendet +% werden. All recognized data types can be used as standalone values. -\begin{multicols}{2} -\begin{minted}{latex} -\luakeysdebug{one,2,3cm} -\end{minted} -\columnbreak -\begin{minted}{lua} -{ 'one', 2, 5594039 } -\end{minted} -\end{multicols} +\InputLatexExample[firstline=14,lastline=19]{luakeysdebug/naked-keys.tex} %----------------------------------------------------------------------- % @@ -475,166 +1318,34 @@ All recognized data types can be used as standalone values. \clearpage -\section{Exported functions of the Lua module \texttt{luakeys.lua}} - -To learn more about the individual functions (local functions), please -read the \href{https://josef-friedrich.github.io/luakeys/}{source code -documentation}, which was created with -\href{http://stevedonovan.github.io/ldoc/}{LDoc}. The Lua module exports -this functions: - -\begin{minted}{lua} -local luakeys = require('luakeys') -local parse = luakeys.parse -local render = luakeys.render ---local print = luakeys.print -- That would overwrite the built-in Lua function -local save = luakeys.save -local get = luakeys.get -\end{minted} - -%% -% -%% +\section{Examples} -\subsection{\texttt{parse(kv\_string, options)}: table} +\subsection{Extend and modify keys of existing macros} -The function \lua{parse(input_string, options)} is the main method of -this module. It parses a key-value string into a Lua table. +Extend the includegraphics macro with a new key named \latex{caption} +and change the accepted values of the \latex{width} key. A number +between 0 and 1 is allowed and converted into +\latex{width=0.5\linewidth} -\begin{minted}{latex} -\newcommand{\mykeyvalcmd}[1][]{ - \directlua{ - result = luakeys.parse('#1') - luakeys.print(result) - } - #2 -} -\mykeyvalcmd[one=1]{test} -\end{minted} +\InputLuaExample{extend-includegraphics/extend-keys.lua} +\InputLatexExample{extend-includegraphics/extend-keys.tex} -\noindent -In plain \TeX: +\subsection{Process document class options} \begin{minted}{latex} -\def\mykeyvalcommand#1{ - \directlua{ - result = luakeys.parse('#1') - luakeys.print(result) - } -} -\mykeyvalcmd{one=1} -\end{minted} - -\noindent -The function can be called with an options table. This options are -supported: - -\begin{minted}{lua} -local result = parse('one,two,three', { - convert_dimensions = false, - unpack_single_array_value = false, - standalone_as_true = false, - converter = function(key, value, depth, current_table, root_table) - return key, value - end, - case_insensitive_keys = false, -}) -\end{minted} - -\noindent -The options can also be set globally using the exported table -|default_options|: - -\begin{minted}{lua} -luakeys.parse('dim=1cm') -- {dim = 1864679} -luakeys.default_options.convert_dimensions = false --- or: --- local defaults = luakeys.default_options --- defaults.convert_dimensions = false -luakeys.parse('dim=1cm') -- {dim = '1cm'} -\end{minted} - -%% -% -%% - -\subsection{\texttt{render(tbl)}: string} - -The function \lua{render(tbl)} reverses the function -\lua{parse(kv_string)}. It takes a Lua table and converts this table -into a key-value string. The resulting string usually has a different -order as the input table. - -\begin{minted}{lua} -result = luakeys.parse('one=1,two=2,tree=3,') -print(luakeys.render(result)) ---- one=1,two=2,tree=3, ---- or: ---- two=2,one=1,tree=3, ---- or: ---- ... -\end{minted} - -\noindent -In Lua only tables with 1-based consecutive integer keys (a.k.a. array -tables) can be parsed in order. - -\begin{minted}{lua} -result = luakeys.parse('one,two,three') -print(luakeys.render(result)) ---- one,two,three, (always) +\directlua{luakeys.parse('\@classoptionslist')} \end{minted} -%% -% -%% - -\subsection{\texttt{print(tbl): void}} - -The function \lua{print(tbl)} pretty prints a Lua table to standard -output (stdout). It is a utility function that can be used to debug and -inspect the resulting Lua table of the function \lua{parse}. You have to -compile your \TeX{} document in a console to see the terminal output. - -%\luakeysdebug{level1={level2={key=value}}} +\InputLatexExample{class-options/test-class.cls} +\InputLatexExample{class-options/use-test-class.tex} \begin{minted}{lua} -result = luakeys.parse('level1={level2={key=value}}') -luakeys.print(result) -\end{minted} - -\noindent -The output should look like this: - -\begin{minted}{md} { - ['level1'] = { - ['level2'] = { - ['key'] = 'value', - }, + [1] = '12pt', + [2] = 'landscape', } \end{minted} -%% -% -%% - -\subsection{\texttt{save(identifier, result): void}} - -The function \lua{save(identifier, result)} saves a result (a -table from a previous run of \lua{parse}) under an identifier. -Therefore, it is not necessary to pollute the global namespace to -store results for the later usage. - -%% -% -%% - -\subsection{\texttt{get(identifier): table}} - -The function \lua{get(identifier)} retrieves a saved result from the -result store. - %----------------------------------------------------------------------- % %----------------------------------------------------------------------- @@ -685,7 +1396,7 @@ An example of how to use the command in \LaTeX: \usepackage{luakeys-debug} \begin{document} \luakeysdebug[ - unpack single array values=false, + unpack=false, convert dimensions=false ]{one,two,three} \end{document} @@ -723,9 +1434,9 @@ An example of how to use the command in \LaTeX: \clearpage -\subsection{luakeys.tex} +\subsection{luakeys.sty} -\inputminted[linenos=true]{latex}{luakeys.tex} +\inputminted[linenos=true]{latex}{luakeys.sty} %% % @@ -766,9 +1477,22 @@ An example of how to use the command in \LaTeX: } \changes{v0.5}{2022/04/04}{ * Add possibility to change options globally -* New option: standalone\_as\_true +* New option: standalone_as_true * Add a recursive converter callback / hook to process the parse tree -* New option: case\_insensitive\_keys +* New option: case_insensitive_keys +} +\changes{v0.6}{2022/06/09}{ +* New feature: keys now can be defined using the function + “define(defs, opts)” or “define(kv_string, { defs = { key = { ... } } })” +* Rename the global options table from “default_options” to “opts” +* New option “format_keys” +* Remove option “case_insensitive_keys”. Use + “format_keys = \{ lower \}” to achieve the same effect. +* The default value of the option “convert_dimension” is now false. +* The option “standalone_as_true” is renamed to “naked_as_value”. + The boolean value of the option must be changed to the opposite to + produce the previous effect. +* The function “print()” is now called “debug()” } \pagebreak \PrintChanges diff --git a/macros/luatex/generic/luakeys/luakeys.lua b/macros/luatex/generic/luakeys/luakeys.lua index c5ccf6b01d..ae689d02c1 100644 --- a/macros/luatex/generic/luakeys/luakeys.lua +++ b/macros/luatex/generic/luakeys/luakeys.lua @@ -15,59 +15,120 @@ -- -- This work consists of the files luakeys.lua, luakeys.sty, luakeys.tex -- luakeys-debug.sty and luakeys-debug.tex. - --- A key-value parser written with Lpeg. -- --- Explanations of some LPeg notation forms: --- --- * `patt ^ 0` = `expression *` --- * `patt ^ 1` = `expression +` --- * `patt ^ -1` = `expression ?` --- * `patt1 * patt2` = `expression1 expression2`: Sequence --- * `patt1 + patt2` = `expression1 / expression2`: Ordered choice --- --- * [TUGboat article: Parsing complex data formats in LuaTEX with LPEG](https://tug.org/TUGboat/tb40-2/tb125menke-Patterndf) --- -- @module luakeys - local lpeg = require('lpeg') -local Variable = lpeg.V -local Pattern = lpeg.P -local Set = lpeg.S -local Range = lpeg.R -local CaptureGroup = lpeg.Cg -local CaptureFolding = lpeg.Cf -local CaptureTable = lpeg.Ct -local CaptureConstant = lpeg.Cc -local CaptureSimple = lpeg.C if not tex then - tex = {} + tex = { + -- Dummy function for the tests. + sp = function(input) + return 1234567 + end, + } +end + +if not token then + token = { + set_macro = function(csname, content, global) + end, + } +end + +local utils = { + --- Get the size of an array like table `{ 'one', 'two', 'three' }` = 3. + -- + -- @tparam table value A table or any input. + -- + -- @treturn number The size of the array like table. 0 if the input is + -- no table or the table is empty. + get_array_size = function(value) + local count = 0 + if type(value) == 'table' then + for _ in ipairs(value) do + count = count + 1 + end + end + return count + end, + + --- Get the size of a table `{ one = 'one', 'two', 'three' }` = 3. + -- + -- @tparam table value A table or any input. + -- + -- @treturn number The size of the array like table. 0 if the input is + -- no table or the table is empty. + get_table_size = function(value) + local count = 0 + if type(value) == 'table' then + for _ in pairs(value) do + count = count + 1 + end + end + return count + end, + + remove_from_array = function(array, element) + for index, value in pairs(array) do + if element == value then + array[index] = nil + return value + end + end + end, +} - -- Dummy function for the tests. - tex['sp'] = function (input) - return 1234567 +--- https://stackoverflow.com/a/1283608/10193818 +local function merge_tables(target, t2) + for k, v in pairs(t2) do + if type(v) == 'table' then + if type(target[k] or false) == 'table' then + merge_tables(target[k] or {}, t2[k] or {}) + elseif target[k] == nil then + target[k] = v + end + elseif target[k] == nil then + target[k] = v + end end + return target end ---- Option handling --- @section +--- http://lua-users.org/wiki/CopyTable +local function clone_table(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[clone_table(orig_key)] = clone_table(orig_value) + end + setmetatable(copy, clone_table(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy +end --- This table stores all allowed option keys. -local option_keys = { - 'convert_dimensions', - 'unpack_single_array_values', - 'standalone_as_true', - 'converter', - 'case_insensitive_keys' +local all_options = { + convert_dimensions = false, + debug = false, + default = true, + defaults = false, + defs = false, + format_keys = false, + hooks = {}, + naked_as_value = false, + no_error = false, + postprocess = false, + preprocess = false, + unpack = true, } --- The default options. -local default_options = { - convert_dimensions = true, - unpack_single_array_values = true, - standalone_as_true = false, -} +local default_options = clone_table(all_options) local function throw_error(message) if type(tex.error) == 'function' then @@ -77,58 +138,142 @@ local function throw_error(message) end end ---- Convert a key so that it can be written as a table field without --- quotes and square brackets (for example `one 2` becomes `one_2`). --- The key can then reference values from a table using dot notation. --- (`table["one 2"]` becomes `table.one_2`). --- --- @tparam string key The key to be converted. --- --- @treturn string The converted key. -local function luafy_key(key) - return key:gsub('[^%w]+', '_') -end +local l3_code_cctab = 10 ---- Convert all keys in a table to strings containig only alphanumeric --- characters and underscores. +--- Convert back to strings +-- @section + +--- The function `render(tbl)` reverses the function +-- `parse(kv_string)`. It takes a Lua table and converts this table +-- into a key-value string. The resulting string usually has a +-- different order as the input table. In Lua only tables with +-- 1-based consecutive integer keys (a.k.a. array tables) can be +-- parsed in order. -- --- @param raw_options Some raw options. +-- @tparam table result A table to be converted into a key-value string. -- --- @treturn table Returns always a table. If the input value is not a --- an empty table is returned. -local function luafy_options(raw_options) - if type(raw_options) ~= 'table' then - raw_options = {} - end - local options = {} - for key, value in pairs(raw_options) do - options[luafy_key(key)] = value +-- @treturn string A key-value string that can be passed to a TeX +-- macro. +local function render(result) + local function render_inner(result) + local output = {} + local function add(text) + table.insert(output, text) + end + for key, value in pairs(result) do + if (key and type(key) == 'string') then + if (type(value) == 'table') then + if (next(value)) then + add(key .. '={') + add(render_inner(value)) + add('},') + else + add(key .. '={},') + end + else + add(key .. '=' .. tostring(value) .. ',') + end + else + add(tostring(value) .. ',') + end + end + return table.concat(output) end - return options + return render_inner(result) end ---- All option keys can be written with underscores or with spaces as --- separators. --- For the LaTeX version of the macro --- `\luakeysdebug[options]{kv-string}`. +--- The function `stringify(tbl, for_tex)` converts a Lua table into a +-- printable string. Stringify a table means to convert the table into +-- a string. This function is used to realize the `debug` function. +-- `stringify(tbl, true)` (`for_tex = true`) generates a string which +-- can be embeded into TeX documents. The macro `\luakeysdebug{}` uses +-- this option. `stringify(tbl, false)` or `stringify(tbl)` generate a +-- string suitable for the terminal. -- --- @tparam table options_raw Options in a raw format. The table may be --- empty or some keys are not set. +-- @tparam table result A table to stringify. -- --- @treturn table -local function normalize_parse_options (options_raw) - options_raw = luafy_options(options_raw) - local options = {} - - for _, option_name in ipairs(option_keys) do - if options_raw[option_name] ~= nil then - options[option_name] = options_raw[option_name] - else - options[option_name] = default_options[option_name] +-- @tparam boolean for_tex Stringify the table into a text string that +-- can be embeded inside a TeX document via tex.print(). Curly braces +-- and whites spaces are escaped. +-- +-- https://stackoverflow.com/a/54593224/10193818 +local function stringify(result, for_tex) + local line_break, start_bracket, end_bracket, indent + + if for_tex then + line_break = '\\par' + start_bracket = '$\\{$' + end_bracket = '$\\}$' + indent = '\\ \\ ' + else + line_break = '\n' + start_bracket = '{' + end_bracket = '}' + indent = ' ' + end + + local function stringify_inner(input, depth) + local output = {} + depth = depth or 0 + + local function add(depth, text) + table.insert(output, string.rep(indent, depth) .. text) + end + + local function format_key(key) + if (type(key) == 'number') then + return string.format('[%s]', key) + else + return string.format('[\'%s\']', key) + end end + + if type(input) ~= 'table' then + return tostring(input) + end + + for key, value in pairs(input) do + if (key and type(key) == 'number' or type(key) == 'string') then + key = format_key(key) + + if (type(value) == 'table') then + if (next(value)) then + add(depth, key .. ' = ' .. start_bracket) + add(0, stringify_inner(value, depth + 1)) + add(depth, end_bracket .. ','); + else + add(depth, key .. ' = ' .. start_bracket .. end_bracket .. ',') + end + else + if (type(value) == 'string') then + value = string.format('\'%s\'', value) + else + value = tostring(value) + end + + add(depth, key .. ' = ' .. value .. ',') + end + end + end + + return table.concat(output, line_break) end - return options + return + start_bracket .. line_break .. stringify_inner(result, 1) .. line_break .. + end_bracket +end + +--- The function `debug(tbl)` pretty prints a Lua table to standard +-- output (stdout). It is a utility function that can be used to +-- debug and inspect the resulting Lua table of the function +-- `parse`. You have to compile your TeX document in a console to +-- see the terminal output. +-- +-- @tparam table result A table to be printed to standard output for +-- debugging purposes. +local function debug(result) + print('\n' .. stringify(result, false)) end --- Parser / Lpeg related @@ -136,18 +281,42 @@ end --- Generate the PEG parser using Lpeg. -- +-- Explanations of some LPeg notation forms: +-- +-- * `patt ^ 0` = `expression *` +-- * `patt ^ 1` = `expression +` +-- * `patt ^ -1` = `expression ?` +-- * `patt1 * patt2` = `expression1 expression2`: Sequence +-- * `patt1 + patt2` = `expression1 / expression2`: Ordered choice +-- +-- * [TUGboat article: Parsing complex data formats in LuaTEX with LPEG](https://tug.org/TUGboat/tb40-2/tb125menke-Patterndf) +-- -- @treturn userdata The parser. -local function generate_parser(options) +local function generate_parser(initial_rule, convert_dimensions) + if convert_dimensions == nil then + convert_dimensions = false + end + + local Variable = lpeg.V + local Pattern = lpeg.P + local Set = lpeg.S + local Range = lpeg.R + local CaptureGroup = lpeg.Cg + local CaptureFolding = lpeg.Cf + local CaptureTable = lpeg.Ct + local CaptureConstant = lpeg.Cc + local CaptureSimple = lpeg.C + -- Optional whitespace local white_space = Set(' \t\n\r') --- Match literal string surrounded by whitespace local ws = function(match) - return white_space^0 * Pattern(match) * white_space^0 + return white_space ^ 0 * Pattern(match) * white_space ^ 0 end - local capture_dimension = function (input) - if options.convert_dimensions then + local capture_dimension = function(input) + if convert_dimensions then return tex.sp(input) else return input @@ -180,8 +349,9 @@ local function generate_parser(options) end end + -- LuaFormatter off return Pattern({ - 'list', + [1] = initial_rule, -- list_item* list = CaptureFolding( @@ -220,6 +390,9 @@ local function generate_parser(options) Variable('string_quoted') * -Variable('value') + Variable('string_unquoted'), + -- for is.boolean() + boolean_only = Variable('boolean') * -1, + -- boolean_true / boolean_false boolean = ( @@ -237,18 +410,31 @@ local function generate_parser(options) Pattern('FALSE') + Pattern('False'), + -- for is.dimension() + dimension_only = Variable('dimension') * -1, + dimension = ( - Variable('sign')^0 * white_space^0 * Variable('tex_number') * white_space^0 * Variable('unit') ) / capture_dimension, - number = - (white_space^0 * (Variable('lua_number') / tonumber) * white_space^0) , + -- for is.number() + number_only = Variable('number') * -1, + + -- capture number + number = Variable('tex_number') / tonumber, + -- sign? white_space? (integer+ fractional? / fractional) tex_number = - (Variable('integer')^1 * (Pattern('.') * Variable('integer')^1)^0) + - (Pattern('.') * Variable('integer')^1), + Variable('sign')^0 * white_space^0 * + (Variable('integer')^1 * Variable('fractional')^0) + + Variable('fractional'), + + sign = Set('-+'), + + fractional = Pattern('.') * Variable('integer')^1, + + integer = Range('09')^1, -- 'bp' / 'BP' / 'cc' / etc. -- https://raw.githubusercontent.com/latex3/lualibs/master/lualibs-util-dim.lua @@ -267,18 +453,6 @@ local function generate_parser(options) Pattern('pt') + Pattern('PT') + Pattern('sp') + Pattern('SP'), - lua_number = - Variable('int') * - Variable('frac')^-1, - - int = Variable('sign')^-1 * ( - Range('19') * Variable('integer') + Variable('integer') - ), - - frac = Pattern('.') * Variable('integer'), - sign = Set('-+'), - integer = Range('09')^1, - -- '"' ('\"' / !'"')* '"' string_quoted = white_space^0 * Pattern('"') * @@ -294,68 +468,25 @@ local function generate_parser(options) word_unquoted = (1 - white_space - Set('{},='))^1 }) +-- LuaFormatter on end ---- Get the size of an array like table `{ 'one', 'two', 'three' }` = 3. --- --- @tparam table value A table or any input. --- --- @treturn number The size of the array like table. 0 if the input is --- no table or the table is empty. -local function get_array_size(value) - local count = 0 - if type(value) == 'table' then - for _ in ipairs(value) do count = count + 1 end - end - return count -end - ---- Get the size of a table `{ one = 'one', 'two', 'three' }` = 3. --- --- @tparam table value A table or any input. --- --- @treturn number The size of the array like table. 0 if the input is --- no table or the table is empty. -local function get_table_size(value) - local count = 0 - if type(value) == 'table' then - for _ in pairs(value) do count = count + 1 end - end - return count -end - ---- Unpack a single valued array table like `{ 'one' }` into `one` or --- `{ 1 }` into `1`. --- --- @treturn If the value is a array like table with one non table typed --- value in it, the unpacked value, else the unchanged input. -local function unpack_single_valued_array_table(value, options) - if - type(value) == 'table' and - get_array_size(value) == 1 and - get_table_size(value) == 1 and - type(value[1]) ~= 'table' - then - if type(value[1]) == 'string' and options.standalone_as_true then - return value - else - return value[1] - end - end - return value -end - -local function visit_parse_tree(parse_tree, callback_func) - if type(parse_tree) ~= 'table' then - throw_error('Parse tree has to be a table') +local function visit_tree(tree, callback_func) + if type(tree) ~= 'table' then + throw_error('Parameter “tree” has to be a table, got: ' .. + tostring(tree)) end - local function visit_parse_tree_recursive(root_table, current_table, result, depth, callback_func) - for key, value in pairs(current_table) do + local function visit_tree_recursive(tree, + current, + result, + depth, + callback_func) + for key, value in pairs(current) do if type(value) == 'table' then - value = visit_parse_tree_recursive(root_table, value, {}, depth + 1, callback_func) + value = visit_tree_recursive(tree, value, {}, depth + 1, callback_func) end - key, value = callback_func(key, value, depth, current_table, root_table) + key, value = callback_func(key, value, depth, current, tree) if key ~= nil and value ~= nil then result[key] = value @@ -366,255 +497,574 @@ local function visit_parse_tree(parse_tree, callback_func) end end - return visit_parse_tree_recursive(parse_tree, parse_tree, {}, 1, callback_func) + local result = visit_tree_recursive(tree, tree, {}, 1, callback_func) + + if result == nil then + return {} + end + return result end ---- Normalize the result tables of the LPeg parser. This normalization --- tasks are performed on the raw input table coming directly from the --- PEG parser: --- --- * Unpack all single valued array like tables: `{ 'text' }` into --- `text` --- --- @tparam table raw The raw input table coming directly from the PEG --- parser --- --- @tparam table options Some options. A table with the key --- `unpack_single_array_values` --- --- @treturn table A normalized table ready for the outside world. -local function normalize(raw, options) - local function normalize_recursive(raw, result, options) - for key, value in pairs(raw) do - if options.unpack_single_array_values then - value = unpack_single_valued_array_table(value, options) - end - if type(value) == 'table' then - result[key] = normalize_recursive(value, {}, options) - else - result[key] = value - end +local is = { + boolean = function(value) + if value == nil then + return false end - return result - end - raw = normalize_recursive(raw, {}, options) + if type(value) == 'boolean' then + return true + end + local parser = generate_parser('boolean_only', false) + local result = parser:match(value) + return result ~= nil + end, + + dimension = function(value) + if value == nil then + return false + end + local parser = generate_parser('dimension_only', false) + local result = parser:match(value) + return result ~= nil + end, + + integer = function(value) + local n = tonumber(value) + if n == nil then + return false + end + return n == math.floor(n) + end, - if options.standalone_as_true then - raw = visit_parse_tree(raw, function (key, value) - if type(key) == 'number' and type(value) == 'string' then - return value, true - end - return key, value - end) - end + number = function(value) + if value == nil then + return false + end + if type(value) == 'number' then + return true + end + local parser = generate_parser('number_only', false) + local result = parser:match(value) + return result ~= nil + end, + + string = function(value) + return type(value) == 'string' + end, +} - if options.case_insensitive_keys then - raw = visit_parse_tree(raw, function (key, value) - if type(key) == 'string' then - return key:lower(), value - end - return key, value - end) +--- Apply the key-value-pair definitions (defs) on an input table in a +--- recursive fashion. +--- +---@param defs table A table containing all definitions. +---@param opts table The parse options table. +---@param input table The current input table. +---@param output table The current output table. +---@param unknown table Always the root unknown table. +---@param key_path table An array of key names leading to the current +---@param input_root table The root input table +--- input and output table. +local function apply_definitions(defs, + opts, + input, + output, + unknown, + key_path, + input_root) + local exclusive_groups = {} + + local function add_to_key_path(key_path, key) + local new_key_path = {} + + for index, value in ipairs(key_path) do + new_key_path[index] = value + end + + table.insert(new_key_path, key) + return new_key_path end - return raw -end + local function set_default_value(def) + if def.default ~= nil then + return def.default + elseif opts ~= nil and opts.default ~= nil then + return opts.default + end + return true + end ---- Parse a LaTeX/TeX style key-value string into a Lua table. With --- this function you should be able to parse key-value strings like --- this example: --- --- show, --- hide, --- key with spaces = String without quotes, --- string="String with double quotes: ,{}=", --- dimension = 1cm, --- number = -1.2, --- list = {one,two,three}, --- key value list = {one=one,two=two,three=three}, --- nested key = { --- nested key 2= { --- key = value, --- }, --- }, --- --- The string above results in this Lua table: --- --- { --- 'show', --- 'hide', --- ['key with spaces'] = 'String without quotes', --- string = 'String with double quotes: ,{}=', --- dimension = 1864679, --- number = -1.2, --- list = {'one', 'two', 'three'}, --- key value list = { --- one = 'one', --- three = 'three', --- two = 'two' --- }, --- ['nested key'] = { --- ['nested key 2'] = { --- key = 'value' --- } --- }, --- } --- --- @tparam string kv_string A string in the TeX/LaTeX style key-value --- format as described above. --- --- @tparam table options A table containing --- settings: `convert_dimensions`, `unpack_single_array_values`, `standalone_as_true`, `converter` --- --- @treturn table A hopefully properly parsed table you can do --- something useful with. -local function parse (kv_string, options) - if kv_string == nil then - return {} + local function find_value(search_key, def) + if input[search_key] ~= nil then + local value = input[search_key] + input[search_key] = nil + return value + -- naked keys: values with integer keys + elseif utils.remove_from_array(input, search_key) ~= nil then + return set_default_value(def) + end end - options = normalize_parse_options(options) - local parser = generate_parser(options) - local parse_tree = parser:match(kv_string) + local apply = { + alias = function(value, key, def) + if type(def.alias) == 'string' then + def.alias = { def.alias } + end + local alias_value + local used_alias_key + -- To get an error if the key and an alias is present + if value ~= nil then + alias_value = value + used_alias_key = key + end + for _, alias in ipairs(def.alias) do + local v = find_value(alias, def) + if v ~= nil then + if alias_value ~= nil then + throw_error(string.format( + 'Duplicate aliases “%s” and “%s” for key “%s”!', + used_alias_key, alias, key)) + end + used_alias_key = alias + alias_value = v + end + end + if alias_value ~= nil then + return alias_value + end + end, - if options.converter ~= nil and type(options.converter) == 'function' then - parse_tree = visit_parse_tree(parse_tree, options.converter) - end - return normalize(parse_tree, options) -end + always_present = function(value, key, def) + if value == nil and def.always_present then + return set_default_value(def) + end + end, ---- Convert back to strings --- @section + choices = function(value, key, def) + if value == nil then + return + end + if def.choices ~= nil and type(def.choices) == 'table' then + local is_in_choices = false + for _, choice in ipairs(def.choices) do + if value == choice then + is_in_choices = true + end + end + if not is_in_choices then + throw_error('The value “' .. value .. + '” does not exist in the choices: ' .. + table.concat(def.choices, ', ') .. '!') + end + end + end, ---- The function `render(tbl)` reverses the function --- `parse(kv_string)`. It takes a Lua table and converts this table --- into a key-value string. The resulting string usually has a --- different order as the input table. In Lua only tables with --- 1-based consecutive integer keys (a.k.a. array tables) can be --- parsed in order. --- --- @tparam table tbl A table to be converted into a key-value string. --- --- @treturn string A key-value string that can be passed to a TeX --- macro. -local function render (tbl) - local function render_inner(tbl) - local output = {} - local function add(text) - table.insert(output, text) - end - for key, value in pairs(tbl) do - if (key and type(key) == 'string') then - if (type(value) == 'table') then - if (next(value)) then - add(key .. '={') - add(render_inner(value)) - add('},') + data_type = function(value, key, def) + if value == nil then + return + end + if def.data_type ~= nil then + local converted + -- boolean + if def.data_type == 'boolean' then + if value == 0 or value == '' or not value then + converted = false else - add(key .. '={},') + converted = true end + -- dimension + elseif def.data_type == 'dimension' then + if is.dimension(value) then + converted = value + end + -- integer + elseif def.data_type == 'integer' then + if is.number(value) then + converted = math.floor(tonumber(value)) + end + -- number + elseif def.data_type == 'number' then + if is.number(value) then + converted = tonumber(value) + end + -- string + elseif def.data_type == 'string' then + converted = tostring(value) else - add(key .. '=' .. tostring(value) .. ',') + throw_error('Unknown data type: ' .. def.data_type) end - else - add(tostring(value) .. ',') + if converted == nil then + throw_error('The value “' .. value .. '” of the key “' .. key .. + '” could not be converted into the data type “' .. + def.data_type .. '”!') + else + return converted + end + end + end, + + exclusive_group = function(value, key, def) + if value == nil then + return + end + if def.exclusive_group ~= nil then + if exclusive_groups[def.exclusive_group] ~= nil then + throw_error('The key “' .. key .. + '” belongs to a mutually exclusive group “' .. + def.exclusive_group .. '” and the key “' .. + exclusive_groups[def.exclusive_group] .. + '” is already present!') + else + exclusive_groups[def.exclusive_group] = key + end + end + end, + + l3_tl_set = function(value, key, def) + if value == nil then + return + end + if def.l3_tl_set ~= nil then + tex.print(l3_code_cctab, '\\tl_set:Nn \\g_' .. def.l3_tl_set .. '_tl') + tex.print('{' .. value .. '}') + end + end, + + macro = function(value, key, def) + if value == nil then + return + end + if def.macro ~= nil then + token.set_macro(def.macro, value, 'global') + end + end, + + match = function(value, key, def) + if value == nil then + return end + if def.match ~= nil then + if type(def.match) ~= 'string' then + throw_error('def.match has to be a string') + end + local match = string.match(value, def.match) + if match == nil then + throw_error('The value “' .. value .. '” of the key “' .. key .. + '” does not match “' .. def.match .. '”!') + else + return match + end + end + end, + + opposite_keys = function(value, key, def) + if def.opposite_keys ~= nil then + local true_value = def.opposite_keys[true] + local false_value = def.opposite_keys[false] + if true_value == nil or false_value == nil then + throw_error( + 'Usage opposite_keys = { [true] = "...", [false] = "..." }') + end + if utils.remove_from_array(input, true_value) ~= nil then + return true + elseif utils.remove_from_array(input, false_value) ~= nil then + return false + end + end + end, + + process = function(value, key, def) + if value == nil then + return + end + if def.process ~= nil and type(def.process) == 'function' then + return def.process(value, input_root, output, unknown) + end + end, + + required = function(value, key, def) + if def.required ~= nil and def.required and value == nil then + throw_error(string.format('Missing required key “%s”!', key)) + end + end, + + sub_keys = function(value, key, def) + if def.sub_keys ~= nil then + local v + -- To get keys defined with always_present + if value == nil then + v = {} + elseif type(value) == 'string' then + v = { value } + elseif type(value) == 'table' then + v = value + end + v = apply_definitions(def.sub_keys, opts, v, output[key], unknown, + add_to_key_path(key_path, key), input_root) + if utils.get_table_size(v) > 0 then + return v + end + end + end, + } + + --- standalone values are removed. + -- For some callbacks and the third return value of parse, we + -- need an unchanged raw result from the parse function. + input = clone_table(input) + if output == nil then + output = {} + end + if unknown == nil then + unknown = {} + end + if key_path == nil then + key_path = {} + end + + for index, def in pairs(defs) do + --- Find key and def + local key + if type(def) == 'table' and def.name == nil and type(index) == 'string' then + key = index + elseif type(def) == 'table' and def.name ~= nil then + key = def.name + elseif type(index) == 'number' and type(def) == 'string' then + key = def + def = { default = true } end - return table.concat(output) + + if type(def) ~= 'table' then + throw_error('Key definition must be a table') + end + + if key == nil then + throw_error('key name couldn’t be detected!') + end + + local value = find_value(key, def) + + for _, def_opt in ipairs({ + 'alias', + 'opposite_keys', + 'always_present', + 'required', + 'data_type', + 'choices', + 'match', + 'exclusive_group', + 'macro', + 'l3_tl_set', + 'process', + 'sub_keys', + }) do + if def[def_opt] ~= nil then + local tmp_value = apply[def_opt](value, key, def) + if tmp_value ~= nil then + value = tmp_value + end + end + end + + output[key] = value end - return render_inner(tbl) + + if utils.get_table_size(input) > 0 then + -- Move to the current unknown table. + local current_unknown = unknown + for _, key in ipairs(key_path) do + if current_unknown[key] == nil then + current_unknown[key] = {} + end + current_unknown = current_unknown[key] + end + + -- Copy all unknown key-value-pairs to the current unknown table. + for key, value in pairs(input) do + current_unknown[key] = value + end + end + + return output, unknown end ---- The function `stringify(tbl, for_tex)` converts a Lua table into a --- printable string. Stringify a table means to convert the table into --- a string. This function is used to realize the `print` function. --- `stringify(tbl, true)` (`for_tex = true`) generates a string which --- can be embeded into TeX documents. The macro `\luakeysdebug{}` uses --- this option. `stringify(tbl, false)` or `stringify(tbl)` generate a --- string suitable for the terminal. --- --- @tparam table input A table to stringify. --- --- @tparam boolean for_tex Stringify the table into a text string that --- can be embeded inside a TeX document via tex.print(). Curly braces --- and whites spaces are escaped. +--- Parse a LaTeX/TeX style key-value string into a Lua table. +--- +---@param kv_string string A string in the TeX/LaTeX style key-value format as described above. +---@param opts table A table containing the settings: +--- `convert_dimensions`, `unpack`, `naked_as_value`, `converter`, +--- `debug`, `preprocess`, `postprocess`. -- --- https://stackoverflow.com/a/54593224/10193818 -local function stringify(input, for_tex) - local line_break, start_bracket, end_bracket, indent - - if for_tex then - line_break = '\\par' - start_bracket = '$\\{$' - end_bracket = '$\\}$' - indent = '\\ \\ ' - else - line_break = '\n' - start_bracket = '{' - end_bracket = '}' - indent = ' ' +---@return table result The final result of all individual parsing and normalization steps. +---@return table unknown A table with unknown, undefinied key-value pairs. +---@return table raw The unprocessed, raw result of the LPeg parser. +local function parse(kv_string, opts) + if kv_string == nil then + return {} end - local function stringify_inner(input, depth) - local output = {} - depth = depth or 0 + --- Normalize the parse options. + --- + --- @param opts table Options in a raw format. The table may be empty or some keys are not set. + --- + --- @return table + local function normalize_opts(opts) + if type(opts) ~= 'table' then + opts = {} + end + for key, _ in pairs(opts) do + if all_options[key] == nil then + throw_error('Unknown parse option: ' .. tostring(key) .. '!') + end + end + local old_opts = opts + opts = {} + for name, _ in pairs(all_options) do + if old_opts[name] ~= nil then + opts[name] = old_opts[name] + else + opts[name] = default_options[name] + end + end - local function add(depth, text) - table.insert(output, string.rep(indent, depth) .. text) + local hooks = { + kv_string = true, + keys_before_opts = true, + result_before_opts = true, + keys_before_def = true, + result_before_def = true, + keys = true, + result = true, + } + + for hook in pairs(opts.hooks) do + if hooks[hook] == nil then + throw_error('Unknown hook: ' .. tostring(hook) .. '!') + end end + return opts + end + opts = normalize_opts(opts) - local function format_key(key) - if (type(key) == 'number') then - return string.format('[%s]', key) + if type(opts.hooks.kv_string) == 'function' then + kv_string = opts.hooks.kv_string(kv_string) + end + + local result = generate_parser('list', opts.convert_dimensions):match( + kv_string) + local raw = clone_table(result) + + local function apply_hook(name) + if type(opts.hooks[name]) == 'function' then + if name:match('^keys') then + result = visit_tree(result, opts.hooks[name]) else - return string.format('[\'%s\']', key) + opts.hooks[name](result) + end + + if opts.debug then + print('After the execution of the hook: ' .. name) + debug(result) end end + end - if type(input) ~= 'table' then - return tostring(input) + local function apply_hooks(at) + if at ~= nil then + at = '_' .. at + else + at = '' end + apply_hook('keys' .. at) + apply_hook('result' .. at) + end - for key, value in pairs(input) do - if (key and type(key) == 'number' or type(key) == 'string') then - key = format_key(key) + apply_hooks('before_opts') - if (type(value) == 'table') then - if (next(value)) then - add(depth, key .. ' = ' .. start_bracket) - add(0, stringify_inner(value, depth + 1)) - add(depth, end_bracket .. ','); - else - add(depth, key .. ' = ' .. start_bracket .. end_bracket .. ',') - end - else - if (type(value) == 'string') then - value = string.format('\'%s\'', value) - else - value = tostring(value) - end + --- Normalize the result table of the LPeg parser. This normalization + -- tasks are performed on the raw input table coming directly from + -- the PEG parser: + -- + --- @param result table The raw input table coming directly from the PEG parser + --- @param opts table Some options. + local function apply_opts(result, opts) + local callbacks = { + unpack = function(key, value) + if type(value) == 'table' and utils.get_array_size(value) == 1 and + utils.get_table_size(value) == 1 and type(value[1]) ~= 'table' then + return key, value[1] + end + return key, value + end, - add(depth, key .. ' = ' .. value .. ',') + process_naked = function(key, value) + if type(key) == 'number' and type(value) == 'string' then + return value, opts.default + end + return key, value + end, + + format_key = function(key, value) + if type(key) == 'string' then + for _, style in ipairs(opts.format_keys) do + if style == 'lower' then + key = key:lower() + elseif style == 'snake' then + key = key:gsub('[^%w]+', '_') + elseif style == 'upper' then + key = key:upper() + else + throw_error('Unknown style to format keys: ' .. tostring(style) .. + ' Allowed styles are: lower, snake, upper') + end + end end + return key, value + end, + } + + if opts.unpack then + result = visit_tree(result, callbacks.unpack) + end + + if not opts.naked_as_value and opts.defs == false then + result = visit_tree(result, callbacks.process_naked) + end + + if opts.format_keys then + if type(opts.format_keys) ~= 'table' then + throw_error('The option “format_keys” has to be a table not ' .. + type(opts.format_keys)) end + result = visit_tree(result, callbacks.format_key) end - return table.concat(output, line_break) + return result + end + result = apply_opts(result, opts) + + -- All unknown keys are stored in this table + local unknown = nil + if type(opts.defs) == 'table' then + apply_hooks('before_defs') + result, unknown = apply_definitions(opts.defs, opts, result, {}, {}, {}, + clone_table(result)) end - return start_bracket .. line_break .. stringify_inner(input, 1) .. line_break .. end_bracket -end + apply_hooks() ---- The function `pretty_print(tbl)` pretty prints a Lua table to standard --- output (stdout). It is a utility function that can be used to --- debug and inspect the resulting Lua table of the function --- `parse`. You have to compile your TeX document in a console to --- see the terminal output. --- --- @tparam table tbl A table to be printed to standard output for --- debugging purposes. -local function pretty_print(tbl) - print(stringify(tbl, false)) + if opts.defaults ~= nil and type(opts.defaults) == 'table' then + merge_tables(result, opts.defaults) + end + + if opts.debug then + debug(result) + end + + -- no_error + if not opts.no_error and type(unknown) == 'table' and + utils.get_table_size(unknown) > 0 then + throw_error('Unknown keys: ' .. render(unknown)) + end + return result, unknown, raw end --- Store results @@ -623,38 +1073,12 @@ end --- A table to store parsed key-value results. local result_store = {} ---- The function `save(identifier, result): void` saves a result (a --- table from a previous run of `parse`) under an identifier. --- Therefore, it is not necessary to pollute the global namespace to --- store results for the later usage. --- --- @tparam string identifier The identifier under which the result is --- saved. --- --- @tparam table result A result to be stored and that was created by --- the key-value parser. -local function save(identifier, result) - result_store[identifier] = result -end - ---- The function `get(identifier): table` retrieves a saved result --- from the result store. --- --- @tparam string identifier The identifier under which the result was --- saved. -local function get(identifier) - -- if result_store[identifier] == nil then - -- throw_error('No stored result was found for the identifier \'' .. identifier .. '\'') - -- end - return result_store[identifier] -end - --- Exports -- @section local export = { --- @see default_options - default_options = default_options, + opts = default_options, --- @see stringify stringify = stringify, @@ -662,27 +1086,58 @@ local export = { --- @see parse parse = parse, + define = function(defs, opts) + return function(kv_string, inner_opts) + local options + if inner_opts ~= nil then + options = inner_opts + elseif opts ~= nil then + options = opts + end + + if options == nil then + options = {} + end + + options.defs = defs + + return parse(kv_string, options) + end + end, + --- @see render render = render, - --- @see pretty_print - print = pretty_print, + --- @see debug + debug = debug, - --- @see save - save = save, - - --- @see get - get = get, + --- The function `save(identifier, result): void` saves a result (a + -- table from a previous run of `parse`) under an identifier. + -- Therefore, it is not necessary to pollute the global namespace to + -- store results for the later usage. + -- + -- @tparam string identifier The identifier under which the result is + -- saved. + -- + -- @tparam table result A result to be stored and that was created by + -- the key-value parser. + save = function(identifier, result) + result_store[identifier] = result + end, + + --- The function `get(identifier): table` retrieves a saved result + -- from the result store. + -- + -- @tparam string identifier The identifier under which the result was + -- saved. + get = function(identifier) + -- if result_store[identifier] == nil then + -- throw_error('No stored result was found for the identifier \'' .. identifier .. '\'') + -- end + return result_store[identifier] + end, + + is = is, } --- http://olivinelabs.com/busted/#private -if _TEST then - export.luafy_key = luafy_key - export.luafy_options = luafy_options - export.normalize = normalize - export.normalize_parse_options = normalize_parse_options - export.unpack_single_valued_array_table = unpack_single_valued_array_table - export.visit_parse_tree = visit_parse_tree -end - return export diff --git a/macros/luatex/generic/luakeys/luakeys.sty b/macros/luatex/generic/luakeys/luakeys.sty index 2cf22b837a..51e2c9bec5 100644 --- a/macros/luatex/generic/luakeys/luakeys.sty +++ b/macros/luatex/generic/luakeys/luakeys.sty @@ -17,5 +17,5 @@ % luakeys-debug.sty and luakeys-debug.tex. \NeedsTeXFormat{LaTeX2e} -\ProvidesPackage{luakeys}[2022/04/04 v0.5 Parsing key-value options using Lua.] +\ProvidesPackage{luakeys}[2022/06/09 v0.6 Parsing key-value options using Lua.] \directlua{luakeys = require('luakeys')} -- cgit v1.2.3