diff options
-rw-r--r-- | Master/texmf-dist/doc/luatex/luakeys/README.md | 33 | ||||
-rw-r--r-- | Master/texmf-dist/doc/luatex/luakeys/documentation.tex (renamed from Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.tex) | 403 | ||||
-rw-r--r-- | Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.pdf | bin | 398632 -> 0 bytes | |||
-rw-r--r-- | Master/texmf-dist/doc/luatex/luakeys/luakeys.pdf | bin | 0 -> 427721 bytes | |||
-rw-r--r-- | Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty | 4 | ||||
-rw-r--r-- | Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex | 4 | ||||
-rw-r--r-- | Master/texmf-dist/tex/luatex/luakeys/luakeys.lua | 2734 | ||||
-rw-r--r-- | Master/texmf-dist/tex/luatex/luakeys/luakeys.sty | 15 | ||||
-rw-r--r-- | Master/texmf-dist/tex/luatex/luakeys/luakeys.tex | 9 |
9 files changed, 1938 insertions, 1264 deletions
diff --git a/Master/texmf-dist/doc/luatex/luakeys/README.md b/Master/texmf-dist/doc/luatex/luakeys/README.md index 2779de92e83..b4a0ddb8a88 100644 --- a/Master/texmf-dist/doc/luatex/luakeys/README.md +++ b/Master/texmf-dist/doc/luatex/luakeys/README.md @@ -14,7 +14,7 @@ mechanism should be pretty robust. ## License -Copyright 2021-2022 Josef Friedrich +Copyright 2021-2023 Josef Friedrich This work may be distributed and/or modified under the conditions of the LaTeX Project Public License, either version 1.3c @@ -48,11 +48,16 @@ local defs = { -- Only values listed in the array table are allowed. choices = { 'one', 'two', 'three' }, - -- Possible data types: boolean, dimension, integer, number, string + -- Possible data types: + -- any, boolean, dimension, integer, number, string, list data_type = 'string', + -- To provide a default value for each naked key individually. default = true, + -- Can serve as a comment. + description = 'Describe your key-value pair.', + -- The key belongs to a mutually exclusive group of keys. exclusive_group = 'name', @@ -64,11 +69,27 @@ local defs = { -- The name of the key, can be omitted name = 'key', + + -- Convert opposite (naked) keys + -- into a boolean value and store this boolean under a target key: + -- show -> opposite_keys = true + -- hide -> opposite_keys = false + -- Short form: opposite_keys = { 'show', 'hide' } opposite_keys = { [true] = 'show', [false] = 'hide' }, + + -- Pick a value by its data type: + -- 'any', 'string', 'number', 'dimension', 'integer', 'boolean'. + pick = false, -- ’false’ disables the picking. + + -- A function whose return value is passed to the key. process = function(value, input, result, unknown) return value end, + + -- To enforce that a key must be specified. required = true, + + -- To build nested key-value pair definitions. sub_keys = { key_level_2 = { } }, } } @@ -78,6 +99,9 @@ local defs = { ```lua local opts = { + -- Result table that is filled with each call of the parse function. + accumulated_result = accumulated_result, + -- Configure the delimiter that assigns a value to a key. assignment_operator = '=', @@ -176,9 +200,6 @@ local result = luakeys.parse('one,two,three', opts) [Lua](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) extension in Visual Studio Code. This extension understands the modified [EmmyLua annotations](https://github.com/sumneko/lua-language-server/wiki/Annotations). -The Lua source code documentation is generated with -[LDoc](https://stevedonovan.github.io/ldoc/manual/doc.md.html). - The Lua code is automatically formatted with the help of the [LuaFormatter](https://github.com/Koihik/LuaFormatter). @@ -212,7 +233,7 @@ This project uses [semantic versioning](https://semver.org). Update version in: -* luakeys-doc.tex +* documentation.tex * luakeys-debug.sty * luakeys.sty * luakeys.lua diff --git a/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.tex b/Master/texmf-dist/doc/luatex/luakeys/documentation.tex index 90041ec5816..42da7c1acc3 100644 --- a/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.tex +++ b/Master/texmf-dist/doc/luatex/luakeys/documentation.tex @@ -13,9 +13,9 @@ \usepackage{syntax} \usepackage{graphicx} +\definecolor{bg}{rgb}{0.95,0.95,0.95} + \usemintedstyle{friendly} -\BeforeBeginEnvironment{minted}{\begin{mdframed}} -\AfterEndEnvironment{minted}{\end{mdframed}} \setminted{ breaklines=true, fontsize=\footnotesize, @@ -25,15 +25,19 @@ \def\latex#1{\mintinline{latex}|#1|} \NewDocumentCommand { \InputLatex } { O{} m } { - \begin{mdframed} - \inputminted[linenos=false,#1]{latex}{examples/#2} - \end{mdframed} + \inputminted[ + linenos=false, + bgcolor=bg, + #1 + ]{latex}{examples/#2} } \NewDocumentCommand { \InputLua } { O{} m } { - \begin{mdframed} - \inputminted[linenos=false,#1]{lua}{examples/#2} - \end{mdframed} + \inputminted[ + linenos=false, + bgcolor=bg, + #1 + ]{lua}{examples/#2} } \catcode`_=12 @@ -60,7 +64,7 @@ The default value of the option “\texttt{#1}” is: \href{https://github.com/Josef-Friedrich/luakeys} {github.com/Josef-Friedrich/luakeys}% } -\date{0.11.0 from 2022/12/23} +\date{v0.12.0 from 2023/01/05} \maketitle @@ -103,7 +107,7 @@ Result: \noindent % |luakeys| ist ein Lua-Modul / Lua\TeX package, das wie die Pakete ... % Schlüssel-Wert-Optionen analysieren kann. -|luakeys| is a Lua module / Lua\TeX package that can parse key-value +|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}, @@ -137,6 +141,18 @@ The TUGboat article {“Implementing key–value input: An introduction” (Volume 30 (2009), No. 1)} by \emph{Joseph Wright} and \emph{Christian Feuersänger} gives a good overview of the available key-value packages. +% +% Dieser Artikel geht auf eine auf tex.stackexchange.com von Will +% Robertson: gestellte Frage zurück. +This article is based on a question asked on tex.stackexchange.com by +Will Robertson: +\href{https://tex.stackexchange.com/questions/26771} +{A big list of every keyval package}. +% +% CTAN stellt auch eine Übersichtsseite zum Thema bereit. +CTAN also provides an overview page on the subject of +\href{https://www.ctan.org/topic/keyval} +{Key-Val: packages with key-value argument systems}. % Dieses Paket wäre ohne den Artikel % \href{https://tug.org/TUGboat/tb40-2/tb125menke-lpeg.pdf} @@ -154,9 +170,8 @@ No. 2)}. % context) analysiert werden. \item Key-value pairs can be parsed independently of the macro collection (\LaTeX{} or Con\TeX{}t). - -% \item Sogar in Plain LuaTex können Schlüssel analysiert werden -\item Even in plain Lua\TeX{} keys can be parsed. +% Sogar in Plain LuaTex können Schlüssel analysiert werden +Even in plain Lua\TeX{} keys can be parsed. % \item |luakeys| kann mit ineinander verschachtelten listen an % Schlüssel-Wert-Paaren umgehen, d. h. es kann mit einer rekursiven @@ -222,18 +237,10 @@ wrapper files. Probably MiK\TeX{} is searching for an occurrence of the \LaTeX{} macro “\latex{\usepackage{luakeys}}”. % -% Die mitgelieferte Lua\LaTeX{}-Datei ist recht klein: -The supplied Lua\LaTeX{} file is quite small: - -\begin{minted}{latex} -\NeedsTeXFormat{LaTeX2e} -\ProvidesPackage{luakeys} -\directlua{luakeys = require('luakeys')} -\end{minted} - -\noindent -% Es lädt das Lua-Modul in die globale Variable \texttt{luakeys}. -It loads the Lua module into the global variable \texttt{luakeys}. +% Die Datei \texttt{luakeys.sty} lädt das Lua-Modul in die globale +% Variable \texttt{luakeys}. +The \texttt{luakeys.sty} file loads the Lua module into the global +variable \texttt{luakeys}. \InputLatex{loading/tex-latex.tex} @@ -245,22 +252,13 @@ It loads the Lua module into the global variable \texttt{luakeys}. % \texttt{luakeys.tex}} \subsection{Using the plain Lua\TeX{} wrapper \texttt{luakeys.tex}} -% Noch kleiner ist die Datei \texttt{luakeys.tex}. -Even smaller is the file \texttt{luakeys.tex}. -% -% Sie besteht aus nur einer Zeile: -It consists of only one line: - -\begin{minted}{latex} -\directlua{luakeys = require('luakeys')} -\end{minted} - \noindent % Es macht dasselbe wie das Lua\LaTeX{}-Hüllpacket und lädt das % Lua-Modul \texttt{luakeys.lua} in die globale Variable % \texttt{luakeys}. -It does the same as the Lua\LaTeX{} wrapper and loads the Lua module -\texttt{luakeys.lua} into the global variable \texttt{luakeys}. +The file \texttt{luakeys.tex} does the same as the Lua\LaTeX{} wrapper +and loads the Lua module \texttt{luakeys.lua} into the global variable +\texttt{luakeys}. \InputLatex{loading/tex-plain.tex} @@ -270,27 +268,15 @@ It does the same as the Lua\LaTeX{} wrapper and loads the Lua module \section{Lua interface / API} +% Luakeys exportiert nur eine Funktion, die aufgerufen werden muss, um auf +% die öffentliche API zuzugreifen +Luakeys exports only one function that must be called to access the +public API. +% % Das Lua-Modul exportiert diese Funktionen und Tabellen: The Lua module exports this functions and tables: -\InputLua[firstline=3,lastline=15]{export.lua} - -\noindent -% Diese Dokumentation stellt nur die öffentlichen Funktionen und -% Tabellen vor. -This documentation presents only the public functions and tables. -% -% Um mehr über die privaten, nicht exportierten Funktionen zu erfahren, -% lesen Sie bitte die -% \href{https://josef-friedrich.github.io/luakeys/}{Quellcode -% Dokumentation}, die mit -% \href{http://stevedonovan.github.io/ldoc/}{LDoc} erstellt wurde. -To learn more about the private, not exported 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}. - -\subsection{Lua indentifier names} +\InputLua[firstline=3,lastline=16]{export.lua} % Das Projekt verwendet einige wenige Abkürzungen für Variablennamen, % die hoffentlich für externe Leser eindeutig und bekannt sind. @@ -334,34 +320,6 @@ The raw result of the Lpeg grammar parser. \\ % %% -\subsection{Function “\texttt{get_private_instance(): table}”} -\label{function:get-private-instance} - -% Über die Tabelle \lua{opts} können die Standardoptionen global -% geändert werden. -The default options can be changed globally via the \lua{opts} table -(\ref{table-opts}). -% Um zu verhindern, dass andere Module die Standardeinstellungen -% ändern, kann mit Hilfe der Funktion \lua{get_private_instance()} eine neue -% Instanz des Moduls \lua{luakeys} erzeugt werden. -To prevent another modules from changing the default settings, a new -instance of the \lua{luakeys} module can be created using the -\lua{get_private_instance()} function. - -\InputLua[firstline=3,lastline=4]{functions/get-private-instance.lua} - -\noindent -% Die Funktion \lua{require()} gibt standardmäßig beim mehrmaligen -% Aufruf immer die gleiche Tabelle zurück. -The \lua{require()} function always returns the same table -when called multiple times. - -\InputLua[firstline=14,lastline=17]{functions/get-private-instance.lua} - -%% -% -%% - \subsection{Function “\texttt{parse(kv_string, opts): result, unknown, raw}”} \label{parse} @@ -391,7 +349,7 @@ The \lua{parse} function can be called with an options table. This options are supported: \catcode`_=12 \directlua{luakeys.print_names('opts')} -\InputLua[firstline=5,lastline=94]{opts/all-opts.lua} +\InputLua[firstline=7,lastline=99]{opts/all-opts.lua} \subsection{Table “\texttt{opts}”} \label{table-opts} @@ -419,6 +377,23 @@ set the options globally, it is recommended to use the % %% +\subsubsection{Option “\texttt{accumulated_result}”} + +% Streng genommen handelt es sich hier um keine Option +Strictly speaking, this is not an option. +% +% Mit der Option \lua{accumulated_result} kann eine Ergebnistabelle +% angegeben werden, die bei jedem Aufruf der \lua{parse}-Funktion weiter +% befüllt wird. +The \lua{accumulated_result} “option” can be used to specify a result +table that is filled with each call of the \lua{parse} function. + +\InputLua[firstline=5,lastline=14]{opts/accumulated-result.lua} + +%% +% +%% + \subsubsection{Option “\texttt{assignment_operator}”} \label{option:assignment-operator} \label{options-delimiter} @@ -486,7 +461,7 @@ tex.print(number.todimen(tex.sp('1cm'), 'cm', '%0.0F%s')) % Wenn die Option \lua{debug} auf true gesetzt ist, wird die % Ergebnistabelle in der Konsole ausgegeben. -If the option \lua{debug} is set to ture, the result table is printed to +If the option \lua{debug} is set to true, the result table is printed to the console. \InputLatex{opts/debug-latex.tex} @@ -969,7 +944,7 @@ These attributes are allowed: The code example below lists all the attributes that can be used to define key-value pairs. -\InputLua[firstline=5,lastline=46]{defs/all-attrs.lua} +\InputLua[firstline=5,lastline=62]{defs/all-attrs.lua} %% % @@ -1034,8 +1009,8 @@ choices: \subsubsection{Attribute “\texttt{data_type}”} % source: Python argparse documentation. -The \lua{data_type} attribute allows type-checking and type conversions to -be performed. +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: @@ -1043,7 +1018,8 @@ The following data types are supported: \lua{'dimension'}, \lua{'integer'}, \lua{'number'}, -\lua{'string'}. +\lua{'string'}, +\lua{'list'}. % % Bei den drei Datentypen integer, number, dimension kann eine % Typenumwandlung scheitern. @@ -1080,6 +1056,17 @@ value can be specified for all naked keys. % %% +\subsubsection{Attribute “\texttt{description}”} + +% Dieses Attribut wird momentan nicht weiterverarbeitet. +This attribute is currently not processed further. +% Es kann als Kommentar dienen. +It can serve as a comment. + +%% +% +%% + \subsubsection{Attribute “\texttt{exclusive_group}”} % Alle Schlüssel, die der gleichen ausschließenden Gruppe angehören, @@ -1108,48 +1095,6 @@ thrown. % %% -\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}. - -\InputLua[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}. - -\InputLua[firstline=13,lastline=13]{defs/attrs/opposite-keys.lua} - -%% -% -%% - \subsubsection{Attribute “\texttt{macro}”} The attribute \texttt{macro} stores the value in a \TeX{} macro. @@ -1251,6 +1196,59 @@ Instead of ... % %% +\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}. + +\InputLua[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}. + +\InputLua[firstline=13,lastline=13]{defs/attrs/opposite-keys.lua} + +\noindent +% Gegensätzliche Schlüsselpaare können in einer Kurzschreibweise +% angegeben werden, nämlich als Liste +Opposing key pairs can be specified in a short form, namely as a list: +% Der gegensätzliche Schlüssel, der den Wahrwert repräsentiert, muss in +% dieser Liste zuerst angegeben werden, dann folgt der Falschwert. +The opposite key, which represents the true value, must be specified +first in this list, followed by the false value. + +\InputLua[firstline=19,lastline=21]{defs/attrs/opposite-keys.lua} + +%% +% +%% + \subsubsection{Attribute “\texttt{pick}”} % Das Attribut \lua{pick} sucht nach einem Wert, der keinem Schlüssel @@ -1278,13 +1276,7 @@ The search for values is activated when the attribute \lua{pick} is set to a data type. % Mit diesen Datentypen kann nach Werten gesucht werden These data types can be used to search for values: -\directlua{ - local types = {} - for t, fn in pairs(luakeys.is) do - table.insert(types, t) - end - tex.print(table.concat(types, ', ')) -}. +string, number, dimension, integer, boolean, any. % Verwendet den Datentyp any um jeden beliebigen Wert zu akzeptieren. Use the data type “any” to accept any value. % Wird einem Schlüssel bereits bei der Eingabe ein Wert zugewiesen, dann @@ -1488,9 +1480,9 @@ result store. In the table \lua{is} some functions are summarized, which check whether an input corresponds to a certain data type. % -% Alle Funktionen akzeptieren nicht nur die entsprechenden Lua-Datentypen, -% sondern auch Eingaben als Zeichenketten. -All functions accept not only the corresponding Lua data types, but also +% Einige Funktionen akzeptieren nicht nur die entsprechenden +% Lua-Datentypen, sondern auch Eingaben als Zeichenketten. +Some functions accept not only the corresponding Lua data types, but also input as strings. % % Beispielsweise wird die Zeichenkette \lua{'true'} von der @@ -1513,6 +1505,9 @@ For example, the string \lua{'true'} is recognized by the \subsubsection{Function “\texttt{is.string(value): boolean}”} \InputLua[firstline=6,lastline=12]{functions/is/string.lua} +\subsubsection{Function “\texttt{is.list(value): boolean}”} +\InputLua[firstline=6,lastline=16]{functions/is/list.lua} + \subsubsection{Function “\texttt{is.any(value): boolean}”} % Die Funktion \lua{is.any(value)} gibt immer wahr zurück und @@ -1526,6 +1521,44 @@ therefore accepts any data type. \subsection{Table “\texttt{utils}”} +% In der Tabelle \lua{utils} sind einige Hilfsfunktionen gebündelt. +The \lua{utils} table bundles some auxiliary functions. + +\InputLua[firstline=3,lastline=34]{utils/all.lua} + +%% +% +%% + +\subsubsection{Function “\texttt{utils.merge_tables(target, source, overwrite): table}”} + +% Die Funktion \lua{merge_tables} führt zwei Tabellen in die erste +% angegebene Tabelle zusammen. Sie kopiert Schlüssel aus der +% `source` Tabelle in die `target`-Tabelle. Sie gibt die geänderte +% Zieltabelle zurück. +The function \lua{merge_tables} merges two tables into the first +specified table. It copies keys from the `source` table into the +`target` table. It returns the target table. + +% Wird der Parameter \lua{overwrite} auf wahr gesetzten, so werden Werte +% in der Zieltabelle überschrieben +If the \lua{overwrite} parameter is set to \lua{true}, values in the +target table are overwritten. + +\InputLua[firstline=4,lastline=8]{utils/merge-tables.lua} + +\noindent +% Geben sie dem Parameter \lua{overwrite} den Wert \lua{false}, um Werte +% in der Zieltabelle zu überschreiben. +Give the parameter \lua{overwrite} the value \lua{false} to overwrite +values in the target table. + +\InputLua[firstline=14,lastline=18]{utils/merge-tables.lua} + +%% +% +%% + \subsubsection{Function “\texttt{utils.scan_oarg(initial_delimiter?, end_delimiter?): string}”} % Plain \TeX{} kennt keine optionalen Argumente (oarg). @@ -1848,7 +1881,7 @@ In Lua an array is a table with numeric indexes (The first index is 1). \InputLatex[firstline=5,lastline=12]{luakeysdebug/naked-keys-latex.tex} \noindent -% VAlle erkannten Datentypen können als eigenständige Werte verwendet +% Alle erkannten Datentypen können als eigenständige Werte verwendet % werden. All recognized data types can be used as standalone values. @@ -1883,31 +1916,40 @@ between 0 and 1 is allowed and converted into \subsection{Process document class options} % Auf die Optionen einer \LaTeX{} Dokumentenklasse kann über das Macro -% \latex{\@classoptionslist} zu gegriffen werden. +% \latex{\LuakeysGetClassOptions} zu gegriffen werden. The options of a \LaTeX{} document class can be accessed via the -\latex{\@classoptionslist} macro. -% -% Die Zeichenkette des Makros \latex{\@classoptionslist} ist bereits -% expandiert und normalisiert. -The string of the macro \latex{\@classoptionslist} is already expanded -and normalized. +\latex{\LuakeysGetClassOptions} macro. % -% Neben Leerzeichen werden auch die geschweiften Klammern entfernt. -In addition to spaces, the curly brackets are also removed. -% Es ist deshalb nicht möglich, in einander geschachtelte hierarische -% Schlüssel-Wert-Paare über die Option zu verarbeiten. -It is therefore not possible to process nested hierarchical key-value -pairs via the option. +% \latex{\LuakeysGetClassOptions} ist ein anderer Name für +\latex{\LuakeysGetClassOptions} is an alias for + +\begin{center} +\latex{\luaescapestring{\@raw@classoptionslist}}. +\end{center} \InputLatex{class-options/test-class.cls} \InputLatex{class-options/use-test-class-latex.tex} -\begin{minted}{lua} -{ - [1] = '12pt', - [2] = 'landscape', -} -\end{minted} +%% +% +%% + +\subsection{Process package options} + +\noindent +% Auf die Optionen eines \LaTeX{}-Pakets kann über das Macro +% \latex{\LuakeysGetPackageOptions} zu gegriffen werden. +The options of a \LaTeX{} package can be accessed via the +\latex{\LuakeysGetPackageOptions} macro. +% \latex{\LuakeysGetPackageOptions} ist ein anderer Name für +\latex{\LuakeysGetPackageOptions} is an alias for + +\begin{center} +\latex{\luaescapestring{\@ptionlist{\@currname.\@currext}}}. +\end{center} + +\InputLatex{package-options/test-package.sty} +\InputLatex{package-options/use-test-package-latex.tex} %----------------------------------------------------------------------- % @@ -2027,30 +2069,30 @@ An example of how to use the command in \LaTeX: \inputminted[linenos=true]{latex}{luakeys-debug.sty} -\changes{0.1.0}{2021/01/18}{Inital release} -\changes{0.2.0}{2021/09/19}{ +\changes{v0.1.0}{2021/01/18}{Inital release} +\changes{v0.2.0}{2021/09/19}{ * Allow all recognized data types as keys. * Allow TeX macros in the values. * New public Lua functions: save(identifier, result), get(identifier). } -\changes{0.3.0}{2021/11/05}{ +\changes{v0.3.0}{2021/11/05}{ * Add a LuaLaTeX wrapper “luakeys.sty”. * Add a plain LuaTeX wrapper “luakeys.tex”. * Rename the previous documentation file “luakeys.tex” to luakeys-doc.tex”. } -\changes{0.4.0}{2021/12/31}{ +\changes{v0.4.0}{2021/12/31}{ * Parser: Add support for nested tables (for example {{'a', 'b'}}). * Parser: Allow only strings and numbers as keys. * Parser: Remove support from Lua numbers with exponents (for example '5e+20'). * Switch the Lua testing framework to busted. } -\changes{0.5.0}{2022/04/04}{ +\changes{v0.5.0}{2022/04/04}{ * Add possibility to change options globally. * New option: standalone_as_true. * Add a recursive converter callback / hook to process the parse tree. * New option: case_insensitive_keys. } -\changes{0.6.0}{2022/06/09}{ +\changes{v0.6.0}{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”. @@ -2063,7 +2105,7 @@ An example of how to use the command in \LaTeX: produce the previous effect. * The function “print()” is now called “debug()”. } -\changes{0.7.0}{2022/07/06}{ +\changes{v0.7.0}{2022/07/06}{ * The project now uses semantic versioning. * New definition attribute “pick” to pick standalone values and assign them to a key. @@ -2071,13 +2113,13 @@ An example of how to use the command in \LaTeX: that means scan for tokens that are enclosed in square brackets. * Extend and improve the documentation. } -\changes{0.8.0}{2022/11/17}{ +\changes{v0.8.0}{2022/11/17}{ * Add 6 new options to change the delimiters: “assignment_operator”, “group_begin”, “group_end”, “list_separator”, “quotation_begin”, “quotation_end”. * Extend the documentation about the option “format_keys”. } -\changes{0.9.0}{2022/11/21}{ +\changes{v0.9.0}{2022/11/21}{ * The definition attibute “pick” accepts a new data type: “any”. * The attribute value “true” for the attribute “pick” is deprecated. * The attribute “pick” accepts now multiple data types specified in @@ -2085,15 +2127,32 @@ An example of how to use the command in \LaTeX: * Add a new function called “any(value)” in the “is” table that accepts any data type. } -\changes{0.10.0}{2022/12/16}{ +\changes{v0.10.0}{2022/12/16}{ * Add support for an invert flat that flips the value of naked keys. * Add new options to specify which strings are recognized as Boolean values. } -\changes{0.11.0}{2022/12/23}{ +\changes{v0.11.0}{2022/12/23}{ * Add a new function called “get_private_instance()” to load a private version of the luakeys module. } +\changes{v0.12.0}{2023/01/05}{ +Added +* Macros \cmd{\LuakeysGetPackageOptions}, \cmd{\LuakeysGetClassOptions}. +* Option “accumulated_result”. +* Data type “list” to the attribute “data_type”. +* Attribute “description”. +* Tables “utils.log” and “utils.ansi_color”. +* Table “errors_message” to set custom messages. +* Short form syntax for the definition attribute “opposite_keys”. +Changed +* Breaking change! luakeys exports now a function instead of a table. + Use “require('luakeys')()” or “luakeys.new()” instead of + “require('luakeys')”. +* Breaking change! “luakeys.parse()”, “luakeys.define()”, “luakeys.save()” + and “luakeys.get()” can’t be used anymore from the global variable luakeys. +* New name for the function “new()” instead of “get_private_instance()". +} \pagebreak \PrintChanges diff --git a/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.pdf b/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.pdf Binary files differdeleted file mode 100644 index ff012a46c63..00000000000 --- a/Master/texmf-dist/doc/luatex/luakeys/luakeys-doc.pdf +++ /dev/null diff --git a/Master/texmf-dist/doc/luatex/luakeys/luakeys.pdf b/Master/texmf-dist/doc/luatex/luakeys/luakeys.pdf Binary files differnew file mode 100644 index 00000000000..5d53f1a31a7 --- /dev/null +++ b/Master/texmf-dist/doc/luatex/luakeys/luakeys.pdf diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty b/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty index ceefd6d0e2b..aa82ed78be7 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.sty @@ -1,5 +1,5 @@ %% luakeys-debug.sty -%% Copyright 2021-2022 Josef Friedrich +%% Copyright 2021-2023 Josef Friedrich % % This work may be distributed and/or modified under the % conditions of the LaTeX Project Public License, either version 1.3c @@ -17,6 +17,6 @@ % luakeys-debug.sty and luakeys-debug.tex. \NeedsTeXFormat{LaTeX2e} -\ProvidesPackage{luakeys-debug}[2022/12/23 0.11.0 Debug package for luakeys.] +\ProvidesPackage{luakeys-debug}[2023/01/05 v0.12.0 Debug package for luakeys.] \input luakeys-debug.tex diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex b/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex index b78951779b1..0b4830f7799 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys-debug.tex @@ -1,5 +1,5 @@ %% luakeys-debug.tex -%% Copyright 2021-2022 Josef Friedrich +%% Copyright 2021-2023 Josef Friedrich % % This work may be distributed and/or modified under the % conditions of the LaTeX Project Public License, either version 1.3c @@ -18,7 +18,7 @@ \directlua { - luakeys = require('luakeys') + luakeys = require('luakeys')() } \def\luakeysdebug% diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua b/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua index 7e86d729cd7..c3da56e7166 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys.lua @@ -1,110 +1,126 @@ --- luakeys.lua --- Copyright 2021-2022 Josef Friedrich --- --- This work may be distributed and/or modified under the --- conditions of the LaTeX Project Public License, either version 1.3c --- of this license or (at your option) any later version. --- The latest version of this license is in --- http://www.latex-project.org/lppl.txt --- and version 1.3c or later is part of all distributions of LaTeX --- version 2008/05/04 or later. --- --- This work has the LPPL maintenance status `maintained'. --- --- The Current Maintainer of this work is Josef Friedrich. --- --- 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. --- --- @module luakeys +---luakeys.lua +---Copyright 2021-2023 Josef Friedrich +--- +---This work may be distributed and/or modified under the +---conditions of the LaTeX Project Public License, either version 1.3c +---of this license or (at your option) any later version. +---The latest version of this license is in +---http://www.latex-project.org/lppl.txt +---and version 1.3c or later is part of all distributions of LaTeX +---version 2008/05/04 or later. +--- +---This work has the LPPL maintenance status `maintained'. +--- +---The Current Maintainer of this work is Josef Friedrich. +--- +---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. +--- +---@module luakeys local lpeg = require('lpeg') if not tex then + ---Dummy functions for the tests. tex = { - -- Dummy function for the tests. sp = function(input) return 1234567 end, } -end -if not token then token = { set_macro = function(csname, content, global) end, } end ---- Merge two tables in the first specified table. ---- The `merge_tables` function copies all keys from the `source` table ---- to a target table. It returns the modified target table. ---- ----@see https://stackoverflow.com/a/1283608/10193818 ---- ----@param target table ----@param source table ---- ----@return table target The modified target table. -local function merge_tables(target, source) - for key, value in pairs(source) do - if type(value) == 'table' then - if type(target[key] or false) == 'table' then - merge_tables(target[key] or {}, source[key] or {}) - elseif target[key] == nil then +local utils = (function() + --- + ---Merge two tables into the first specified table. + ---The `merge_tables` function copies keys from the `source` table + ---to the `target` table. It returns the target table. + --- + ---https://stackoverflow.com/a/1283608/10193818 + --- + ---@param target table # The target table where all values are copied. + ---@param source table # The source table from which all values are copied. + ---@param overwrite? boolean # Overwrite the values in the target table if they are present (default true). + --- + ---@return table target The modified target table. + local function merge_tables(target, source, overwrite) + if overwrite == nil then + overwrite = true + end + for key, value in pairs(source) do + if type(value) == 'table' and type(target[key] or false) == + 'table' then + merge_tables(target[key] or {}, source[key] or {}, overwrite) + elseif (not overwrite and target[key] == nil) or + (overwrite and target[key] ~= value) then target[key] = value end - elseif target[key] == nil then - target[key] = value end + return target end - return target -end ----Clone a table. ---- ----@see http://lua-users.org/wiki/CopyTable ---- ----@param orig table ---- ----@return table -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) + --- + ---Clone a table, i.e. make a deep copy of the source table. + --- + ---http://lua-users.org/wiki/CopyTable + --- + ---@param source table # The source table to be cloned. + --- + ---@return table # A deep copy of the source table. + local function clone_table(source) + local copy + if type(source) == 'table' then + copy = {} + for orig_key, orig_value in next, source, nil do + copy[clone_table(orig_key)] = clone_table(orig_value) + end + setmetatable(copy, clone_table(getmetatable(source))) + else ---number, string, boolean, etc + copy = source end - setmetatable(copy, clone_table(getmetatable(orig))) - else -- number, string, boolean, etc - copy = orig + return copy end - return copy -end -local utils = { - --- Get the size of an array like table `{ 'one', 'two', 'three' }` = 3. --- - ---@param value table # A table or any input. + ---Remove an element from a table. --- - ---@return 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 + ---@param source table + ---@param value any + --- + ---@return any|nil + local function remove_from_table(source, value) + for index, v in pairs(source) do + if value == v then + source[index] = nil + return value end end - return count - end, + end + + --- + ---@param source table + --- + ---@return table # An array table with the sorted key names. + local function get_table_keys(source) + local keys = {} + for key in pairs(source) do + table.insert(keys, key) + end + table.sort(keys) + return keys + end + --- ---Get the size of a table `{ one = 'one', 'two', 'three' }` = 3. --- - ---@param value table|any # A table or any input. + ---@param value any # A table or any input. --- ---@return 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 function get_table_size(value) local count = 0 if type(value) == 'table' then for _ in pairs(value) do @@ -112,28 +128,34 @@ local utils = { end end return count - end, - - merge_tables = merge_tables, - - clone_table = clone_table, + end - remove_from_array = function(array, element) - for index, value in pairs(array) do - if element == value then - array[index] = nil - return value + --- + ---Get the size of an array like table, for example `{ 'one', 'two', + ---'three' }` = 3. + --- + ---@param value any # A table or any input. + --- + ---@return number # The size of the array like table. 0 if the input is no table or the table is empty. + local function get_array_size(value) + local count = 0 + if type(value) == 'table' then + for _ in ipairs(value) do + count = count + 1 end end - end, + return count + end + --- ---Scan for an optional argument. --- ---@param initial_delimiter? string # The character that marks the beginning of an optional argument (by default `[`). ---@param end_delimiter? string # The character that marks the end of an optional argument (by default `]`). --- ---@return string|nil # The string that was enclosed by the delimiters. The delimiters themselves are not returned. - scan_oarg = function(initial_delimiter, end_delimiter) + local function scan_oarg(initial_delimiter, + end_delimiter) if initial_delimiter == nil then initial_delimiter = '[' end @@ -167,11 +189,603 @@ local utils = { else token.put_next(t) end - end, -} + end + + --- + ---Throw a single error message. + --- + ---@param message string + ---@param help? table + local function throw_error_message(message, help) + if type(tex.error) == 'function' then + tex.error(message, help) + else + error(message) + end + end + + --- + ---Throw an error by specifying an error code. + --- + ---@param error_messages table + ---@param error_code string + ---@param args? table + local function throw_error_code(error_messages, + error_code, + args) + local template = error_messages[error_code] + + --- + ---@param message string + ---@param a table + --- + ---@return string + local function replace_args(message, a) + for key, value in pairs(a) do + if type(value) == 'table' then + value = table.concat(value, ', ') + end + message = message:gsub('@' .. key, + '“' .. tostring(value) .. '”') + end + return message + end + + --- + ---@param list table + ---@param a table + --- + ---@return table + local function replace_args_in_list(list, a) + for index, message in ipairs(list) do + list[index] = replace_args(message, a) + end + return list + end + + --- + ---@type string + local message + ---@type table + local help = {} + + if type(template) == 'table' then + message = template[1] + if args ~= nil then + help = replace_args_in_list(template[2], args) + else + help = template[2] + end + else + message = template + end + + if args ~= nil then + message = replace_args(message, args) + end + + message = 'luakeys error [' .. error_code .. ']: ' .. message + + for _, help_message in ipairs({ + 'You may be able to find more help in the documentation:', + 'http://mirrors.ctan.org/macros/luatex/generic/luakeys/luakeys-doc.pdf', + 'Or ask a question in the issue tracker on Github:', + 'https://github.com/Josef-Friedrich/luakeys/issues', + }) do + table.insert(help, help_message) + end + + throw_error_message(message, help) + end + + local function visit_tree(tree, callback_func) + if type(tree) ~= 'table' then + throw_error_message( + 'Parameter “tree” has to be a table, got: ' .. + tostring(tree)) + end + 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_tree_recursive(tree, value, {}, depth + 1, + callback_func) + end + + key, value = callback_func(key, value, depth, current, tree) + + if key ~= nil and value ~= nil then + result[key] = value + end + end + if next(result) ~= nil then + return result + end + end + + local result = + visit_tree_recursive(tree, tree, {}, 1, callback_func) + + if result == nil then + return {} + end + return result + end + + ---@alias ColorName 'black'|'red'|'green'|'yellow'|'blue'|'magenta'|'cyan'|'white'|'reset' + ---@alias ColorMode 'bright'|'dim' + + --- + ---Small library to surround strings with ANSI color codes. + -- + ---[SGR (Select Graphic Rendition) Parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) + --- + ---__attributes__ + --- + ---| color |code| + ---|------------|----| + ---| reset | 0 | + ---| clear | 0 | + ---| bright | 1 | + ---| dim | 2 | + ---| underscore | 4 | + ---| blink | 5 | + ---| reverse | 7 | + ---| hidden | 8 | + --- + ---__foreground__ + --- + ---| color |code| + ---|------------|----| + ---| black | 30 | + ---| red | 31 | + ---| green | 32 | + ---| yellow | 33 | + ---| blue | 34 | + ---| magenta | 35 | + ---| cyan | 36 | + ---| white | 37 | + --- + ---__background__ + --- + ---| color |code| + ---|------------|----| + ---| onblack | 40 | + ---| onred | 41 | + ---| ongreen | 42 | + ---| onyellow | 43 | + ---| onblue | 44 | + ---| onmagenta | 45 | + ---| oncyan | 46 | + ---| onwhite | 47 | + local ansi_color = (function() + + --- + ---@param code integer + --- + ---@return string + local function format_color_code(code) + return string.char(27) .. '[' .. tostring(code) .. 'm' + end + + --- + ---@private + --- + ---@param color ColorName # A color name. + ---@param mode? ColorMode + ---@param background? boolean # Colorize the background not the text. + --- + ---@return string + local function get_color_code(color, mode, background) + local output = '' + local code + + if mode == 'bright' then + output = format_color_code(1) + elseif mode == 'dim' then + output = format_color_code(2) + end + + if not background then + if color == 'reset' then + code = 0 + elseif color == 'black' then + code = 30 + elseif color == 'red' then + code = 31 + elseif color == 'green' then + code = 32 + elseif color == 'yellow' then + code = 33 + elseif color == 'blue' then + code = 34 + elseif color == 'magenta' then + code = 35 + elseif color == 'cyan' then + code = 36 + elseif color == 'white' then + code = 37 + else + code = 37 + end + else + if color == 'black' then + code = 40 + elseif color == 'red' then + code = 41 + elseif color == 'green' then + code = 42 + elseif color == 'yellow' then + code = 43 + elseif color == 'blue' then + code = 44 + elseif color == 'magenta' then + code = 45 + elseif color == 'cyan' then + code = 46 + elseif color == 'white' then + code = 47 + else + code = 40 + end + end + return output .. format_color_code(code) + end + + --- + ---@param text any + ---@param color ColorName # A color name. + ---@param mode? ColorMode + ---@param background? boolean # Colorize the background not the text. + --- + ---@return string + local function colorize(text, color, mode, background) + return string.format('%s%s%s', + get_color_code(color, mode, background), text, + get_color_code('reset')) + end + + return { + colorize = colorize, + + --- + ---@param text any + --- + ---@return string + red = function(text) + return colorize(text, 'red') + end, + + --- + ---@param text any + --- + ---@return string + green = function(text) + return colorize(text, 'green') + end, + + ---@return string + yellow = function(text) + return colorize(text, 'yellow') + end, + + --- + ---@param text any + --- + ---@return string + blue = function(text) + return colorize(text, 'blue') + end, + + --- + ---@param text any + --- + ---@return string + magenta = function(text) + return colorize(text, 'magenta') + end, + + --- + ---@param text any + --- + ---@return string + cyan = function(text) + return colorize(text, 'cyan') + end, + } + end)() + + --- + ---A small logging library. + --- + ---Log levels: + --- + ---* 0: silent + ---* 1: error + ---* 2: warn + ---* 3: info + ---* 4: verbose + ---* 5: debug + --- + local log = (function() + ---@private + local opts = { level = 0 } + + ---@private + local function print_message(message, ...) + print(string.format(message, ...)) + end + + --- + ---Set the log level. + --- + ---@param level 0|'silent'|1|'error'|2|'warn'|3|'info'|4|'verbose'|5|'debug' + local function set_log_level(level) + if type(level) == 'string' then + if level == 'silent' then + opts.level = 0 + elseif level == 'error' then + opts.level = 1 + elseif level == 'warn' then + opts.level = 2 + elseif level == 'info' then + opts.level = 3 + elseif level == 'verbose' then + opts.level = 4 + elseif level == 'debug' then + opts.level = 5 + else + throw_error_message(string.format('Unknown log level: %s', + level)) + end + else + if level > 5 or level < 0 then + throw_error_message(string.format( + 'Log level out of range 0-5: %s', level)) + end + opts.level = level + end + + end + + --- + ---Log at level 1 (error). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function error(message, ...) + if opts.level >= 1 then + print_message(message, ...) + end + end + + --- + ---Log at level 2 (warn). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function warn(message, ...) + if opts.level >= 2 then + print_message(message, ...) + end + end + + --- + ---Log at level 3 (info). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function info(message, ...) + if opts.level >= 3 then + print_message(message, ...) + end + end + + --- + ---Log at level 4 (verbose). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function verbose(message, ...) + if opts.level >= 4 then + print_message(message, ...) + end + end + + --- + ---Log at level 5 (debug). + --- + ---The other log levels are: 0 (silent), 1 (error), 2 (warn), 3 (info), 4 (verbose), 5 (debug). + --- + ---@param message string + ---@param ... any + local function debug(message, ...) + if opts.level >= 5 then + print_message(message, ...) + end + end + + return { + set_log_level = set_log_level, + error = error, + warn = warn, + info = info, + verbose = verbose, + debug = debug, + } + end)() + + return { + merge_tables = merge_tables, + clone_table = clone_table, + remove_from_table = remove_from_table, + get_table_keys = get_table_keys, + get_table_size = get_table_size, + get_array_size = get_array_size, + visit_tree = visit_tree, + scan_oarg = scan_oarg, + throw_error_message = throw_error_message, + throw_error_code = throw_error_code, + log = log, + ansi_color = ansi_color, + } +end)() + +--- +---Convert back to strings +---@section +local visualizers = (function() + + --- + ---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 result table # A table to be converted into a key-value string. + --- + ---@return string # A key-value string that can be passed to a TeX macro. + local function render(result) + local function render_inner(result) + local output = {} + local function add(text) + table.insert(output, text) + end + for key, value in pairs(result) do + if (key and type(key) == 'string') then + if (type(value) == 'table') then + if (next(value)) then + add(key .. '={') + add(render_inner(value)) + add('},') + else + add(key .. '={},') + end + else + add(key .. '=' .. tostring(value) .. ',') + end + else + add(tostring(value) .. ',') + end + end + return table.concat(output) + end + return render_inner(result) + 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 `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. + --- + ---@see https://stackoverflow.com/a/54593224/10193818 + --- + ---@param result table # A table to stringify. + ---@param for_tex? boolean # 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. + --- + ---@return string + local function stringify(result, for_tex) + local line_break, start_bracket, end_bracket, indent + + if for_tex then + line_break = '\\par' + start_bracket = '$\\{$' + end_bracket = '$\\}$' + indent = '\\ \\ ' + else + line_break = '\n' + start_bracket = '{' + end_bracket = '}' + indent = ' ' + end + + local function stringify_inner(input, depth) + local output = {} + depth = depth or 0 + + local function add(depth, text) + table.insert(output, string.rep(indent, depth) .. text) + end + + local function format_key(key) + if (type(key) == 'number') then + return string.format('[%s]', key) + else + return string.format('[\'%s\']', key) + end + end + + if type(input) ~= 'table' then + return tostring(input) + end + + for key, value in pairs(input) do + if (key and type(key) == 'number' or type(key) == 'string') then + key = format_key(key) + + if (type(value) == 'table') then + if (next(value)) then + add(depth, key .. ' = ' .. start_bracket) + add(0, stringify_inner(value, depth + 1)) + add(depth, end_bracket .. ','); + else + add(depth, + key .. ' = ' .. start_bracket .. end_bracket .. ',') + end + else + if (type(value) == 'string') then + value = string.format('\'%s\'', value) + else + value = tostring(value) + end + + add(depth, key .. ' = ' .. value .. ',') + end + end + end + + return table.concat(output, line_break) + end + + return start_bracket .. line_break .. stringify_inner(result, 1) .. + line_break .. end_bracket + end + + --- + ---The function `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 + ---`parse`. You have to compile your TeX document in a console to + ---see the terminal output. + --- + ---@param result table # A table to be printed to standard output for debugging purposes. + local function debug(result) + print('\n' .. stringify(result, false)) + end + + return { render = render, stringify = stringify, debug = debug } +end)() local namespace = { opts = { + accumulated_result = false, assignment_operator = '=', convert_dimensions = false, debug = false, @@ -209,6 +823,7 @@ local namespace = { choices = true, data_type = true, default = true, + description = true, exclusive_group = true, l3_tl_set = true, macro = true, @@ -220,1151 +835,1116 @@ local namespace = { required = true, sub_keys = true, }, -} - ---- The default options. -local default_options = clone_table(namespace.opts) -local function throw_error(message) - if type(tex.error) == 'function' then - tex.error(message) - else - error(message) - end -end + error_messages = { + E001 = { + 'Unknown parse option: @unknown!', + { 'The available options are:', '@opt_names' }, + }, + E002 = { + 'Unknown hook: @unknown!', + { 'The available hooks are:', '@hook_names' }, + }, + E003 = 'Duplicate aliases @alias1 and @alias2 for key @key!', + E004 = 'The value @value does not exist in the choices: @choices', + E005 = 'Unknown data type: @unknown', + E006 = 'The value @value of the key @key could not be converted into the data type @data_type!', + E007 = 'The key @key belongs to the mutually exclusive group @exclusive_group and another key of the group named @another_key is already present!', + E008 = 'def.match has to be a string', + E009 = 'The value @value of the key @key does not match @match!', + + E011 = 'Wrong data type in the “pick” attribute: @unknown. Allowed are: @data_types.', + E012 = 'Missing required key @key!', + E013 = 'The key definition must be a table! Got @data_type for key @key.', + E014 = { + 'Unknown definition attribute: @unknown', + { 'The available attributes are:', '@attr_names' }, + }, + E015 = 'Key name couldn’t be detected!', + E017 = 'Unknown style to format keys: @unknown! Allowed styles are: @styles', + E018 = 'The option “format_keys” has to be a table not @data_type', + E019 = 'Unknown keys: @unknown', + + ---Input / parsing error + E021 = 'Opposite key was specified more than once: @key!', + E020 = 'Both opposite keys were given: @true and @false!', + ---Config error (wrong configuration of luakeys) + E010 = 'Usage: opposite_keys = { "true_key", "false_key" } or { [true] = "true_key", [false] = "false_key" } ', + E023 = { + 'Don’t use this function from the global luakeys table. Create a new instance using e. g.: local lk = luakeys.new()', + { + 'This functions should not be used from the global luakeys table:', + 'parse()', + 'save()', + 'get()', + }, + }, + }, +} ---- Normalize the parse options. ---- ----@param opts? table # Options in a raw format. The table may be empty or some keys are not set. --- ----@return table -local function normalize_opts(opts) - if type(opts) ~= 'table' then - opts = {} - end - for key, _ in pairs(opts) do - if namespace.opts[key] == nil then - throw_error('Unknown parse option: ' .. tostring(key) .. '!') - end - end - local old_opts = opts - opts = {} - for name, _ in pairs(namespace.opts) do - if old_opts[name] ~= nil then - opts[name] = old_opts[name] - else - opts[name] = default_options[name] - end - end +---@return table # The public interface of the module. +local function main() - for hook in pairs(opts.hooks) do - if namespace.hooks[hook] == nil then - throw_error('Unknown hook: ' .. tostring(hook) .. '!') - end - end - return opts -end + ---The default options. + local default_opts = utils.clone_table(namespace.opts) -local l3_code_cctab = 10 + local error_messages = utils.clone_table(namespace.error_messages) ---- Convert back to strings --- @section + --- + ---@param error_code string + ---@param args? table + local function throw_error(error_code, args) + utils.throw_error_code(error_messages, error_code, args) + end ---- The function `render(tbl)` reverses the function ---- `parse(kv_string)`. It takes a Lua table and converts this table ---- into a key-value string. The resulting string usually has a ---- different order as the input table. In Lua only tables with ---- 1-based consecutive integer keys (a.k.a. array tables) can be ---- parsed in order. ---- ----@param result table # A table to be converted into a key-value string. ---- ----@return 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) + --- + ---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(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 + for key, _ in pairs(opts) do + if namespace.opts[key] == nil then + throw_error('E001', { + unknown = key, + opt_names = utils.get_table_keys(namespace.opts), + }) + end + end + local old_opts = opts + opts = {} + for name, _ in pairs(namespace.opts) do + if old_opts[name] ~= nil then + opts[name] = old_opts[name] else - add(tostring(value) .. ',') + opts[name] = default_opts[name] end end - return table.concat(output) - end - return render_inner(result) -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 `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. ---- ----@see https://stackoverflow.com/a/54593224/10193818 ---- ----@param result table # A table to stringify. ----@param for_tex? boolean # 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. ---- ----@return string -local function stringify(result, for_tex) - local line_break, start_bracket, end_bracket, indent - - if for_tex then - line_break = '\\par' - start_bracket = '$\\{$' - end_bracket = '$\\}$' - indent = '\\ \\ ' - else - line_break = '\n' - start_bracket = '{' - end_bracket = '}' - indent = ' ' + for hook in pairs(opts.hooks) do + if namespace.hooks[hook] == nil then + throw_error('E002', { + unknown = hook, + hook_names = utils.get_table_keys(namespace.hooks), + }) + end + end + return opts end - local function stringify_inner(input, depth) - local output = {} - depth = depth or 0 + local l3_code_cctab = 10 - local function add(depth, text) - table.insert(output, string.rep(indent, depth) .. text) - end + --- + ---Parser / Lpeg related + ---@section - local function format_key(key) - if (type(key) == 'number') then - return string.format('[%s]', key) - else - return string.format('[\'%s\']', key) - end + ---Generate the PEG parser using Lpeg. + --- + ---Explanations of some LPeg notation forms: + --- + ---* `patt ^ 0` = `expression *` + ---* `patt ^ 1` = `expression +` + ---* `patt ^ -1` = `expression ?` + ---* `patt1 * patt2` = `expression1 expression2`: Sequence + ---* `patt1 + patt2` = `expression1 / expression2`: Ordered choice + --- + ---* [TUGboat article: Parsing complex data formats in LuaTEX with LPEG](https://tug.or-g/TUGboat/tb40-2/tb125menke-Patterndf) + --- + ---@param initial_rule string # The name of the first rule of the grammar table passed to the `lpeg.P(attern)` function (e. g. `list`, `number`). + ---@param opts? table # Whether the dimensions should be converted to scaled points (by default `false`). + --- + ---@return userdata # The parser. + local function generate_parser(initial_rule, opts) + if type(opts) ~= 'table' then + opts = normalize_opts(opts) end - if type(input) ~= 'table' then - return tostring(input) + 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 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 + local line_up_pattern = function(patterns) + local result + for _, pattern in ipairs(patterns) do + if result == nil then + result = Pattern(pattern) else - if (type(value) == 'string') then - value = string.format('\'%s\'', value) - else - value = tostring(value) - end - - add(depth, key .. ' = ' .. value .. ',') + result = result + Pattern(pattern) end end + return result end - return table.concat(output, line_break) - end - - return start_bracket .. line_break .. stringify_inner(result, 1) .. - line_break .. end_bracket -end - ---- The function `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 --- `parse`. You have to compile your TeX document in a console to --- see the terminal output. --- ----@param result table # 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.or-g/TUGboat/tb40-2/tb125menke-Patterndf) ---- ----@param initial_rule string # The name of the first rule of the grammar table passed to the `lpeg.P(attern)` function (e. g. `list`, `number`). ----@param opts? table # Whether the dimensions should be converted to scaled points (by default `false`). ---- ----@return userdata # The parser. -local function generate_parser(initial_rule, opts) - if type(opts) ~= 'table' then - opts = normalize_opts(opts) - 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 - end - - local line_up_pattern = function(patterns) - local result - for _, pattern in ipairs(patterns) do - if result == nil then - result = Pattern(pattern) + --- + ---Convert a dimension to an normalized dimension string or an + ---integer in the scaled points format. + --- + ---@param input string + --- + ---@return integer|string # A dimension as an integer or a dimension string. + local capture_dimension = function(input) + ---Remove all whitespaces + input = input:gsub('%s+', '') + ---Convert the unit string into lowercase. + input = input:lower() + if opts.convert_dimensions then + return tex.sp(input) else - result = result + Pattern(pattern) + return input end end - return result - end - --- Convert a dimension to an normalized dimension string or an - --- integer in the scaled points format. - --- - ---@param input string - --- - ---@return integer|string # A dimension as an integer or a dimension string. - local capture_dimension = function(input) - -- Remove all whitespaces - input = input:gsub('%s+', '') - -- Convert the unit string into lowercase. - input = input:lower() - if opts.convert_dimensions then - return tex.sp(input) - else - return input + --- + ---Add values to a table in two modes: + --- + ---Key-value pair: + --- + ---If `arg1` and `arg2` are not nil, then `arg1` is the key and `arg2` is the + ---value of a new table entry. + --- + ---Indexed value: + --- + ---If `arg2` is nil, then `arg1` is the value and is added as an indexed + ---(by an integer) value. + --- + ---@param result table # The result table to which an additional key-value pair or value should to be added + ---@param arg1 any # The key or the value. + ---@param arg2? any # Always the value. + --- + ---@return table # The result table to which an additional key-value pair or value has been added. + local add_to_table = function(result, arg1, arg2) + if arg2 == nil then + local index = #result + 1 + return rawset(result, index, arg1) + else + return rawset(result, arg1, arg2) + end end - end - --- Add values to a table in two modes: - -- - -- Key-value pair: - -- - -- If `arg1` and `arg2` are not nil, then `arg1` is the key and `arg2` is the - -- value of a new table entry. - -- - -- Indexed value: - -- - -- If `arg2` is nil, then `arg1` is the value and is added as an indexed - -- (by an integer) value. - -- - ---@param result table # The result table to which an additional key-value pair or value should to be added - ---@param arg1 any # The key or the value. - ---@param arg2? any # Always the value. - --- - ---@return table # The result table to which an additional key-value pair or value has been added. - local add_to_table = function(result, arg1, arg2) - if arg2 == nil then - local index = #result + 1 - return rawset(result, index, arg1) - else - return rawset(result, arg1, arg2) - end - end + -- LuaFormatter off + return Pattern({ + [1] = initial_rule, - -- LuaFormatter off - return Pattern({ - [1] = initial_rule, - - -- list_item* - list = CaptureFolding( - CaptureTable('') * Variable('list_item')^0, - add_to_table - ), - - -- '{' list '}' - list_container = - ws(opts.group_begin) * Variable('list') * ws(opts.group_end), - - -- ( list_container / key_value_pair / value ) ','? - list_item = - CaptureGroup( - Variable('list_container') + - Variable('key_value_pair') + - Variable('value') - ) * ws(opts.list_separator)^-1, - - -- key '=' (list_container / value) - key_value_pair = - (Variable('key') * ws(opts.assignment_operator)) * (Variable('list_container') + Variable('value')), - - -- number / string_quoted / string_unquoted - key = - Variable('number') + - Variable('string_quoted') + - Variable('string_unquoted'), - - -- boolean !value / dimension !value / number !value / string_quoted !value / string_unquoted - -- !value -> Not-predicate -> * -Variable('value') - value = - Variable('boolean') * -Variable('value') + - Variable('dimension') * -Variable('value') + - Variable('number') * -Variable('value') + - Variable('string_quoted') * -Variable('value') + - Variable('string_unquoted'), - - -- for is.boolean() - boolean_only = Variable('boolean') * -1, - - -- boolean_true / boolean_false - boolean = - ( - Variable('boolean_true') * CaptureConstant(true) + - Variable('boolean_false') * CaptureConstant(false) + ---list_item* + list = CaptureFolding( + CaptureTable('') * Variable('list_item')^0, + add_to_table ), - boolean_true = line_up_pattern(opts.true_aliases), - - boolean_false = line_up_pattern(opts.false_aliases), - - -- for is.dimension() - dimension_only = Variable('dimension') * -1, - - dimension = ( - Variable('tex_number') * white_space^0 * - Variable('unit') - ) / capture_dimension, - - -- for is.number() - number_only = Variable('number') * -1, - - -- capture number - number = Variable('tex_number') / tonumber, - - -- sign? white_space? (integer+ fractional? / fractional) - tex_number = - 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 - -- https://github.com/TeX-Live/luatex/blob/51db1985f5500dafd2393aa2e403fefa57d3cb76/source/texk/web2c/luatexdir/lua/ltexlib.c#L434-L625 - unit = - Pattern('bp') + Pattern('BP') + - Pattern('cc') + Pattern('CC') + - Pattern('cm') + Pattern('CM') + - Pattern('dd') + Pattern('DD') + - Pattern('em') + Pattern('EM') + - Pattern('ex') + Pattern('EX') + - Pattern('in') + Pattern('IN') + - Pattern('mm') + Pattern('MM') + - Pattern('mu') + Pattern('MU') + - Pattern('nc') + Pattern('NC') + - Pattern('nd') + Pattern('ND') + - Pattern('pc') + Pattern('PC') + - Pattern('pt') + Pattern('PT') + - Pattern('px') + Pattern('PX') + - Pattern('sp') + Pattern('SP'), - - -- '"' ('\"' / !'"')* '"' - string_quoted = - white_space^0 * Pattern(opts.quotation_begin) * - CaptureSimple((Pattern('\\' .. opts.quotation_end) + 1 - Pattern(opts.quotation_end))^0) * - Pattern(opts.quotation_end) * white_space^0, - - string_unquoted = - white_space^0 * - CaptureSimple( - Variable('word_unquoted')^1 * - (Set(' \t')^1 * Variable('word_unquoted')^1)^0) * - white_space^0, - - word_unquoted = (1 - white_space - Set( - opts.group_begin .. - opts.group_end .. - opts.assignment_operator .. - opts.list_separator))^1 - }) + ---'{' list '}' + list_container = + ws(opts.group_begin) * Variable('list') * ws(opts.group_end), + + ---( list_container / key_value_pair / value ) ','? + list_item = + CaptureGroup( + Variable('list_container') + + Variable('key_value_pair') + + Variable('value') + ) * ws(opts.list_separator)^-1, + + ---key '=' (list_container / value) + key_value_pair = + (Variable('key') * ws(opts.assignment_operator)) * (Variable('list_container') + Variable('value')), + + ---number / string_quoted / string_unquoted + key = + Variable('number') + + Variable('string_quoted') + + Variable('string_unquoted'), + + ---boolean !value / dimension !value / number !value / string_quoted !value / string_unquoted + ---!value -> Not-predicate -> * -Variable('value') + value = + Variable('boolean') * -Variable('value') + + Variable('dimension') * -Variable('value') + + Variable('number') * -Variable('value') + + Variable('string_quoted') * -Variable('value') + + Variable('string_unquoted'), + + ---for is.boolean() + boolean_only = Variable('boolean') * -1, + + ---boolean_true / boolean_false + boolean = + ( + Variable('boolean_true') * CaptureConstant(true) + + Variable('boolean_false') * CaptureConstant(false) + ), + + boolean_true = line_up_pattern(opts.true_aliases), + + boolean_false = line_up_pattern(opts.false_aliases), + + ---for is.dimension() + dimension_only = Variable('dimension') * -1, + + dimension = ( + Variable('tex_number') * white_space^0 * + Variable('unit') + ) / capture_dimension, + + ---for is.number() + number_only = Variable('number') * -1, + + ---capture number + number = Variable('tex_number') / tonumber, + + ---sign? white_space? (integer+ fractional? / fractional) + tex_number = + 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 + ---https://github.com/TeX-Live/luatex/blob/51db1985f5500dafd2393aa2e403fefa57d3cb76/source/texk/web2c/luatexdir/lua/ltexlib.c#L434-L625 + unit = + Pattern('bp') + Pattern('BP') + + Pattern('cc') + Pattern('CC') + + Pattern('cm') + Pattern('CM') + + Pattern('dd') + Pattern('DD') + + Pattern('em') + Pattern('EM') + + Pattern('ex') + Pattern('EX') + + Pattern('in') + Pattern('IN') + + Pattern('mm') + Pattern('MM') + + Pattern('mu') + Pattern('MU') + + Pattern('nc') + Pattern('NC') + + Pattern('nd') + Pattern('ND') + + Pattern('pc') + Pattern('PC') + + Pattern('pt') + Pattern('PT') + + Pattern('px') + Pattern('PX') + + Pattern('sp') + Pattern('SP'), + + ---'"' ('\"' / !'"')* '"' + string_quoted = + white_space^0 * Pattern(opts.quotation_begin) * + CaptureSimple((Pattern('\\' .. opts.quotation_end) + 1 - Pattern(opts.quotation_end))^0) * + Pattern(opts.quotation_end) * white_space^0, + + string_unquoted = + white_space^0 * + CaptureSimple( + Variable('word_unquoted')^1 * + (Set(' \t')^1 * Variable('word_unquoted')^1)^0) * + white_space^0, + + word_unquoted = (1 - white_space - Set( + opts.group_begin .. + opts.group_end .. + opts.assignment_operator .. + opts.list_separator))^1 + }) -- LuaFormatter on -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 - 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_tree_recursive(tree, value, {}, depth + 1, - callback_func) + + 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') + local result = parser:match(tostring(value)) + return result ~= nil + end, - key, value = callback_func(key, value, depth, current, tree) + dimension = function(value) + if value == nil then + return false + end + local parser = generate_parser('dimension_only') + local result = parser:match(tostring(value)) + return result ~= nil + end, - if key ~= nil and value ~= nil then - result[key] = value + integer = function(value) + local n = tonumber(value) + if n == nil then + return false end - end - if next(result) ~= nil then - return result - end - end + return n == math.floor(n) + end, - local result = visit_tree_recursive(tree, tree, {}, 1, callback_func) + number = function(value) + if value == nil then + return false + end + if type(value) == 'number' then + return true + end + local parser = generate_parser('number_only') + local result = parser:match(tostring(value)) + return result ~= nil + end, - if result == nil then - return {} - end - return result -end + string = function(value) + return type(value) == 'string' + end, -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') - local result = parser:match(tostring(value)) - return result ~= nil - end, - - dimension = function(value) - if value == nil then - return false - end - local parser = generate_parser('dimension_only') - local result = parser:match(tostring(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, + list = function(value) + if type(value) ~= 'table' then + return false + end - number = function(value) - if value == nil then - return false - end - if type(value) == 'number' then + for k, _ in pairs(value) do + if type(k) ~= 'number' then + return false + end + end return true - end - local parser = generate_parser('number_only') - local result = parser:match(tostring(value)) - return result ~= nil - end, - - string = function(value) - return type(value) == 'string' - end, - - any = function(value) - return true - end, -} - ---- Apply the key-value-pair definitions (defs) on an input table in a ---- recursive fashion. ---- ----@param defs table # A table containing all definitions. ----@param opts table # The parse options table. ----@param input table # The current input table. ----@param output table # The current output table. ----@param unknown table # Always the root unknown table. ----@param key_path table # An array of key names leading to the current ----@param input_root table # The root input table input and output table. -local function apply_definitions(defs, - opts, - input, - output, - unknown, - key_path, - input_root) - local exclusive_groups = {} - - local function add_to_key_path(key_path, key) - local new_key_path = {} - - for index, value in ipairs(key_path) do - new_key_path[index] = value - end + end, - table.insert(new_key_path, key) - return new_key_path - end + any = function(value) + return true + end, + } - local function get_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 + --- + ---Apply the key-value-pair definitions (defs) on an input table in a + ---recursive fashion. + --- + ---@param defs table # A table containing all definitions. + ---@param opts table # The parse options table. + ---@param input table # The current input table. + ---@param output table # The current output table. + ---@param unknown table # Always the root unknown table. + ---@param key_path table # An array of key names leading to the current + ---@param input_root table # The root input table input and output table. + local function apply_definitions(defs, + opts, + input, + output, + unknown, + key_path, + input_root) + local exclusive_groups = {} + + local function add_to_key_path(key_path, key) + local new_key_path = {} + + for index, value in ipairs(key_path) do + new_key_path[index] = value + end - 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 get_default_value(def) + table.insert(new_key_path, key) + return new_key_path end - end - local apply = { - alias = function(value, key, def) - if type(def.alias) == 'string' then - def.alias = { def.alias } - end - local alias_value - local used_alias_key - -- To get an error if the key and an alias is present - if value ~= nil then - alias_value = value - used_alias_key = key - end - for _, alias in ipairs(def.alias) do - local v = find_value(alias, def) - if v ~= nil then - if alias_value ~= nil then - throw_error(string.format( - 'Duplicate aliases “%s” and “%s” for key “%s”!', - used_alias_key, alias, key)) - end - used_alias_key = alias - alias_value = v - end - end - if alias_value ~= nil then - return alias_value + local function get_default_value(def) + if def.default ~= nil then + return def.default + elseif opts ~= nil and opts.default ~= nil then + return opts.default end - end, + return true + end - always_present = function(value, key, def) - if value == nil and def.always_present then + 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_table(input, search_key) ~= nil then return get_default_value(def) end - 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 + local apply = { + alias = function(value, key, def) + if type(def.alias) == 'string' then + def.alias = { def.alias } + end + local alias_value + local used_alias_key + ---To get an error if the key and an alias is present + if value ~= nil then + alias_value = value + used_alias_key = key + end + for _, alias in ipairs(def.alias) do + local v = find_value(alias, def) + if v ~= nil then + if alias_value ~= nil then + throw_error('E003', { + alias1 = used_alias_key, + alias2 = alias, + key = key, + }) + end + used_alias_key = alias + alias_value = v end end - if not is_in_choices then - throw_error('The value “' .. value .. - '” does not exist in the choices: ' .. - table.concat(def.choices, ', ') .. '!') + if alias_value ~= nil then + return alias_value end - 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 + always_present = function(value, key, def) + if value == nil and def.always_present then + return get_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 - -- dimension - elseif def.data_type == 'dimension' then - if is.dimension(value) then - converted = value + if not is_in_choices then + throw_error('E004', { value = value, choices = def.choices }) end - -- integer - elseif def.data_type == 'integer' then - if is.number(value) then - local n = tonumber(value) - if type(n) == 'number' and n ~= nil then - converted = math.floor(n) + 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 + local n = tonumber(value) + if type(n) == 'number' and n ~= nil then + converted = math.floor(n) + end + 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) + ---list + elseif def.data_type == 'list' then + if is.list(value) then + converted = value end + else + throw_error('E005', { data_type = def.data_type }) end - -- number - elseif def.data_type == 'number' then - if is.number(value) then - converted = tonumber(value) + if converted == nil then + throw_error('E006', { + value = value, + key = key, + data_type = def.data_type, + }) + else + return converted 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, + 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 + exclusive_group = function(value, key, def) + if value == nil then + return 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, + if def.exclusive_group ~= nil then + if exclusive_groups[def.exclusive_group] ~= nil then + throw_error('E007', { + key = key, + exclusive_group = def.exclusive_group, + another_key = exclusive_groups[def.exclusive_group], + }) + else + exclusive_groups[def.exclusive_group] = key + end + 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, + 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, - 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') + macro = function(value, key, def) + if value == nil then + return 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 + if def.macro ~= nil then + token.set_macro(def.macro, value, 'global') end - 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] = "..." }') + match = function(value, key, def) + if value == nil then + return 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 + if def.match ~= nil then + if type(def.match) ~= 'string' then + throw_error('E008') + end + local match = string.match(value, def.match) + if match == nil then + throw_error('E009', { + value = value, + key = key, + match = def.match:gsub('%%', '%%%%'), + }) + else + return match + end end - 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, + opposite_keys = function(value, key, def) + if def.opposite_keys ~= nil then + local function get_value(key1, key2) + local opposite_name + if def.opposite_keys[key1] ~= nil then + opposite_name = def.opposite_keys[key1] + elseif def.opposite_keys[key2] ~= nil then + opposite_name = def.opposite_keys[key2] + end + return opposite_name + end + local true_key = get_value(true, 1) + local false_key = get_value(false, 2) + if true_key == nil or false_key == nil then + throw_error('E010') + end - pick = function(value, key, def) - if def.pick then - local pick_types + ---@param value string + local function remove_values(value) + local count = 0 + while utils.remove_from_table(input, value) do + count = count + 1 + end + return count + end - -- Allow old deprecated attribut pick = true - if def.pick == true then - pick_types = { 'any' } - elseif type(def.pick) == 'table' then - pick_types = def.pick - else - pick_types = { def.pick } - end + local true_count = remove_values(true_key) + local false_count = remove_values(false_key) + + if true_count > 1 then + throw_error('E021', { key = true_key }) + end + + if false_count > 1 then + throw_error('E021', { key = false_key }) + end - -- Check if the pick attribute is valid - for _, pick_type in ipairs(pick_types) do - if type(pick_type) == 'string' and is[pick_type] == nil then - throw_error( - 'Wrong data type in the “pick” attribute: ' .. - tostring(pick_type) .. - '. Allowed are: any, boolean, dimension, integer, number, string.') + if true_count > 0 and false_count > 0 then + throw_error('E020', + { ['true'] = true_key, ['false'] = false_key }) end + + return true_count == 1 or false_count == 0 end + end, - -- The key has already a value. We leave the function at this - -- point to be able to check the pick attribute for errors - -- beforehand. - if value ~= nil then - return value + 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, - for _, pick_type in ipairs(pick_types) do - for i, v in pairs(input) do - -- We can not use ipairs here. `ipairs(t)` iterates up to the - -- first absent index. Values are deleted from the `input` - -- table. - if type(i) == 'number' then - local picked_value = nil - if is[pick_type](v) then - picked_value = v - end + pick = function(value, key, def) + if def.pick then + local pick_types - if picked_value ~= nil then - input[i] = nil - return picked_value - end + ---Allow old deprecated attribut pick = true + if def.pick == true then + pick_types = { 'any' } + elseif type(def.pick) == 'table' then + pick_types = def.pick + else + pick_types = { def.pick } + end + + ---Check if the pick attribute is valid + for _, pick_type in ipairs(pick_types) do + if type(pick_type) == 'string' and is[pick_type] == nil then + throw_error('E011', { + unknown = tostring(pick_type), + data_types = { + 'any', + 'boolean', + 'dimension', + 'integer', + 'number', + 'string', + }, + }) end end - end - 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, + ---The key has already a value. We leave the function at this + ---point to be able to check the pick attribute for errors + ---beforehand. + if value ~= nil then + return value + 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 + for _, pick_type in ipairs(pick_types) do + for i, v in pairs(input) do + ---We can not use ipairs here. `ipairs(t)` iterates up to the + ---first absent index. Values are deleted from the `input` + ---table. + if type(i) == 'number' then + local picked_value = nil + if is[pick_type](v) then + picked_value = v + end + + if picked_value ~= nil then + input[i] = nil + return picked_value + end + end + end + end 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, + + required = function(value, key, def) + if def.required ~= nil and def.required and value == nil then + throw_error('E012', { key = key }) end - end - end, - } + end, - --- standalone values are removed. - -- For some callbacks and the third return value of parse, we - -- need an unchanged raw result from the parse function. - input = clone_table(input) - if output == nil then - output = {} - end - if unknown == nil then - unknown = {} - end - if key_path == nil then - key_path = {} - end + 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, + } - for index, def in pairs(defs) do - -- Find key and def - local key - -- `{ key1 = { }, key2 = { } }` - if type(def) == 'table' and def.name == nil and type(index) == - 'string' then - key = index - -- `{ { name = 'key1' }, { name = 'key2' } }` - elseif type(def) == 'table' and def.name ~= nil then - key = def.name - -- Definitions as strings in an array: `{ 'key1', 'key2' }` - elseif type(index) == 'number' and type(def) == 'string' then - key = def - def = { default = get_default_value({}) } + ---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 = utils.clone_table(input) + if output == nil then + output = {} end - - if type(def) ~= 'table' then - throw_error('Key definition must be a table!') + if unknown == nil then + unknown = {} + end + if key_path == nil then + key_path = {} end - for attr, _ in pairs(def) do - if namespace.attrs[attr] == nil then - throw_error('Unknown definition attribute: ' .. tostring(attr)) + for index, def in pairs(defs) do + ---Find key and def + local key + ---`{ key1 = { }, key2 = { } }` + if type(def) == 'table' and def.name == nil and type(index) == + 'string' then + key = index + ---`{ { name = 'key1' }, { name = 'key2' } }` + elseif type(def) == 'table' and def.name ~= nil then + key = def.name + ---Definitions as strings in an array: `{ 'key1', 'key2' }` + elseif type(index) == 'number' and type(def) == 'string' then + key = def + def = { default = get_default_value({}) } end - end - if key == nil then - throw_error('Key name couldn’t be detected!') - end + if type(def) ~= 'table' then + throw_error('E013', { data_type = tostring(def), key = index }) ---key is nil + end - local value = find_value(key, def) - - for _, def_opt in ipairs({ - 'alias', - 'opposite_keys', - 'pick', - '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 + for attr, _ in pairs(def) do + if namespace.attrs[attr] == nil then + throw_error('E014', { + unknown = attr, + attr_names = utils.get_table_keys(namespace.attrs), + }) end end - end - output[key] = value - end + if key == nil then + throw_error('E015') + end - 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] = {} + local value = find_value(key, def) + + for _, def_opt in ipairs({ + 'alias', + 'opposite_keys', + 'pick', + 'always_present', + 'required', + 'data_type', + 'choices', + 'match', + 'exclusive_group', + 'macro', + 'l3_tl_set', + 'process', + 'sub_keys', + }) do + if def[def_opt] ~= nil then + local tmp_value = apply[def_opt](value, key, def) + if tmp_value ~= nil then + value = tmp_value + end + end end - current_unknown = current_unknown[key] + + output[key] = value end - -- Copy all unknown key-value-pairs to the current unknown table. - for key, value in pairs(input) do - current_unknown[key] = 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 + current_unknown = current_unknown[key] + end + + ---Copy all unknown key-value-pairs to the current unknown table. + for key, value in pairs(input) do + current_unknown[key] = value + end end + + return output, unknown end - return output, unknown -end + --- + ---Parse a LaTeX/TeX style key-value string into a Lua table. + --- + ---@param kv_string string # A string in the TeX/LaTeX style key-value format as described above. + ---@param opts? table # A table containing options. + --- + ---@return table result # The final result of all individual parsing and normalization steps. + ---@return table unknown # A table with unknown, undefinied key-value pairs. + ---@return table raw # The unprocessed, raw result of the LPeg parser. + local function parse(kv_string, opts) + if kv_string == nil then + return {}, {}, {} + end ---- Parse a LaTeX/TeX style key-value string into a Lua table. ---- ----@param kv_string string # A string in the TeX/LaTeX style key-value format as described above. ----@param opts? table # A table containing the settings: ---- `convert_dimensions`, `unpack`, `naked_as_value`, `converter`, ---- `debug`, `preprocess`, `postprocess`. --- ----@return table result # The final result of all individual parsing and normalization steps. ----@return table unknown # A table with unknown, undefinied key-value pairs. ----@return table raw # The unprocessed, raw result of the LPeg parser. -local function parse(kv_string, opts) - if kv_string == nil then - return {}, {}, {} - end + opts = normalize_opts(opts) - opts = normalize_opts(opts) + if type(opts.hooks.kv_string) == 'function' then + kv_string = opts.hooks.kv_string(kv_string) + end - if type(opts.hooks.kv_string) == 'function' then - kv_string = opts.hooks.kv_string(kv_string) - end + local result = generate_parser('list', opts):match(kv_string) + local raw = utils.clone_table(result) - local result = generate_parser('list', opts):match(kv_string) - local raw = clone_table(result) + local function apply_hook(name) + if type(opts.hooks[name]) == 'function' then + if name:match('^keys') then + result = utils.visit_tree(result, opts.hooks[name]) + else + opts.hooks[name](result) + end - 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 - opts.hooks[name](result) + if opts.debug then + print('After the execution of the hook: ' .. name) + visualizers.debug(result) + end end + end - if opts.debug then - print('After the execution of the hook: ' .. name) - debug(result) + local function apply_hooks(at) + if at ~= nil then + at = '_' .. at + else + at = '' end + apply_hook('keys' .. at) + apply_hook('result' .. at) end - end - local function apply_hooks(at) - if at ~= nil then - at = '_' .. at - else - at = '' - end - apply_hook('keys' .. at) - apply_hook('result' .. at) - end + apply_hooks('before_opts') + + --- + ---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, - apply_hooks('before_opts') + 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('E017', { + unknown = style, + styles = { 'lower', 'snake', 'upper' }, + }) + end + end + end + return key, value + end, - --- Normalize the result table of the LPeg parser. This normalization - -- tasks are performed on the raw input table coming directly from - -- the PEG parser: - -- - ---@param result table # The raw input table coming directly from the PEG parser - ---@param opts table # Some options. - local function apply_opts(result, opts) - local callbacks = { - unpack = function(key, value) - if type(value) == 'table' and utils.get_array_size(value) == 1 and - utils.get_table_size(value) == 1 and type(value[1]) ~= 'table' then - return key, value[1] - end - return key, value - end, + apply_invert_flag = function(key, value) + if type(key) == 'string' and key:find(opts.invert_flag) then + return key:gsub(opts.invert_flag, ''), not value + 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, + if opts.unpack then + result = utils.visit_tree(result, callbacks.unpack) + end - format_key = function(key, value) - if type(key) == 'string' then - for _, style in ipairs(opts.format_keys) do - if style == 'lower' then - key = key:lower() - elseif style == 'snake' then - key = key:gsub('[^%w]+', '_') - elseif style == 'upper' then - key = key:upper() - else - throw_error('Unknown style to format keys: ' .. - tostring(style) .. - ' Allowed styles are: lower, snake, upper') - end - end - end - return key, value - end, + if not opts.naked_as_value and opts.defs == false then + result = utils.visit_tree(result, callbacks.process_naked) + end - apply_invert_flag = function(key, value) - if type(key) == 'string' and key:find(opts.invert_flag) then - return key:gsub(opts.invert_flag, ''), not value + if opts.format_keys then + if type(opts.format_keys) ~= 'table' then + throw_error('E018', { data_type = type(opts.format_keys) }) end - return key, value - end, - } + result = utils.visit_tree(result, callbacks.format_key) + end + + if opts.invert_flag then + result = utils.visit_tree(result, callbacks.apply_invert_flag) + end - if opts.unpack then - result = visit_tree(result, callbacks.unpack) + return result end + result = apply_opts(result, opts) + + ---All unknown keys are stored in this table + local unknown = nil + if type(opts.defs) == 'table' then + apply_hooks('before_defs') + result, unknown = apply_definitions(opts.defs, opts, result, {}, + {}, {}, utils.clone_table(result)) + end + + apply_hooks() - if not opts.naked_as_value and opts.defs == false then - result = visit_tree(result, callbacks.process_naked) + if opts.defaults ~= nil and type(opts.defaults) == 'table' then + utils.merge_tables(result, opts.defaults, false) 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) + if opts.debug then + visualizers.debug(result) end - if opts.invert_flag then - result = visit_tree(result, callbacks.apply_invert_flag) + if opts.accumulated_result ~= nil and type(opts.accumulated_result) == + 'table' then + utils.merge_tables(opts.accumulated_result, result, true) end - return result - end - result = apply_opts(result, opts) - - -- All unknown keys are stored in this table - local unknown = nil - if type(opts.defs) == 'table' then - apply_hooks('before_defs') - result, unknown = apply_definitions(opts.defs, opts, result, {}, {}, - {}, clone_table(result)) + ---no_error + if not opts.no_error and type(unknown) == 'table' and + utils.get_table_size(unknown) > 0 then + throw_error('E019', { unknown = visualizers.render(unknown) }) + end + return result, unknown, raw end - apply_hooks() - - if opts.defaults ~= nil and type(opts.defaults) == 'table' then - merge_tables(result, opts.defaults) - end + --- + ---A table to store parsed key-value results. + local result_store = {} - if opts.debug then - debug(result) - end + return { + new = main, - -- 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 + version = { 0, 12, 0 }, ---- Store results --- @section + ---@see parse + parse = parse, ---- A table to store parsed key-value results. -local result_store = {} + define = function(defs, opts) + return function(kv_string, inner_opts) + local options ---- Exports --- @section + if inner_opts ~= nil and opts ~= nil then + options = utils.merge_tables(opts, inner_opts) + elseif inner_opts ~= nil then + options = inner_opts + elseif opts ~= nil then + options = opts + end -local export = { - version = { 0, 11, 0 }, + if options == nil then + options = {} + end - ---Get a new instance of the luakeys module. - --- - ---@return table # A new instance of the luakeys module. - get_private_instance = function() - package.loaded.luakeys = nil - local luakeys = require('luakeys') - package.loaded.luakeys = nil - return luakeys - end, + options.defs = defs - namespace = namespace, + return parse(kv_string, options) + end + end, - ---This function is used in the documentation. - --- - ---@param from string # A key in the namespace table, either `opts`, `hook` or `attrs`. - print_names = function(from) - local names = {} - for name in pairs(namespace[from]) do - table.insert(names, name) - end - table.sort(names) - tex.print(table.concat(names, ', ')) - end, + ---@see default_opts + opts = default_opts, - print_default = function(from, name) - tex.print(tostring(namespace[from][name])) - end, + error_messages = error_messages, - --- @see default_options - opts = default_options, + ---@see visualizers.render + render = visualizers.render, - --- @see stringify - stringify = stringify, + ---@see visualizers.stringify + stringify = visualizers.stringify, - --- @see parse - parse = parse, + ---@see visualizers.debug + debug = visualizers.debug, - define = function(defs, opts) - return function(kv_string, inner_opts) - local options + --- + ---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. + --- + ---@param identifier string # The identifier under which the result is saved. + --- + ---@param result table # A result to be stored and that was created by the key-value parser. + save = function(identifier, result) + result_store[identifier] = result + end, - if inner_opts ~= nil and opts ~= nil then - options = merge_tables(opts, inner_opts) - elseif inner_opts ~= nil then - options = inner_opts - elseif opts ~= nil then - options = opts - end + ---The function `get(identifier): table` retrieves a saved result + ---from the result store. + --- + ---@param identifier string # The identifier under which the result was saved. + --- + ---@return table + 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, - if options == nil then - options = {} - end + is = is, - options.defs = defs + utils = utils, - return parse(kv_string, options) - end - end, + --- + ---Exported but intentionally undocumented functions + --- - --- @see render - render = render, + namespace = utils.clone_table(namespace), - --- @see debug - debug = debug, + --- + ---This function is used in the documentation. + --- + ---@param from string # A key in the namespace table, either `opts`, `hook` or `attrs`. + print_names = function(from) + local names = utils.get_table_keys(namespace[from]) + tex.print(table.concat(names, ', ')) + end, - --- The function `save(identifier, result): void` saves a result (a - -- table from a previous run of `parse`) under an identifier. - -- Therefore, it is not necessary to pollute the global namespace to - -- store results for the later usage. - -- - ---@param identifier string # The identifier under which the result is saved. - -- - ---@param result table # A result to be stored and that was created by the key-value parser. - save = function(identifier, result) - result_store[identifier] = result - end, + print_default = function(from, name) + tex.print(tostring(namespace[from][name])) + end, - --- The function `get(identifier): table` retrieves a saved result - -- from the result store. - -- - ---@param identifier string # The identifier under which the result was saved. - --- - ---@return table - 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, + --- + ---@param exported_table table + depublish_functions = function(exported_table) + local function warn_global_import() + throw_error('E023') + end - is = is, + exported_table.parse = warn_global_import + exported_table.define = warn_global_import + exported_table.save = warn_global_import + exported_table.get = warn_global_import + end, + } - utils = utils, -} +end -return export +return main diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty b/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty index 8cb59cf5ef5..4b5432faa3b 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys.sty @@ -1,5 +1,5 @@ %% luakeys.sty -%% Copyright 2021-2022 Josef Friedrich +%% Copyright 2021-2023 Josef Friedrich % % This work may be distributed and/or modified under the % conditions of the LaTeX Project Public License, either version 1.3c @@ -17,5 +17,14 @@ % luakeys-debug.sty and luakeys-debug.tex. \NeedsTeXFormat{LaTeX2e} -\ProvidesPackage{luakeys}[2022/12/23 0.11.0 Parsing key-value options using Lua.] -\directlua{luakeys = require('luakeys')} +\ProvidesPackage{luakeys}[2023/01/05 v0.12.0 Parsing key-value options using Lua.] +\directlua{ + if luakeys == nil then + luakeys = require('luakeys')() + luakeys.depublish_functions(luakeys) + end +} + +\def\LuakeysGetPackageOptions{\luaescapestring{\@ptionlist{\@currname.\@currext}}} + +\def\LuakeysGetClassOptions{\luaescapestring{\@raw@classoptionslist}} diff --git a/Master/texmf-dist/tex/luatex/luakeys/luakeys.tex b/Master/texmf-dist/tex/luatex/luakeys/luakeys.tex index c3c69a0ba35..f993e7c83fb 100644 --- a/Master/texmf-dist/tex/luatex/luakeys/luakeys.tex +++ b/Master/texmf-dist/tex/luatex/luakeys/luakeys.tex @@ -1,5 +1,5 @@ %% luakeys.tex -%% Copyright 2021-2022 Josef Friedrich +%% Copyright 2021-2023 Josef Friedrich % % This work may be distributed and/or modified under the % conditions of the LaTeX Project Public License, either version 1.3c @@ -16,4 +16,9 @@ % This work consists of the files luakeys.lua, luakeys.sty, luakeys.tex % luakeys-debug.sty and luakeys-debug.tex. -\directlua{luakeys = require('luakeys')} +\directlua{ + if luakeys == nil then + luakeys = require('luakeys')() + luakeys.depublish_functions(luakeys) + end +} |