texlive[63537] Master/texmf-dist: luakeys (10jun22)

commits+karl at tug.org commits+karl at tug.org
Fri Jun 10 23:08:52 CEST 2022


Revision: 63537
          http://tug.org/svn/texlive?view=revision&revision=63537
Author:   karl
Date:     2022-06-10 23:08:52 +0200 (Fri, 10 Jun 2022)
Log Message:
-----------
luakeys (10jun22)

Modified Paths:
--------------
    trunk/Master/texmf-dist/doc/luatex/luakeys/README.md
    trunk/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.pdf
    trunk/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.tex
    trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty
    trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex
    trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua
    trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty

Modified: trunk/Master/texmf-dist/doc/luatex/luakeys/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/luatex/luakeys/README.md	2022-06-10 21:08:40 UTC (rev 63536)
+++ trunk/Master/texmf-dist/doc/luatex/luakeys/README.md	2022-06-10 21:08:52 UTC (rev 63537)
@@ -30,6 +30,120 @@
 
 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

Modified: trunk/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.tex
===================================================================
--- trunk/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.tex	2022-06-10 21:08:40 UTC (rev 63536)
+++ trunk/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.tex	2022-06-10 21:08:52 UTC (rev 63537)
@@ -1,6 +1,6 @@
 \documentclass{ltxdoc}
 
-\usepackage{hyperref}
+\usepackage[hidelinks]{hyperref}
 \EnableCrossrefs
 \CodelineIndex
 \RecordChanges
@@ -17,9 +17,25 @@
 \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}
 
 \providecommand*{\url}{\texttt}
@@ -30,20 +46,14 @@
   \url{josef at 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}}}
+\InputLuaExample[firstline=4,lastline=7]{first-page.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}
-
 \noindent
 Result:
 
@@ -52,12 +62,11 @@
 {
   ['level1'] = {
     ['level2'] = {
-      ['level3'] = {
-        ['dim'] = 1864679,
-        ['bool'] = true,
-        ['num'] = -0.001
-        ['str'] = 'lua',
-      }
+      ['naked'] = true,
+      ['dim'] = 1864679,
+      ['bool'] = false,
+      ['num'] = -0.001,
+      ['str'] = 'lua,{}',
     }
   }
 }
@@ -77,15 +86,29 @@
 \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},
+% |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. 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{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.
 
@@ -92,21 +115,19 @@
 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.
+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)}.
+{“Parsing complex data formats in LuaTEX with LPEG” (Volume 40 (2019), No. 2)}.
 
 %-----------------------------------------------------------------------
 %
 %-----------------------------------------------------------------------
 
-\clearpage
+\section{How the package is loaded}
 
-\section{Usage}
-
 %%
 %
 %%
@@ -113,29 +134,16 @@
 
 \subsection{Using the Lua module \texttt{luakeys.lua}}
 
-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}.
+% 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|.
 
-\begin{minted}{latex}
-\documentclass{article}
-\directlua{
-  luakeys = require('luakeys')
-}
+\InputLatexExample{loading/lua.tex}
 
-\newcommand{\helloworld}[2][]{
-  \directlua{
-    local keys = luakeys.parse('\luaescapestring{\unexpanded{#1}}')
-    luakeys.print(keys)
-    local marg = '#2'
-    tex.print(keys.greeting .. ', ' .. marg .. keys.punctuation)
-  }
-}
-\begin{document}
-\helloworld[greeting=hello,punctuation=!]{world}
-\end{document}
-\end{minted}
-
 %%
 %
 %%
@@ -142,6 +150,22 @@
 
 \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}
@@ -153,20 +177,8 @@
 \noindent
 It loads the Lua module into the global variable \texttt{luakeys}.
 
-\begin{minted}{latex}
-\documentclass{article}
-\usepackage{luakeys}
+\InputLatexExample{loading/latex.tex}
 
-\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}
-
 %%
 %
 %%
@@ -184,18 +196,883 @@
 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}
-\input luakeys.tex
+\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{
-  local keys = luakeys.parse('one,two,three')
-  tex.print(keys[1])
-  tex.print(keys[2])
-  tex.print(keys[3])
+  luakeys.parse('one,two,three', { debug = true })
 }
