% \iffalse meta-comment
%
% Copyright (C) 2021-2025
% The LaTeX Project and any individual authors listed elsewhere
% in this file.
%
% This file is part of the LaTeX base system.
% -------------------------------------------
%
% It may be distributed and/or modified under the
% conditions of the LaTeX Project Public License, either version 1.3c
% of this license or (at your option) any later version.
% The latest version of this license is in
%    https://www.latex-project.org/lppl.txt
% and version 1.3c or later is part of all distributions of LaTeX
% version 2008 or later.
%
% This file has the LPPL maintenance status "maintained".
%
% The list of all files belonging to the LaTeX base distribution is
% given in the file `manifest.txt'. See also `legal.txt' for additional
% information.
%
% The list of derived (unpacked) files belonging to the distribution
% and covered by LPPL is defined by the unpacking scripts (with
% extension .ins) which are part of the distribution.
%
% \fi
%
% \iffalse
%%% From File: ltkeys.dtx
%
%<*driver>
% \fi
\ProvidesFile{ltkeys.dtx}
             [2024/06/20 v1.0o LaTeX Kernel (Keyval options)]
% \iffalse
\documentclass{l3doc}
\GetFileInfo{ltkeys.dtx}
\title{\filename}
\date{\filedate}
\author{%
  \LaTeX{} Team}

\begin{document}
 \maketitle
 \DocInput{ltkeys.dtx}
