% \iffalse meta-comment
%
%% File: l3pdfdict.dtx
%
% Copyright (C) 2018-2024 The LaTeX Project
%
% It may be distributed and/or modified under the conditions of the
% LaTeX Project Public License (LPPL), either version 1.3c of this
% license or (at your option) any later version.  The latest version
% of this license is in the file
%
%    http://www.latex-project.org/lppl.txt
%
% This file is part of the "LaTeX PDF management testphase bundle" (The Work in LPPL)
% and all files in that bundle must be distributed together.
%
% -----------------------------------------------------------------------
%
% The development version of the bundle can be found at
%
%    https://github.com/latex3/pdfresources
%
% for those people who are interested.
%
%<*driver>
\DocumentMetadata{pdfstandard=A-2b}
\documentclass[full]{l3doc}
\usepackage{array,booktabs}
\hypersetup{pdfauthor=The LaTeX Project,
  pdftitle=l3pdfdict (LaTeX PDF management testphase bundle)}

\begin{document}
  \DocInput{\jobname.dtx}
\end{document}
%</driver>
% \fi
% \title{^^A
%   The \pkg{l3pdfdict} module---tools for PDF dictionaries ^^A
%   \\
%   \LaTeX{} PDF management testphase bundle
% }
%
% \author{^^A
%  The \LaTeX{} Project\thanks
%    {^^A
%      E-mail:
%        \href{mailto:latex-team@latex-project.org}
%          {latex-team@latex-project.org}^^A
%    }^^A
% }
%
% \date{Version 0.96m, released 2024-09-29}
%
% \maketitle
% \begin{documentation}
%
% \section{\pkg{l3pdfdict} documentation}
% Many PDF objects are or contain dictionaries---structures
% containing a number of \mbox{(pdf-)}name/value pairs.
% Examples are attributes of links,
% filespec dictionaries, xform dictionaries, the catalog, the info dictionary.
% The commands in this module offer an number of
% tools to handle such dictionaries. The module setups a name space for the
% dictionary names and offers some commands to output dictionaries.
%
% The dictionaries work in many respects
% like property lists with a few PDF specific changes:
% \begin{itemize}
% \item The keys are always converted with \cs{str_convert_pdfname:n}
% to get a correct PDF name;
% \item a key with a empty value can not be added, it will be ignored;
% \item there is a dedicated function to output the property as space
% separated list with keys with slash: \texttt{/key1 value1 /key2 value2}.
% \end{itemize}
% Local and global dictionaries can be created.
%
% \subsection{User Commands}
% \begin{function}[updated = 2020-12-03]
%   {\pdfdict_new:n}
%   \begin{syntax}
%     \cs{pdfdict_new:n} \Arg{dictionary name}
%   \end{syntax}
% This function create a new local or global dictionary. Which one depends on
% \meta{dictionary name}: If it begins with the standard |g| the dictionary is global,
% with |l| the dictionary is local, other starting chars will give an error.
% It is recommended to begin the name in the standard expl3 naming scheme
% with one or two underscores and a module name,
% so |g_module_XXXX| or |g__module_XXXX|.
% \end{function}
%
% \begin{function}[added = 2020-06-16,updated = 2020-12-03]
%   {\pdfdict_set_eq:nn,\pdfdict_gset_eq:nn}
%   \begin{syntax}
%     \cs{pdfdict_set_eq:nn}  \Arg{local dictionary name_1} \Arg{dictionary name_2}\\
%     \cs{pdfdict_gset_eq:nn} \Arg{global dictionary name_1}\Arg{dictionary name_2}
%   \end{syntax}
% This functions copy \meta{dictionary name_2} into
% \meta{local/global dictionary name_1} locally or globally. If the
% dictionary \meta{local/global dictionary name_1} doesn't exist yet, it will be created.
% If \meta{dictionary name_2} doesn't exist yet, an error will be raised.
% \end{function}
%
% \begin{function}[added = 2020-04-06]
%   {\pdfdict_put:nnn, \pdfdict_gput:nnn,\pdfdict_gput:nee}
%   \begin{syntax}
%     \cs{pdfdict_put:nnn}  \Arg{local dictionary}  \Arg{name} \Arg{value} \\
%     \cs{pdfdict_gput:nnn} \Arg{global dictionary} \Arg{name} \Arg{value}
%   \end{syntax}
% This function puts key \meta{name} and value \meta{value} locally or globally in the
% \meta{dictionary} created with \cs{pdfdict_new:n}.
% \Arg{name} should be a PDF Name without the starting slash. It will be stored
% with \cs{str_convert_pdfname:n}, so will be automatically correctly escaped in case
% it contains slashes, spaces or other chars not allowed in a PDF name.
% \meta{value} should be a valid PDF value for this name in the
% target dictionary. The value is \emph{neither} converted \emph{nor} escaped automatically.
% If the value is blank nothing is added to the dictionary.
%
% When adding a value keep in mind that the expansion behaviour
% of the backends differ. Some backends expand a
% value always fully when writing to the PDF, with other backends commands
% could end as strings in the PDF. This makes controlling the
% expansion quite tricky. It is better to not rely on
% \meta{value} to be expanded nor not expanded by the backend commands.
% \end{function}
%
% \begin{function}[EXP,added = 2020-12-04]
%   { \pdfdict_item:nn, \pdfdict_item:ne  }
%   \begin{syntax}
%     \cs{pdfdict_item:nn} \Arg{key} \Arg{value}
%   \end{syntax}
% A simple command to output key-value as |/key value|. This is
% needed to output dictionaries in mapping commands. The command doesn't
% do any escaping, it expects that the name has been escaped when the value
% has been stored into the dictionary.
% If the value is blank nothing is output.
% The command is expandable if the content is it.
% \end{function}
%
% \begin{function}[EXP,updated = 2020-12-03]
%   { \pdfdict_use:n  }
%   \begin{syntax}
%     \cs{pdfdict_use:n} \Arg{dictionary}
%   \end{syntax}
%   This outputs the property list of the dictionary as a list of
%   |/key value| pairs.
%   This can be used e.g. when writing a dictionary object with
%   \cs{pdf_object_write:nne}
% \end{function}
%
% \begin{function}[updated = 2020-12-03]
%   {\pdfdict_show:n }
%   \begin{syntax}
%     \cs{pdfdict_show:n}  \Arg{dictionary}
%   \end{syntax}
%   This shows the content of \meta{dictionary} in the log and on the terminal.
% \end{function}
%\begin{function}[EXP, pTF,updated = 2020-12-03]
%   { \pdfdict_if_exist:n  }
%   \begin{syntax}
%     \cs{pdfdict_if_exist:n} \Arg{dictionary}
%   \end{syntax}
%   This tests if the dictionary exists.
% \end{function}
% \begin{function}[EXP, pTF,updated = 2020-12-03]
%   { \pdfdict_if_empty:n }
%   \begin{syntax}
%     \cs{pdfdict_if_empty:n} \Arg{dictionary}
%   \end{syntax}
%   This tests if the dictionary is empty. The result is false if the
%   dictionary doesn't exist.
% \end{function}
% \begin{function}[added = 2020-07-06]
%   {\pdfdict_get:nnN }
%   \begin{syntax}
%     \cs{pdfdict_get:nnN} \Arg{dictionary}  \Arg{name} \meta{tl var}
%   \end{syntax}
%   Recovers the \meta{value} stored by \cs{pdfdict_put:nnn} or
%   \cs{pdfdict_gput:nnn}
%   for \meta{name} and places this in the \meta{token list
%   variable}. If \meta{name} is not found
%   then the \meta{token list variable} is set
%   to the special marker \cs{q_no_value}. \meta{name} is first converted
%   with \cs{str_convert_pdfname:n}.  The \meta{token list
%   variable} is set within the current \TeX{} group.
% \end{function}
%
% \begin{function}[updated = 2020-12-03]
%   {
%     \pdfdict_remove:nn, \pdfdict_gremove:nn
%   }
%   \begin{syntax}
%     \cs{pdfdict_remove:nn}  \Arg{local dictionary}  \Arg{name}\\
%     \cs{pdfdict_gremove:nn} \Arg{global dictionary} \Arg{name}
%   \end{syntax}
%   Removes  \meta{name} and its associated \meta{value} from
%   the \Arg{dictionary}
%   The removal is local from local dictionaries
%   and global from global dictionaries.
%   If \meta{name} is not found no change occurs,
%   \emph{i.e}~there is no need to test for the existence of a name before
%   trying to remove it. \meta{name} is first converted
%   with \cs{str_convert_pdfname:n}.
% \end{function}
% \end{documentation}
%
% \begin{implementation}
% \section{\pkg{l3pdfdict} implementation}
%    \begin{macrocode}
%<@@=pdfdict>
%<*header>
\ProvidesExplPackage{l3pdfdict}{2024-09-29}{0.96m}
  {Tools for PDF dictionaries (LaTeX PDF management testphase bundle)}