-\bye
+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}”}
+
+\InputLuaExample[firstline=4,lastline=5]{defs/attrs/required.lua}
+
+\InputLuaExample[firstline=13,lastline=14]{defs/attrs/required.lua}
+
+\noindent
+A recursive example:
+
+\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}
+
+%%
+%
+%%
+
+\subsubsection{Attribute “\texttt{sub_keys}”}
+
+% 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.
+
+\InputLuaExample[firstline=4,lastline=16]{defs/attrs/sub-keys.lua}
+
+%-----------------------------------------------------------------------
+%
+%-----------------------------------------------------------------------
+
+\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.
+
+\InputLuaExample[firstline=16,lastline=17]{functions/render.lua}
+
+%%
+%
+%%
+
+\subsection{Function \texttt{debug(result): void}}
+
+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.
+
+\InputLuaExample[firstline=4,lastline=5]{functions/debug.lua}
+
+\noindent
+The output should look like this:
+
+\begin{minted}{md}
+{
+  ['level1'] = {
+      ['level2'] = {
+        ['key'] = 'value',
+    },
+  }
+}
+\end{minted}
+
+%%
+%
+%%
+
+\subsection{Function \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{Function \texttt{get(identifier): result}}
+
+The function \lua{get(identifier)} retrieves a saved result from the
+result store.
+
+%%
+%
+%%
+
+\subsection{Table \texttt{is}}
+
+\subsubsection{Function \texttt{is.boolean(value): boolean}}
+\InputLuaExample[firstline=7,lastline=23]{is-table.lua}
+
+\subsubsection{Function \texttt{is.dimension(value): boolean}}
+\InputLuaExample[firstline=27,lastline=37]{is-table.lua}
+
+\subsubsection{Function \texttt{is.integer(value): boolean}}
+\InputLuaExample[firstline=41,lastline=46]{is-table.lua}
+
+\subsubsection{Function \texttt{is.number(value): boolean}}
+\InputLuaExample[firstline=50,lastline=57]{is-table.lua}
+
+\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 @@
 %
 %%
 
-\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,10 +1108,18 @@
   \alt <string-quoted>
   \alt <string-unquoted>
 
+<dimension> ::= <number> <unit>
+
+<number> ::= <sign> ( <integer> [ <fractional> ] | <fractional> )
+
+<fractional> ::= `.' <integer>
+
 <sign> ::= `-' | `+'
 
-<integer> ::= `0' | `1' | `2' | `3' | `4' | `5' | `6' | `7' | `8' | `9'
+<integer> ::= <digit> \{ <digit> \}
 
+<digit> ::= `0' | `1' | `2' | `3' | `4' | `5' | `6' | `7' | `8' | `9'
+
 <unit> ::= `bp' | `BP'
   \alt `cc' | `CC'
   \alt `cm' | `CM'
@@ -297,8 +1182,6 @@
 %
 %%
 
-\clearpage
-
 \subsubsection{number}
 
 \begin{multicols}{2}
@@ -322,35 +1205,14 @@
 %
 %%
 
-\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 @@
 \end{tabular}
 \end{center}
 
-\begin{multicols}{2}
+\columnbreak
+
 \begin{minted}{latex}
 \luakeysdebug{
   bp = 1bp,
@@ -385,6 +1248,9 @@
   sp = 1sp,
 }
 \end{minted}
+
+\columnbreak
+
 \begin{minted}{lua}
 {
   ['bp'] = 65781,
@@ -410,64 +1276,41 @@
 
 \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.
+% 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.
 
-\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}
+\InputLuaExample[firstline=4,lastline=17]{data-types/string.lua}
 
-\subsubsection{Standalone values}
+\subsubsection{Naked keys}
 
-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).
+% 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{multicols}{2}
-\begin{minted}{latex}
-\luakeysdebug{one,two,three}
-\end{minted}
-\columnbreak
-\begin{minted}{lua}
-{ 'one', 'two', 'three' }
-\end{minted}
+\InputLatexExample[firstline=5,lastline=12]{luakeysdebug/naked-keys.tex}
 
 \noindent
