%!TEX root=template.tex %% ---------------------------------------------------------------------------- %% novathesis — memstore.sty %% ---------------------------------------------------------------------------- %% %% Version 8.4.0 (2026-09-27) %% Copyright (C) 2004-26 by João M. Lourenço %% Departamento de Informática (www.di.fct.unl.pt) %% Faculdade de Ciências e Tecnologia (www.fct.unl.pt) %% Universidade NOVA de Lisboa (www.unl.pt) %% %% Memory storage with robust value trimming using LaTeX3 properties. %% ---------------------------------------------------------------------------- \NeedsTeXFormat{LaTeX2e} \ProvidesExplPackage{memstore}{2024/01/05}{1.6.1}{Memory storage with robust value trimming} \typeout{ NT FILE StyFiles/memstore.sty } % % --------------------------------------------------------------------------- % Argument spec for the \Set commands built by \NewMemStore. % % Accepts \Set(key)={value} \Set*(key)={value} % \Set(key):={value} \Set*(key):={value} % and tolerates the separator being absent entirely. % % This deliberately avoids the 'u' (until) argument type that earlier versions % used. 'u' is deprecated, is absent from the LaTeX kernel's ltcmd and survives % only in the xparse package, so relying on it made this package silently % dependent on something else having loaded xparse first; when 'u' is % unavailable every \Set dies at definition time with "Invalid argument % type 'u'". 'u' also scans ahead for its delimiter, so a call that omits the % '=' runs away to the end of the file. The optional-token type 't' is % standard, needs no xparse, and simply reports absence. % % The spec is a *str* constant on purpose: \str_const:Nn gives every character % catcode 12, which is what makes "t:" match the ':' typed at a call site. % Written literally inside \ExplSyntaxOn the ':' would be catcode 11 (letter) % and would NOT match, silently capturing the ':' as the value instead -- % "\SetLangLong(cat):={catalan}" would store ':' and leave {catalan} stranded. \str_const:Nn \c__mem_setter_spec_str { s r() t: t= m } %%======================================================================= %% VARIABLES & VARIANTS %%======================================================================= \bool_new:N \l__mem_found_bool \tl_new:N \l__mem_current_key_tl \tl_new:N \l__mem_current_val_tl % Generate variants \cs_generate_variant:Nn \prop_gput:Nnn { cee, ceV } \cs_generate_variant:Nn \prop_item:Nn { ce } \cs_generate_variant:Nn \prop_if_in:NnTF { ceTF } % NOTE: \prop_if_in:ceT must be derived from the "T" base, NOT from the "TF" % one. A T-form variant cannot be built from a TF base: the request is % silently discarded (no error), leaving \prop_if_in:ceT undefined. Because an % undefined conditional swallows its branches, \__mem_check_defined:nn and % \__mem_check_blank:nn below then always fell through to the false branch -- % i.e. \IfMemDefinedTF, \IfMemBlankTF, \MemMatch and friends returned the WRONG % answer rather than failing loudly. \cs_generate_variant:Nn \prop_if_in:NnT { ceT } \cs_generate_variant:Nn \prop_if_exist:NTF { cTF } \cs_generate_variant:Nn \prop_get:NnN { cnN, ceN } \cs_generate_variant:Nn \prop_gremove:Nn { ce } \cs_generate_variant:Nn \prop_map_inline:Nn { cn } \cs_generate_variant:Nn \prop_log:N { c } \cs_generate_variant:Nn \tl_set:Nn { cn } \cs_generate_variant:Nn \tl_set:Nx { cx } %%======================================================================= %% INTERNAL HELPERS %%======================================================================= \cs_new_protected:Npn \__mem_exec_branch:nnn #1 #2 #3 { \cs_set:Npn \__mem_tmp_code:nn ##1 ##2 { #1 } \__mem_tmp_code:nn { #2 } { #3 } } \cs_new_protected:Npn \__mem_exec_foreach:nnn #1 #2 #3 { \cs_set:Npn \__mem_tmp_loop:nn ##1 ##2 { #1 } \__mem_tmp_loop:nn { #2 } { #3 } } \cs_new_protected:Npn \__mem_ensure_exists:n #1 { \prop_if_exist:cF { g__mem_#1_prop } { \prop_new:c { g__mem_#1_prop } } } %%======================================================================= %% CORE COMMANDS: SET AND GET %%======================================================================= % Syntax: \MemSet(*){}{}{} \NewDocumentCommand{\MemSet} { s m m m } { \__mem_ensure_exists:n {#2} % Pre-expand and trim the key using \tl_set:Ne (edef-like exhaustive expansion). % \prop_gput:cee's e-variant cannot fully expand keys containing \tl_trim_spaces:n % because that function uses \unexpanded internally to protect its result, which % stops further expansion of macros inside the \expanded context used by e-variants. % \tl_set:Ne runs independently of any surrounding \expanded, so it correctly % expands all macros (e.g. \@DOCTYPE -> phd) before the key is stored. \tl_set:Ne \l__mem_current_key_tl { #3 } \tl_trim_spaces:N \l__mem_current_key_tl \IfBooleanTF {#1} { % Starred: exhaustively expand value before storing \tl_set:Ne \l__mem_current_val_tl { #4 } \tl_trim_spaces:N \l__mem_current_val_tl } { % Unstarred: store value as-is (macros remain unexpanded) \tl_set:Nn \l__mem_current_val_tl { #4 } \tl_trim_spaces:N \l__mem_current_val_tl } \prop_gput:ceV { g__mem_#2_prop } { \l__mem_current_key_tl } \l__mem_current_val_tl } % Syntax: \MemGet(*){}{} % % Expandable, so it may be used inside \edef / \tl_set:Ne / \MemSet* -- but % ONLY on the success path. \msg_error:nnnn is not expandable: if the store or % key is missing inside an expansion context, the error cannot fire and its % tokens are written verbatim into the result instead, e.g. % \edef\x{\MemGet{Lang}{nope}} % -> \x = {\msg_error:nnnn {memstore}{unknown-key}{Lang}{nope}} % together with a confusing low-level "Undefined control sequence". % Outside an expansion context the error is raised normally. % When a missing entry is a legitimate possibility inside \edef, use % \MemGetSafe (below), which expands to nothing and never errors. \NewExpandableDocumentCommand{\MemGet} { s m m } { \prop_if_exist:cTF { g__mem_#2_prop } { \prop_if_in:ceTF { g__mem_#2_prop } {#3} { \IfBooleanTF {#1} { \use:e { \prop_item:ce { g__mem_#2_prop } {#3} } } { \prop_item:ce { g__mem_#2_prop } {#3} } } { \NoCaseChange { \msg_error:nnnn { memstore } { unknown-key } { #2 } { #3 } } } } { \NoCaseChange{ \msg_error:nnn { memstore } { unknown-memory } {#2} } } } %%======================================================================= %% CONDITIONAL LOGIC %%======================================================================= \cs_new_protected:Npn \__mem_check_defined:nn #1 #2 { \bool_set_false:N \l__mem_found_bool \tl_clear:N \l__mem_current_key_tl \tl_clear:N \l__mem_current_val_tl \prop_if_exist:cT { g__mem_#1_prop } { \clist_map_inline:nn { #2 } { \prop_if_in:ceT { g__mem_#1_prop } { ##1 } { \bool_set_true:N \l__mem_found_bool \tl_set:Nn \l__mem_current_key_tl { ##1 } \prop_get:ceN { g__mem_#1_prop } { ##1 } \l__mem_current_val_tl \clist_map_break: } } } } \cs_new_protected:Npn \__mem_check_blank:nn #1 #2 { \bool_set_false:N \l__mem_found_bool \tl_clear:N \l__mem_current_key_tl \tl_clear:N \l__mem_current_val_tl \prop_if_exist:cT { g__mem_#1_prop } { \clist_map_inline:nn { #2 } { \prop_if_in:ceT { g__mem_#1_prop } { ##1 } { \prop_get:ceN { g__mem_#1_prop } { ##1 } \l__mem_current_val_tl \tl_if_blank:VT { \l__mem_current_val_tl } { \bool_set_true:N \l__mem_found_bool \tl_set:Nn \l__mem_current_key_tl { ##1 } \clist_map_break: } } } } } % Syntax: \IfMemDefinedTF(*){}{}{}{} \NewDocumentCommand{\IfMemDefinedTF} { s m m +m +m } { \__mem_check_defined:nn {#2} {#3} \bool_if:NTF \l__mem_found_bool { \IfBooleanT { #1 } { \tl_set:Nx \l__mem_current_val_tl { \l__mem_current_val_tl } } \__mem_exec_branch:nnn { #4 } { \l__mem_current_key_tl } { \l__mem_current_val_tl } } { \__mem_exec_branch:nnn {#5} {} {} } } \NewDocumentCommand{\IfMemDefinedT} { s m m +m } { \__mem_check_defined:nn {#2} {#3} \bool_if:NT \l__mem_found_bool { \IfBooleanT { #1 } { \tl_set:Nx \l__mem_current_val_tl { \l__mem_current_val_tl } } \__mem_exec_branch:nnn { #4 } { \l__mem_current_key_tl } { \l__mem_current_val_tl } } } \NewDocumentCommand{\IfMemDefinedF} { s m m +m } { \__mem_check_defined:nn {#2} {#3} \bool_if:NF \l__mem_found_bool { \__mem_exec_branch:nnn {#4} {} {} } } % Syntax: \IfMemBlankTF(*){}{}{}{} \NewDocumentCommand{\IfMemBlankTF} { s m m +m +m } { \__mem_check_blank:nn {#2} {#3} \bool_if:NTF \l__mem_found_bool { \IfBooleanT { #1 } { \tl_set:Nx \l__mem_current_val_tl { \l__mem_current_val_tl } } \__mem_exec_branch:nnn { #4 } { \l__mem_current_key_tl } { \l__mem_current_val_tl } } { \__mem_exec_branch:nnn {#5} {} {} } } \NewDocumentCommand{\IfMemBlankT} { s m m +m } { \__mem_check_blank:nn {#2} {#3} \bool_if:NT \l__mem_found_bool { \IfBooleanT { #1 } { \tl_set:Nx \l__mem_current_val_tl { \l__mem_current_val_tl } } \__mem_exec_branch:nnn { #4 } { \l__mem_current_key_tl } { \l__mem_current_val_tl } } } \NewDocumentCommand{\IfMemBlankF} { s m m +m } { \__mem_check_blank:nn {#2} {#3} \bool_if:NF \l__mem_found_bool { \__mem_exec_branch:nnn {#4} {} {} } } % Syntax: \IfMemVoidTF{}{}{}{} \NewDocumentCommand{\IfMemVoidTF} { m m +m +m } { \prop_if_exist:cTF { g__mem_#1_prop } { \prop_if_in:ceTF { g__mem_#1_prop } {#2} { \prop_get:ceN { g__mem_#1_prop } {#2} \l__mem_current_val_tl \tl_if_blank:VTF \l__mem_current_val_tl { #3 } % Value is blank -> Void { #4 } % Value has content -> Not Void } { #3 } % Key does not exist -> Void } { #3 } % Storage does not exist -> Void } \NewDocumentCommand{\IfMemVoidT} { m m +m } { \IfMemVoidTF { #1 } { #2 } { #3 } { } } \NewDocumentCommand{\IfMemVoidF} { m m +m } { \IfMemVoidTF { #1 } { #2 } { } { #3 } } %%======================================================================= %% MATCHING COMMANDS %%======================================================================= % Syntax: \MemMatch(*){}{}{}{} \NewDocumentCommand{\MemMatch} { s m m m m } { \IfBooleanTF {#1} { \IfMemDefinedTF* { #2 } { #3 } { \tl_set:Nx #4 {##1} \tl_set:Nn #5 {##2} } { \msg_error:nnn { memstore } { no-match-error } { #2 } } } { \IfMemDefinedTF { #2 } { #3 } { \tl_set:Nx #4 {##1} \tl_set:Nn #5 {##2} } { \msg_error:nnn { memstore } { no-match-error } { #2 } } } } % Syntax: \MemMatchCS(*){}{}{}{} \NewDocumentCommand{\MemMatchCS} { s m m m m } { \IfBooleanTF {#1} { \IfMemDefinedTF* { #2 } { #3 } { \tl_set:cx {#4} {##1} \tl_set:cn {#5} {##2} } { \msg_error:nnn { memstore } { no-match-error } { #2 } } } { \IfMemDefinedTF { #2 } { #3 } { \tl_set:cx {#4} {##1} \tl_set:cn {#5} {##2} } { \msg_error:nnn { memstore } { no-match-error } { #2 } } } } %%======================================================================= %% UTILITIES %%======================================================================= % Syntax: \MemGetDefault{}[]{} % Outputs the value at if defined and non-blank; % else the value at (if given) if defined and non-blank; % else nothing. \NewDocumentCommand{\MemGetDefault} { m o m } { \IfMemVoidTF {#1} {#3} { \IfValueT {#2} { \IfMemVoidF {#1} {#2} { \MemGet {#1} {#2} } } } { \MemGet {#1} {#3} } } % Syntax: \MemGetSafe{}{} % Like \MemGet but returns empty (no error) when the store or key doesn't exist. \NewExpandableDocumentCommand{\MemGetSafe} { m m } { \prop_if_exist:cTF { g__mem_#1_prop } { \prop_if_in:ceTF { g__mem_#1_prop } {#2} { \prop_item:ce { g__mem_#1_prop } {#2} } { } } { } } % Syntax: \MemUndef{}{} \NewDocumentCommand{\MemUndef} { m m } { \prop_if_exist:cT { g__mem_#1_prop } { \clist_map_inline:nn { #2 } { \prop_gremove:ce { g__mem_#1_prop } { ##1 } } } } % Syntax: \MemForEach{}{} \NewDocumentCommand{\MemForEach} { m +m } { \prop_if_exist:cT { g__mem_#1_prop } { \prop_map_inline:cn { g__mem_#1_prop } { \__mem_exec_foreach:nnn { #2 } { ##1 } { ##2 } } } } % Syntax: \MemDebug{} \NewDocumentCommand{\MemDebug} { m } { \prop_if_exist:cTF { g__mem_#1_prop } { \prop_log:c { g__mem_#1_prop } } { \msg_warning:nnn { memstore } { unknown-memory } {#1} } } %%======================================================================= %% DYNAMIC INTERFACE (\NewMemStore) %%======================================================================= % Syntax: \NewMemStore{} \NewDocumentCommand{\NewMemStore} { m } { \__mem_ensure_exists:n {#1} % Define \Set()={} (see \c__mem_setter_spec_str above) % Note: key trimming and expansion are handled inside \MemSet. % \use:e is needed so the spec constant is expanded into place; the command % name still comes from \exp_not:c, and the body is passed unexpanded after. \use:e { \exp_not:N \NewDocumentCommand \exp_not:c { Set#1 } { \c__mem_setter_spec_str } } { % ##1 = star, ##2 = key, ##3 = optional ':', ##4 = optional '=', % ##5 = value. ##3/##4 are consumed only to keep them out of the value. \IfBooleanTF { ##1 } { \MemSet* { #1 } { ##2 } { ##5 } } { \MemSet { #1 } { ##2 } { ##5 } } } % Define \Get() — r() argument, expandable, normal form \exp_args:Nc \NewExpandableDocumentCommand { Get#1 } { s r() } { \IfBooleanTF { ##1 } { \MemGet* { #1 } { ##2 } } { \MemGet { #1 } { ##2 } } } % Define \GetB{key} — m argument (braces), PDF-safe form \exp_args:Nc \NewExpandableDocumentCommand { Get#1B } { m } { \MemGet { #1 } { ##1 } } } %%======================================================================= %% MESSAGES %%======================================================================= \msg_new:nnn { memstore } { unknown-memory } { Memory~container~'#1'~does~not~exist. } \msg_new:nnn { memstore } { unknown-key } { Key~'#2'~not~found~in~memory~'#1'. } \msg_new:nnn { memstore } { no-match-error } { No~valid~key~from~the~requested~list~found~in~memory~'#1'.\\ Compilation~cannot~proceed. } \endinput