%</header>
%    \end{macrocode}
% \subsection{messages}
%    \begin{macrocode}
%<*package>
\cs_new:Npn \@@_get_type:n #1
  {
    \str_case_e:nn { \str_head:n{#1} }
     {
       {g}{global}
       {l}{local}
     }
  }
\msg_new:nnn  { pdfdict } { show-dict }
  { %#1: name of the dictionary
    %#2: expanded content
    %#3: type
    The~#3~dictionary~'#1'~
    \tl_if_empty:nTF {#2}
      { is~empty \\>~ . }
      { contains~the~pairs~(without~outer~braces): #2 . }
  }
\msg_new:nnn  { pdfdict } { unknown-dict }
  {
    The~dictionary~'#1'~is~unknown.
  }
\msg_new:nnn  { pdfdict } { dict-already-defined  }
  {
    The~#2~dictionary~'#1'~is~already~defined.
  }
\msg_new:nnn  { pdfdict } { empty-value }
              { The~value~#1~for~#2~is~blank~and~will~be~ignored }

\msg_new:nnn  { pdfdict } { invalid-name }
              { Name~'#1'~is~not~valid\\
                Names~of~dictionaries~should~start~with~'g_'~or~'l_' }

%    \end{macrocode}
%

% \subsection{Creating dictionaries}
% \begin{variable}
%    {\g_@@_names_seq,\g_@@_gnames_seq}
% Two seq to store the used names for diagnostics.
%    \begin{macrocode}
\seq_new:N \g_@@_lnames_seq
\seq_new:N \g_@@_gnames_seq
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}
%   {
%    \@@_name:n, \__kernel_pdfdict_name:n,
%    \@@_new:n,  \pdfdict_new:n,
%   }
%
%   This are the commands to create new dictionaries and to access their internal
%   name. All internal names start with |g__pdfdict_/| or |l__pdfdict_/|.
%
%  For the other modules we also need a kernel command to access the internal
%  name to speed up the code and allow the use standard commands
%  of the \texttt{prop} module to deal with the dictionaries. For example\\
%  |\prop_clear:c { \__kernel_pdfdict_name:n { name }}|
%
%    \begin{macrocode}
\cs_new:Npn \@@_name:n #1  % #1 dictionary name
  {
    \str_head:n{#1}_@@_/#1_prop
  }
\cs_set_eq:NN \__kernel_pdfdict_name:n \@@_name:n

\cs_new_protected:Npn \@@_new:n #1
  {
    \@@_if_exist:nTF { #1 }
      {
        \msg_error:nnee
          { pdfdict }
          { dict-already-defined }
          { \tl_to_str:n {#1} }
          { \@@_get_type:n{#1} }
       }
       {
         \str_case_e:nnF { \str_head:n{#1} }
           {
             {g}
             {
                 \prop_new:c  { \@@_name:n { #1 } }
                 \seq_gput_right:cn {g_@@_gnames_seq} { #1 }
             }
             {l}
             {
                 \prop_new:c  { \@@_name:n { #1 } }
                 \seq_gput_right:cn {g_@@_lnames_seq} { #1 }
             }
           }
           {
             \msg_error:nne{pdfdict}{invalid-name}{\tl_to_str:n{#1}}
           }
       }
  }

\cs_set_eq:NN \pdfdict_new:n \@@_new:n
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}
%   {
%     \@@_set_eq:nn,\pdfdict_set_eq:nn,
%     \@@_gset_eq:nn,\pdfdict_gset_eq:nn
%   }
%    \begin{macrocode}
\cs_new_protected:Npn \@@_set_eq:nn #1 #2
  {
    \@@_if_exist:nTF  { #2 }
      {
        \@@_if_exist:nF { #1 }
          {
            \@@_new:n { #1 }
          }
        \prop_set_eq:cc { \@@_name:n {#1} }{ \@@_name:n {#2} }
      }
      {
         \msg_error:nnn { pdfdict } { unknown-dict } { #1 }
      }
  }

\cs_set_eq:NN \pdfdict_set_eq:nn \@@_set_eq:nn

\cs_new_protected:Npn \@@_gset_eq:nn #1 #2
  {
    \@@_if_exist:nTF { #2 }
      {
        \@@_if_exist:nF { #1 }
          {
            \@@_new:n { #1 }
          }
        \prop_gset_eq:cc { \@@_name:n {#1} }{ \@@_name:n {#2} }
      }
      {
         \msg_error:nnn { pdfdict } { unknown-dict } { #1 }
      }
  }

\cs_set_eq:NN \pdfdict_gset_eq:nn \@@_gset_eq:nn
%    \end{macrocode}
% \end{macro}
%
%
% \begin{macro}[pTF]
%   {
%     \@@_if_exist:n,  \pdfdict_if_exist:n,
%   }
%
% Existence tests.
%    \begin{macrocode}
%local
\prg_new_conditional:Npnn \@@_if_exist:n #1 { p , T , F , TF }
  {
    \prop_if_exist:cTF
      { \@@_name:n { #1 }  }
      { \prg_return_true:  }
      { \prg_return_false: }
  }
\prg_set_eq_conditional:NNn
  \pdfdict_if_exist:n \@@_if_exist:n { p , T , F , TF }
%    \end{macrocode}
% \end{macro}
%
%
% \begin{macro}[pTF]
%   {
%     \@@_if_empty:n,  \pdfdict_if_empty:n,
%   }
%
% Tests for emptiness.
%    \begin{macrocode}
\prg_new_conditional:Npnn \@@_if_empty:n #1 { p , T , F , TF }
  {
    \prop_if_empty:cTF
      { \@@_name:n { #1 } }
      { \prg_return_true:  }
      { \prg_return_false: }
  }

\prg_set_eq_conditional:NNn
  \pdfdict_if_empty:n \@@_if_empty:n { p , T , F , TF }
%    \end{macrocode}
%  \end{macro}
%
%
% \begin{macro}
%   {
%     \@@_put:nnn, \pdfdict_put:nnn,
%     \@@_gput:nnn,\pdfdict_gput:nnn
%   }
% These are the commands to store values into the dictionaries.
% The main difference to adding values to a normal property list is,
% that the keys are converted with \cs{str_convert_pdfname:n}
% and that empty values are ignored.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_put:nnn #1 #2 #3  %#1 (local) dict, #2 name, #3 value
  {
    \tl_if_blank:nTF { #3 }
      {
        \msg_warning:nnnn { pdfdict }{ empty-value }{ #2 } { #1 }
      }
      {
        \@@_if_exist:nTF  { #1 }
          {
            \exp_args:Nne \prop_put:cnn
              { \@@_name:n { #1 } }{ \str_convert_pdfname:n { #2 } } { #3 }
          }
          {
            \msg_error:nnn { pdfdict } { unknown-dict } { #1 }
          }
      }
  }

\cs_set_eq:NN \pdfdict_put:nnn \@@_put:nnn
\cs_generate_variant:Nn \pdfdict_put:nnn {nne,nno,nee,nnx}

\cs_new_protected:Npn \@@_gput:nnn #1 #2 #3  %#1 global dict, #2 name, #3 value
  {
    \tl_if_empty:nTF { #3 }
      {
        \msg_warning:nnnn { pdfdict }{ empty-value }{ #2 } { #1 }
      }
      {
        \@@_if_exist:nTF { #1 }
          {
            \exp_args:Nne \prop_gput:cnn
              { \@@_name:n { #1 } }{ \str_convert_pdfname:n { #2 } } { #3 }
          }
          {
            \msg_error:nnn { pdfdict } { unknown-dict } { #1 }
          }
      }
  }

\cs_set_eq:NN \pdfdict_gput:nnn \@@_gput:nnn
\cs_generate_variant:Nn \pdfdict_gput:nnn {nne,nno,nee,nnx}
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}
%   {
%     \@@_get:nnN, \pdfdict_get:nnN,
%   }
% Recover the values. The name must be first escaped to match the stored name.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_get:nnN  #1 #2 #3 %dict,key,macro
  {
    \@@_if_exist:nTF { #1 }
      {
        \exp_args:Nne \prop_get:cnN
          { \@@_name:n { #1 } }
          { \str_convert_pdfname:n { #2 } } #3
      }
      {
        \msg_error:nnn { pdfdict } { unknown-dict } { #1 }
      }
  }

\cs_set_eq:NN \pdfdict_get:nnN \@@_get:nnN
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}
%   {
%     \@@_remove:nn, \pdfdict_remove:nn,
%     \@@_gremove:nn,\pdfdict_gremove:nn
%   }
% This removes a name/value pair from a dictionary.
% The name has to be passed through the escaping.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_remove:nn #1 #2 %dict,name
  {
    \@@_if_exist:nTF { #1 }
      {
        \exp_args:Nne \prop_remove:cn
          { \@@_name:n  { #1 } }{ \str_convert_pdfname:n { #2 } }
      }
      {
        \msg_error:nnn { pdfdict } { unknown-dict } { #1 }
      }
  }
\cs_set_eq:NN \pdfdict_remove:nn \@@_remove:nn

\cs_new_protected:Npn \@@_gremove:nn #1 #2 %dict,name
  {
    \@@_if_exist:nTF  { #1 }
       {
         \exp_args:Nne \prop_gremove:cn
           { \@@_name:n  { #1 } }{ \str_convert_pdfname:n { #2 } }
       }
       {
         \msg_error:nnn { pdfdict } { unknown-dict } { #1 }
       }
  }

\cs_set_eq:NN \pdfdict_gremove:nn \@@_gremove:nn
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}
%   { \@@_show:Nn, \pdfdict_show:n }
% This allows to show the content of dictionaries. It also displays if a
% dictionary is local or global. If both exists both are shown.
%    \begin{macrocode}
\cs_new_protected:Npn \@@_show:Nn #1#2 %#1 message command, #2 dict
  {
    \prop_if_exist:cTF { \@@_name:n { #2 } }
       {
         #1
            { pdfdict }
            { show-dict }
            { \tl_to_str:n {#2}  }
            { \prop_map_function:cN {\@@_name:n { #2 }} \msg_show_item:nn }
            { \@@_get_type:n{#2} }
            { }
       }
       {
         #1 { pdfdict } { unknown-dict } { #2 } {}{}{}
       }
  }
\cs_new_protected:Npn \pdfdict_show:n #1
  {
    \@@_show:Nn \msg_show:nneeee {#1}
  }
%    \end{macrocode}
% \end{macro}
%
%
% \begin{macro}{\@@_item:nn, \@@_item:ne}
% \begin{macro}{\pdfdict_item:nn, \pdfdict_item:ne}
%   \begin{macrocode}
\cs_new:Npn \@@_item:nn #1 #2 %#1 name, #2 value
  {
    \tl_if_blank:nF {#2} { /#1~#2~ }
  }
\cs_generate_variant:Nn \@@_item:nn {ne}
\cs_set_eq:NN \pdfdict_item:nn \@@_item:nn
\cs_generate_variant:Nn \pdfdict_item:nn {ne}
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
%
% \begin{macro}
%   {
%     \@@_use:n,\pdfdict_use:n
%   }
% \cs{@@_use:n} outputs a prop as needed in a dictionary:
%  as a list of /\meta{key} \meta{value} pairs.
% \begin{NOTE}{UF}
%  !! is e-expansion the right thing?
% \end{NOTE}
%    \begin{macrocode}
\cs_new:Npn \@@_use:n #1  %#1 dict
  {
    \prop_map_function:cN { \@@_name:n { #1 } } \@@_item:ne
  }

\cs_set_eq:NN \pdfdict_use:n \@@_use:n
%    \end{macrocode}
% \end{macro}
%
%
%    \begin{macrocode}
%</package>
%    \end{macrocode}
%
% \end{implementation}
%
% \PrintIndex