-is equivalent to
-
-\begin{minted}{lua}
-{
-  [1] = 'one',
-  [2] = 'two',
-  [3] = 'three',
-}
-\end{minted}
-\end{multicols}
-
-\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 @@
 
 \clearpage
 
-\section{Exported functions of the Lua module \texttt{luakeys.lua}}
+\section{Examples}
 
-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:
+\subsection{Extend and modify keys of existing macros}
 
-\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}
+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}
 
-%%
-%
-%%
+\InputLuaExample{extend-includegraphics/extend-keys.lua}
+\InputLatexExample{extend-includegraphics/extend-keys.tex}
 
-\subsection{\texttt{parse(kv\_string, options)}: table}
+\subsection{Process document class options}
 
-The function \lua{parse(input_string, options)} is the main method of
-this module. It parses a key-value string into a Lua table.
-
 \begin{minted}{latex}
-\newcommand{\mykeyvalcmd}[1][]{
-  \directlua{
-    result = luakeys.parse('#1')
-    luakeys.print(result)
-  }
-  #2
-}
-\mykeyvalcmd[one=1]{test}
+\directlua{luakeys.parse('\@classoptionslist')}
 \end{minted}
 
-\noindent
-In plain \TeX:
+\InputLatexExample{class-options/test-class.cls}
+\InputLatexExample{class-options/use-test-class.tex}
 
-\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)
-\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}}}
-
-\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 @@
 \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 @@
 
 \clearpage
 
-\subsection{luakeys.tex}
+\subsection{luakeys.sty}
 
-\inputminted[linenos=true]{latex}{luakeys.tex}
+\inputminted[linenos=true]{latex}{luakeys.sty}
 
 %%
 %
@@ -766,10 +1477,23 @@
 }
 \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
 \pagebreak

Modified: trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty	2022-06-10 21:08:40 UTC (rev 63536)
+++ trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty	2022-06-10 21:08:52 UTC (rev 63537)
@@ -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

Modified: trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex	2022-06-10 21:08:40 UTC (rev 63536)
+++ trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex	2022-06-10 21:08:52 UTC (rev 63537)
@@ -37,7 +37,10 @@
 }%
 \def\luakeysdebug at parse@options#1{
   \directlua{
-    luakeys.save('debug_options', luakeys.parse('#1'))
+    luakeys.save(
+      'debug_options',
+      luakeys.parse('#1', { format_keys = { 'snake', 'lower' } })
+    )
   }
 }%
 \def\luakeysdebug at 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)
     }
   }
 }%

Modified: trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua	2022-06-10 21:08:40 UTC (rev 63536)
+++ trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua	2022-06-10 21:08:52 UTC (rev 63537)
@@ -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
 
-  -- Dummy function for the tests.
-  tex['sp'] = function (input)
-    return 1234567
+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,
+}
+
+--- 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,77 +138,185 @@
   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 = {}
+-- @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
-  local options = {}
-  for key, value in pairs(raw_options) do
-    options[luafy_key(key)] = value
-  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 = {}
+-- @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
 
-  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]
+  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
 -- @section
 
 --- 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 @@
     end
   end
 