\end{document}
%</driver>
% \fi
%
% \section{Creating and using keyval options}
%
% As with any key--value input, using key--value pairs as package or
% class options has two parts: creating the key options and setting (using)
% them. Options created in this way \emph{may} be used after package loading
% as general key--value settings: this will depend on the nature of the
% underlying code.
%
% \begin{function}{\DeclareKeys}
%   \begin{syntax}
%     \cs{DeclareKeys} \oarg{family} \marg{declarations}
%   \end{syntax}
%   Creates a series of options from a comma-separated \meta{declarations} list.
%   Each entry in this list is a key--value pair, with the \meta{key} having one
%   or more \meta{properties}. A small number of \enquote{basic}
%   \meta{properties} are described below. The full range of properties,
%   provided by \pkg{l3keys}, can also be used for more powerful processing.
%   See \texttt{interface3} for the full details.
%   
%   The basic properties provided here are
%   \begin{itemize}
%    \item \texttt{.code} --- execute arbitrary code
%     \item \texttt{.if} --- sets a \TeX{} \cs{if...} switch
%     \item \texttt{.ifnot} --- sets an inverted \TeX{} \cs{if...} switch
%     \item \texttt{.pass-to-packages} --- for class options, this specifies
%       whether the option should be treated \enquote{global} (read by packages
%       from the global list); for package options this property has no effect
%     \item \texttt{.store} --- stores a value in a macro
%     \item \texttt{.usage} -- defines whether the option can be given only
%       when loading (\texttt{load}), in the preamble (\texttt{preamble}) or
%       has no limitation on scope (\texttt{general})
%   \end{itemize}
%   The part of the \meta{key} before the \meta{property} is the \meta{name},
%   with the \meta{value} working with the \meta{property} to define the
%   behaviour of the option.
%
%   For example, with
%   \begin{verbatim}
%     \DeclareKeys[mypkg]
%       {
%         draft.if          = @mypkg@draft      ,
%         draft.usage       = preamble          ,
%         name.store        = \@mypkg@name      ,
%         name.usage        = load              ,
%         second-name.store = \@mypkg@other@name
%       }
%   \end{verbatim}
%   three options would be create. The option \texttt{draft} can be given
%   anywhere in the preamble, and will set a switch called \cs{if@mypkg@draft}.
%   The option \texttt{name} can only be given during package loading, and will
%   save whatever value it is given in \cs{@mypkg@name}. Finally, the option
%   \texttt{second-name} can be given anywhere, and will save its value in
%   \cs{@mypkg@other@name}.
%
%   Keys created \emph{before} the use of \cs{ProcessKeyOptions} act as
%   package options.
% \end{function}
%
% \begin{function}{\DeclareUnknownKeyHandler}
%   \begin{syntax}
%     \cs{DeclareUnknownKeyHandler} \oarg{family} \marg{code}
%   \end{syntax}
%   The function \cs{DeclareUnknownKeyHandler} may be used to define
%   the behavior when an undefined key is encountered. The \meta{code}
%   will receive the unknown key name as |#1| and the value as |#2|.
%   These can then be processed as appropriate, e.g.~by forwarding
%   to another package.
% \end{function}
%
% \begin{function}{\ProcessKeyOptions}
%   \begin{syntax}
%     \cs{ProcessKeyOptions} \oarg{family}
%   \end{syntax}
%   The \cs{ProcessKeyOptions} function is used to check the current
%   option list against the keys defined for \meta{family}. Global (class)
%   options and local (package) options are checked when this function
%   is called in a package.
% \end{function}
%
% \begin{function}{\SetKeys}
%   \begin{syntax}
%     \cs{SetKeys} \oarg{family} \Arg{keyvals}
%   \end{syntax}
%   Sets (applies) the explicit list of \meta{keyvals}  for the \meta{family}:
%   it the latter is not given, the value of \cs{@currname} used. This command
%   may be used within a package to set options before or after using
%   \cs{ProcessKeyOptions}.
% \end{function}
%
% \StopEventually{}
%
% \subsection{Implementation of \pkg{ltkeys}}
%
%    \begin{macrocode}
%<@@=keys>
%    \end{macrocode}
%
%    \begin{macrocode}
%<*2ekernel>
%    \end{macrocode}
%
%    \begin{macrocode}
\ExplSyntaxOn
%    \end{macrocode}
%
% \subsection{Key properties}
%
% \begin{macro}{.code, .if, .ifnot, .store, .usage}
% \changes{v1.0b}{2022/02/05}
%         {Create properties in \texttt{ltlkeys}}
% \changes{v1.0c}{2022/02/07}
%         {Correct \texttt.{.code} property}
% \changes{v1.0i}{2022/06/22}
%         {Add \texttt.{.notif} property}
%    \begin{macrocode}
\group_begin:
  \cs_set_protected:Npn \@@_tmp:nn #1#2
    {
      \quark_if_recursion_tail_stop:n {#1}
      \cs_new_eq:cc
        { \c_@@_props_root_str . #2 }
        { \c_@@_props_root_str . #1 }
      \@@_tmp:nn
    }
  \@@_tmp:nn
    { code:n }          { code }
    { legacy_if_set:n } { if }
    { legacy_if_set_inverse:n } { ifnot }
    { tl_set:N }        { store }
    { usage:n }         { usage }
    { \q_recursion_tail } { }
    \q_recursion_stop
\group_end:
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{.pass-to-packages}
% \changes{v1.0n}{2024/06/19}{New key property}
% \begin{macro}{\@@_scope:n}
% \changes{v1.0n}{2024/06/19}{New function}
% \begin{macro}{\@@_scope:N}
% \changes{v1.0n}{2024/06/19}{New function}
% \changes{v1.0o}{2024/06/20}{Ensure only key name is stored}
%   Used to force options to be global: as this property (uniquely) has
%   an \emph{optional} value, there is a bit of work to do.
%    \begin{macrocode}
\cs_new_protected:cpn { \c_@@_props_root_str .pass-to-packages }
  {
    \bool_if:NTF \l_@@_no_value_bool
      { \@@_scope:n { true } }
      { \@@_scope:n }
  }
\cs_new_protected:Npn \@@_scope:n #1
  {
    \str_case:nnF {#1}
      {
        { true }
          { \@@_scope:N \clist_put_right:NV }
        { false }
          { \@@_scope:N \clist_remove_all:NV }
      }
      {
        \msg_error:nnnn { keys }
          { choice-unknown }
          { .pass-to-packages }
          {#1}
      }
  }
\cs_new_protected:Npn \@@_scope:N #1
  {
    \exp_after:wN \@@_find_key_module:wNN
      \l_keys_path_str \s_@@_stop
        \l_keys_key_tl \l_keys_key_str
    #1 \l_@@_forced_global_clist \l_keys_key_str
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \subsection{Main mechanism}
% 
%    \begin{macrocode}
\cs_generate_variant:Nn \clist_if_in:NnT { Ne }
\cs_generate_variant:Nn \clist_if_in:NnTF { Ne }
\cs_generate_variant:Nn \clist_put_right:Nn { Nv }
%    \end{macrocode}
%
% \begin{macro}{\l_@@_class_only_clist}
% \changes{v1.0n}{2024/06/19}{New variable}
%   Used to track class-only options.
%    \begin{macrocode}
\clist_new:N \l_@@_class_only_clist
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\l_@@_forced_global_clist}
% \changes{v1.0n}{2024/06/19}{New variable}
%   Used to force options to be global.
%    \begin{macrocode}
\clist_new:N \l_@@_forced_global_clist
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\l_@@_options_clist}
%   A single list is used for all options, into which they are collected
%   before processing.
%    \begin{macrocode}
\clist_new:N \l_@@_options_clist
%    \end{macrocode}
% \end{macro}
%
% \begin{variable}{\l_@@_options_loading_bool}
%   Used to indicate we are in the loading phase: controls the outcome
%   of warnings.
%    \begin{macrocode}
\bool_new:N \l_@@_options_loading_bool
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}{\@@_options:n}
% \changes{v1.0f}{2022/03/18}{Simplify to always cover global options}
% \begin{macro}{\@@_options_aux:n}
% \changes{v1.0b}{2022/01/15}
%         {Clear option list in end-of-package hook}
% \changes{v1.0l}{2022/10/20}
%         {Define key option handler in \pkg{ltkeys}}
% \changes{v1.0i}{2022/07/05}{Support \cs{CurrentOption}}
% \begin{macro}{\@@_options_end:}
%   The main function calls functions to collect up the global and local
%   options into \cs{l_@@_options_clist} before calling the
%   underlying functions to actually do the processing. So that a suitable
%   message is produced if the option is unknown, the special
%   \texttt{unknown} key is set if it does not already exist for the
%   current family, and is cleaned up afterwards if required. To allow
%   the \LaTeXe{} layer to know this mechanism is active, and to deal
%   with the key family not matching the file name, we store the family
%   in all cases.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_options:n #1
  { \@@_options_expand_module:Nn \@@_options_aux:n {#1} }
\cs_new_protected:Npn \@@_options_aux:n #1
  {
    \cs_gset_protected:cpn { opt@handler@\@currname.\@currext }
      { \ProcessKeyOptions [ #1 ] }
    \cs_set_protected:Npn \@@_option_end: { }
    \clist_clear:N \l_@@_options_clist
    \@@_options_global:n {#1}
    \@@_options_local:
    \keys_if_exist:nnF {#1} { unknown }
      {
        \keys_define:nn {#1}
          {
            unknown .code:n =
              {
                \msg_error:nnxx { keys } { option-unknown }
                  { \l_keys_key_str } { \@currname }
              }
          }
        \cs_set_protected:Npn \@@_option_end:
          { \keys_define:nn {#1} { unknown .undefine: } }
      }
    \bool_set_true:N \l_@@_options_loading_bool
    \clist_map_variable:NNn \l_@@_options_clist \CurrentOption
      { \keys_set:nV {#1} \CurrentOption }
    \bool_set_false:N \l_@@_options_loading_bool
    \AtEndOfPackage { \cs_set_eq:NN \@unprocessedoptions \scan_stop: }
    \@@_option_end:
    \@@_options_loaded:n {#1}
  }
%    \end{macrocode}
% \changes{v1.0j}{2022/07/23}
%         {Output `public' package name in messages}
%    \begin{macrocode}
\msg_new:nnnn { keys } { option-unknown }
  { Unknown~option~'#1'~for~package~#2. }
  {
    LaTeX~has~been~asked~to~set~an~option~called~'#1'~
    but~the~package~"\msg_module_name:n {#2}"~has~not~created~an~option~with~this~name.
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_options_global:n}
% \changes{v1.0f}{2022/03/18}{Simplify to always cover global options}
% \changes{v1.0h}{2022/06/20}{Use raw options data}
%   Global (class) options are handled differently for \LaTeXe{} packages
%   and classes. Hence this function is essentially a check on the current
%  file type. The initial test is needed as \LaTeXe{} allows variables to
%   be equal to \cs{scan_stop:}, which is usually forbidden in \pkg{expl3}
%   code.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_options_global:n #1
  {
    \cs_if_eq:NNF \@raw@classoptionslist \scan_stop:
      {
        \cs_if_eq:NNTF \@currext \@clsextension
          { \@@_options_class:n {#1} }
          { \@@_options_package:n {#1} }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_options_class:n}
% \changes{v1.0g}{2022/06/16}{Better handling of option removal}
% \changes{v1.0h}{2022/06/19}{Further work on handling of option removal}
% \changes{v1.0h}{2022/06/20}{Use raw options data}
% \changes{v1.0m}{2024/01/13}{Trim spaces off key names}
% \changes{v1.0n}{2024/06/19}{Refactor function}
% \begin{macro}{\@@_options_class:nnn}
% \changes{v1.0h}{2022/06/20}{New function}
% \changes{v1.0i}{2022/07/05}{Correct naming of raw class options storage}
% \changes{v1.0l}{2022/10/22}{Correct handling of unused option list}
% \changes{v1.0n}{2024/06/19}{Refactor function}
% \changes{v1.0n}{2024/06/19}{Track options used by classes}
% \begin{macro}{\@@_options_class:nn}
% \changes{v1.0n}{2024/06/19}{New function}
%   For classes, each option (stripped of any content after |=|)
%   is checked for existence as a key. If found, the option is added to
%   the combined list for processing. On the other hand, unused options
%   are stored up in \cs{@unusedoptionlist}. An earlier version of
%   this code checked for the \texttt{unknown} key just once and
%   if found short-cutted the loop: that though makes handling more
%   complex situations harder, so we take the performance hit instead.
%   Options used by classes are tracked but the catch-all \texttt{unknown}
%   is excluded (hence not using a lazy evaluation for the key testing).
%    \begin{macrocode}
\cs_new_protected:Npn \@@_options_class:n #1
  {
    \cs_if_free:cF { @raw@opt@ \@currname . \@currext }
      {
        \clist_map_inline:cn { @raw@opt@ \@currname . \@currext }
          {
            \exp_args:Ne \@@_options_class:nnn
              { \tl_trim_spaces:e { \@@_remove_equals:n {##1} } }
                {##1} {#1}
          }
      }
  }
\cs_new_protected:Npn \@@_options_class:nnn #1#2#3
  {
    \keys_if_exist:nnTF {#3} {#1}
      {
        \@@_options_class:nn {#1} {#2}
        \clist_put_right:Ne \l_@@_class_only_clist { \tl_to_str:n {#1} }
      }
      {
        \keys_if_exist:nnTF {#3} { unknown }
          { \@@_options_class:nn {#1} {#2} }
          {
            \clist_if_in:NnF \@unusedoptionlist {#1}
              { \clist_put_right:Nn \@unusedoptionlist {#1} }
          }
      }
  }
\cs_new_protected:Npn \@@_options_class:nn #1#2
  {
    \clist_remove_all:Nn \@unusedoptionlist {#1}
    \clist_put_right:Nn \l_@@_options_clist {#2}
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_options_package:n}
% \changes{v1.0g}{2022/06/16}{Better handling of option removal}
% \changes{v1.0h}{2022/06/19}{Further work on handling of option removal}
% \changes{v1.0h}{2022/06/20}{Use raw options data}
% \changes{v1.0m}{2024/01/13}{Trim spaces off key names}
% \begin{macro}{\@@_options_package:nnn}
% \changes{v1.0h}{2022/06/19}{New function}
% \changes{v1.0n}{2024/06/19}{Skip options given to packages}
% \begin{macro}{\@@_options_package:nn}
% \changes{v1.0n}{2024/06/19}{New functions}
%   For global options when processing a package, the tasks are slightly
%   different from those for a class. The check is the same, but here
%   there is nothing to do if the option is not applicable. Each valid
%   option also needs to be removed from \cs{@unusedoptionlist}.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_options_package:n #1
  {
    \clist_map_inline:Nn \@raw@classoptionslist
      {
        \exp_args:Ne \@@_options_package:nnn
          { \tl_trim_spaces:e { \@@_remove_equals:n {##1} } }
          {##1} {#1}
      }
  }
%    \end{macrocode}
%   The forced-global test here needs to use \cs{tl_to_str:n} as the data come
%   from a key name, which is always a string.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_options_package:nnn #1#2#3
  {
    \keys_if_exist:nnT {#3} {#1}
      {
        \clist_if_in:NeTF \l_@@_class_only_clist { \tl_to_str:n {#1} }
          {
            \clist_if_in:NeT \l_@@_forced_global_clist { \tl_to_str:n {#1} }
              { \@@_options_package:nn {#1} {#2} }
          }
          { \@@_options_package:nn {#1} {#2} }
      }
   }
\cs_new_protected:Npn \@@_options_package:nn #1#2
  {
    \clist_put_right:Nn \l_@@_options_clist {#2}
    \clist_remove_all:Nn \@unusedoptionlist {#1}
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\@@_options_local:}
% \changes{v1.0h}{2022/06/20}{Use raw options data}
%   If local options are found, they are added to the processing list.
%   \LaTeXe{} stores options for each file in a macro which may or may not
%   exist, hence the need to use \cs{cs_if_exist:c}.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_options_local:
  {
    \cs_if_eq:NNF \@currext \@clsextension
      {
        \cs_if_exist:cT { @raw@opt@ \@currname . \@currext }
          {
            \clist_put_right:Nv \l_@@_options_clist
              { @raw@opt@ \@currname . \@currext }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[EXP]{\@@_remove_equals:n}
% \begin{macro}[EXP]{\@@_remove_equals:w}
%   As the name suggests, this is a simple function to remove an equals
%   sign from the input. This is all wrapped up in an \texttt{n} function
%   so that there will always be a sign available.
%    \begin{macrocode}
\cs_new:Npn \@@_remove_equals:n #1
  { \@@_remove_equals:w #1 = \s_@@_stop }
\cs_new:Npn \@@_remove_equals:w #1 = #2 \s_@@_stop { \exp_not:n {#1} }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{The document interfaces}
%
%    \begin{macrocode}
\cs_generate_variant:Nn \keys_define:nn { nx }
%    \end{macrocode}
%
% \begin{macro}{\@@_options_expand_module:Nn}
% \changes{v1.0e}{2022/02/21}
%         {Faster approach to module expansion}
% \begin{macro}{\@@_options_expand_module:nN}
%   To deal with active characters inside the module argument whilst also
%   expanding that argument, we use a combination of \texttt{c}- and
%   \texttt{f}-type expansion. This works as the definitions for active
%   UTF-8 bytes contain an \cs{ifincsname} test.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_options_expand_module:Nn #1#2
  {
    \cs:w @@_options_expand_module:nN \use:e { \cs_end: {#2} } #1
  }
\cs_new_protected:Npn \@@_options_expand_module:nN #1#2
  { #2 {#1} }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \begin{macro}{\DeclareKeys}
% \changes{v1.0c}{2022/02/15}{Expand module argument}
% \changes{v1.0d}{2022/02/16}{Allow for active characters in module argument}
%   Defining key options is quite straight-forward: we have an intermediate
%   function to allow for potential set-up steps.
%    \begin{macrocode}
\NewDocumentCommand \DeclareKeys { O { \@currname } +m }
  { \@@_options_expand_module:Nn \keys_define:nn {#1} {#2} }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\DeclareUnknownKeyHandler}
% \changes{v1.0c}{2022/02/15}{Added \cs{DeclareUnknownKeysHandler}}
% \changes{v1.0d}{2022/02/16}{Allow for active characters in module argument}
% \changes{v1.0d}{2022/02/16}
%   {Rename \cs{DeclareUnknownKeysHandler} to \cs{DeclareUnknownKeyHandler}}
%    \begin{macrocode}
\NewDocumentCommand \DeclareUnknownKeyHandler { O { \@currname } +m }
  {
    \cs_set_protected:cpn { @@_unknown_handler_ #1 :nn } ##1##2 {#2}
    \@@_options_expand_module:Nn \keys_define:nx {#1}
      {
        unknown .code:n = 
          \exp_not:N \exp_args:NV
            \exp_not:c { @@_unknown_handler_ #1 :nn }
            \exp_not:N \l_keys_key_str {####1}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\ProcessKeyOptions}
% \changes{v1.0c}{2022/02/15}{Expand module argument}
% \changes{v1.0d}{2022/02/16}{Allow for active characters in module argument}
% \changes{v1.0f}{2022/03/18}{Remove \cs{ProcessKeyPackageOptions}}
%   We need to deal with the older interface from \pkg{l3keys2e} here: it had
%   a mandatory argument. We can mop that up using a look-ahead, and then
%   exploit that information to determine whether the package option handling
%   is set up for the new approach for clash handling.
%    \begin{macrocode}
\NewDocumentCommand \ProcessKeyOptions { O { \@currname } }
  { \@@_options:n {#1} }
\@onlypreamble \ProcessKeyOptions
%    \end{macrocode}
% \end{macro}
%
% \subsection{Option usage scope}
%
% \begin{macro}{\@@_options_loaded:n}
% \begin{macro}{\@@_options_loaded:nn}
% \changes{v1.0l}{2022/10/20}{Correct an argument for a message}
%   Indicates that the load-time options for a package have been processed:
%   once this has happened, make them unavailable either with a warning or
%   an error.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_options_loaded:n #1
  {
    \prop_get:NnNT \l_keys_usage_load_prop {#1} \l_@@_tmpa_tl
      {
        \clist_map_inline:Nn \l_@@_tmpa_tl
          {
            \keys_define:nn {#1}
              {
                ##1 .code:n =
                  \@@_options_loaded:nn {#1} {##1}
              }
          }
      }
  }
\cs_new_protected:Npn \@@_options_loaded:nn #1#2
  {
    \bool_if:NTF \l_@@_options_loading_bool
      { \msg_warning:nnnn { keys } { load-option-ignored } }
      { \msg_error:nnnn { keys } { load-only } }
        {#1} {#2}
  }
%    \end{macrocode}
% \changes{v1.0j}{2022/07/23}
%         {Output `public' package name in messages}
%    \begin{macrocode}
\msg_new:nnn { keys } { load-option-ignored }
  {
    Package~"\msg_module_name:n {#1}"~has~already~been~loaded:~
    ignoring~load-time~option~"#2".
  }
%    \end{macrocode}
% \changes{v1.0j}{2022/07/23}
%         {Output `public' package name in messages}
% \changes{v1.0k}{2022/08/21}
%         {Correct error message}
%    \begin{macrocode}
\msg_new:nnnn { keys } { load-only }
  {
    Key~"#2"~may~only~be~used~during~loading~of~package~
    "\msg_module_name:n {#1}".
  }
  {
    LaTeX~was~asked~to~set~a~key~called~"#2",~but~this~is~only~allowed~
    in~the~optional~argument~when~loading~package~"\msg_module_name:n{#1}".
  }
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
%  Disable all preamble options in one shot.
%    \begin{macrocode}
\tl_gput_left:Nn \@kernel@after@begindocument
  {
    \prop_map_inline:Nn \l_keys_usage_preamble_prop
      {
        \clist_map_inline:nn {#2}
          {
            \keys_define:nn {#1}
              {
                ##1 .code:n =
                  \msg_error:nnn { keys } { preamble-only } {##1}
              }
          }
      }
  }
\msg_new:nnnn { keys } { preamble-only }
  { Key~"#1"~may~only~be~used~in~the~preamble. }
  {
    LaTeX~was~asked~to~set~a~key~called~"#1",~but~this~is~only~allowed~
    before~\begin{document}.~You~will~need~to~set~the~key~earlier.
  }
%    \end{macrocode}
%
% \subsection{General key setting}
%
% \begin{macro}{\SetKeys}
% \changes{v1.0c}{2022/02/15}{Expand module argument}
% \changes{v1.0d}{2022/02/16}{Allow for active characters in module argument}
%   A simple wrapper.
%    \begin{macrocode}
\NewDocumentCommand \SetKeys { O { \@currname } +m }
  { \@@_options_expand_module:Nn \keys_set:nn {#1} {#2} }
%    \end{macrocode}
% \end{macro}
%
%    \begin{macrocode}
\ExplSyntaxOff
%    \end{macrocode}
%
%    \begin{macrocode}
%</2ekernel>
%    \end{macrocode}