+  -- LuaFormatter off
   return Pattern({
-    'list',
+    [1] = initial_rule,
 
     -- list_item*
     list = CaptureFolding(
@@ -220,6 +390,9 @@
       Variable('string_quoted') * -Variable('value') +
       Variable('string_unquoted'),
 
+    -- for is.boolean()
+    boolean_only = Variable('boolean') * -1,
+
     -- boolean_true / boolean_false
     boolean =
       (
@@ -237,19 +410,32 @@
       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
     unit =
@@ -267,18 +453,6 @@
       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 @@
 
     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
+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
-  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')
-  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 @@
     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)
+local is = {
+  boolean = function(value)
+    if value == nil then
+      return false
+    end
+    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,
+
+  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,
+}
+
+--- Apply the key-value-pair definitions (defs) on an input table in a
+--- recursive fashion.
+---
+--- at param defs table A table containing all definitions.
+--- at param opts table The parse options table.
+--- at param input table The current input table.
+--- at param output table The current output table.
+--- at param unknown table Always the root unknown table.
+--- at param key_path table An array of key names leading to the current
+--- at 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
+
+  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
+
+  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
+
+  local apply = {
+    alias = function(value, key, def)
+      if type(def.alias) == 'string' then
+        def.alias = { def.alias }
       end
-      if type(value) == 'table' then
-        result[key] = normalize_recursive(value, {}, options)
-      else
-        result[key] = value
+      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
-    end
-    return result
+      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,
+
+    always_present = function(value, key, def)
+      if value == nil and def.always_present then
+        return set_default_value(def)
+      end
+    end,
+
+    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,
+
+    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
+            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
+          throw_error('Unknown data type: ' .. def.data_type)
+        end
+        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
-  raw = normalize_recursive(raw, {}, options)
+  if unknown == nil then
+    unknown = {}
+  end
+  if key_path == nil then
+    key_path = {}
+  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
+  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
+
+    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
-      return key, value
-    end)
+    end
+
+    output[key] = value
   end
 
-  if options.case_insensitive_keys then
-    raw = visit_parse_tree(raw, function (key, value)
-      if type(key) == 'string' then
-        return key:lower(), value
+  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
-      return key, value
-    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 raw
+  return output, unknown
 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:
+--- Parse a LaTeX/TeX style key-value string into a Lua table.
+---
+--- at param kv_string string A string in the TeX/LaTeX style key-value format as described above.
+--- at param opts table A table containing the settings:
+---   `convert_dimensions`, `unpack`, `naked_as_value`, `converter`,
+---   `debug`, `preprocess`, `postprocess`.
 --
---     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)
+--- at return table result The final result of all individual parsing and normalization steps.
+--- at return table unknown A table with unknown, undefinied key-value pairs.
+--- at return table raw The unprocessed, raw result of the LPeg parser.
+local function parse(kv_string, opts)
   if kv_string == nil then
     return {}
   end
-  options = normalize_parse_options(options)
 
-  local parser = generate_parser(options)
-  local parse_tree = parser:match(kv_string)
-
-  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
-
---- 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.
---
--- @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)
+  --- 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, 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('},')
-          else
-            add(key .. '={},')
-          end
-        else
-          add(key .. '=' .. tostring(value) .. ',')
-        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
-        add(tostring(value) .. ',')
+        opts[name] = default_options[name]
       end
     end
-    return table.concat(output)
-  end
-  return render_inner(tbl)
-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.
---
--- https://stackoverflow.com/a/54593224/10193818
-local function stringify(input, for_tex)
-  local line_break, start_bracket, end_bracket, indent
+    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,
+    }
 
-  if for_tex then
-    line_break = '\\par'
-    start_bracket = '$\\{$'
-    end_bracket = '$\\}$'
-    indent = '\\ \\ '
-  else
-    line_break = '\n'
-    start_bracket = '{'
-    end_bracket = '}'
-    indent = '  '
+    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 stringify_inner(input, depth)
-    local output = {}
-    depth = depth or 0
+  if type(opts.hooks.kv_string) == 'function' then
+    kv_string = opts.hooks.kv_string(kv_string)
+  end
 
-    local function add(depth, text)
-      table.insert(output, string.rep(indent, depth) .. text)
-    end
+  local result = generate_parser('list', opts.convert_dimensions):match(
+    kv_string)
+  local raw = clone_table(result)
 
-    local function format_key(key)
-      if (type(key) == 'number') then
-        return string.format('[%s]', key)
+  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 .. ',')
+  --- 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,
+
+      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
-        else
-          if (type(value) == 'string') then
-            value = string.format('\'%s\'', value)
-          else
-            value = tostring(value)
-          end
+        end
+        return key, value
+      end,
+    }
 
-          add(depth, 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)
 
-  return start_bracket .. line_break .. stringify_inner(input, 1) .. line_break .. end_bracket
-end
+  -- 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
 
---- 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))
+  apply_hooks()
+
+  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 @@
 --- 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 @@
   --- @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,
+  --- 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,
 
-  --- @see get
-  get = get,
+  --- 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

Modified: trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty	2022-06-10 21:08:40 UTC (rev 63536)
+++ trunk/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty	2022-06-10 21:08:52 UTC (rev 63537)
@@ -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')}



More information about the tex-live-commits mailing list.