%!TEX root=template.tex %% ---------------------------------------------------------------------------- %% novathesis — nt-render-persons.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) %% %% Person list rendering: advisers and committee table printing. %% Loaded by novathesis.cls. %% ---------------------------------------------------------------------------- \typeout{NT FILE StyFiles/nt-render-persons.sty}% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Print persons list {advisers | committee} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \NewDocumentCommand{\ntprintpersons}{O{full}mO{l}mmm}{% % #1 = format (default: full) {full, name, list} % #2 = width in % of textwidth % #3 = positioning (default: l) {l, c, r} % #4 = number of columns {0, 1, 2} % #5 = adviser | committee % #6 = {a,c} | {c,r,a,m,g} % Validate format value \IfSubStr{,full,name,list,}{,#1,}{}{% \ClassError{novathesis}{String "#1" not in "full,name,list"}{}% }% % Validate alignment value \IfStrEqCase{#3}{% {r}{\let\nt@alignment@cmd\raggedleft}% {c}{\let\nt@alignment@cmd\centering}% {l}{\let\nt@alignment@cmd\raggedright}% }[\ClassError{novathesis}{Invalid alignment "#3"}{}]% % Set up tabularx horizontal alignment \def\nt@tabularx@horz{}% \IfStrEq{#3}{c}{\def\nt@tabularx@horz{\centering\arraybackslash}}{}% \IfStrEq{#3}{r}{\def\nt@tabularx@horz{\raggedleft\arraybackslash}}{}% \IfStrEq{#3}{l}{\def\nt@tabularx@horz{\raggedright\arraybackslash}}{}% % Render the person list if toggle is enabled \iftoggle{/novathesis/print/#5}{% \begin{minipage}[t]{\linewidth}% \setlength{\parindent}{0pt}% \nt@alignment@cmd% \begin{varwidth}{#2\linewidth}% \IfStrEqCase{#4}{% {0}{\GetPersonsAsList{#5}{#6}}% {1}{\@ntprintpersonsasonecolumn{#1}{#2}{#5}{#6}}% {2}{\@ntprintpersonsastwocolumns{#1}{#5}{#6}}% }[\ClassError{novathesis}{Invalid argument [#4] to \string\ntprintpersons}{}]% \end{varwidth}% \end{minipage}% }{}% } %============================================================ % Print persons list {advisers | committee} as list %============================================================ \ExplSyntaxOn % Define variables to hold the lists \clist_new:N \l_nt_master_clist % Create variant to handle expansion correctly, so the role-list argument % (e.g. "\GetCommitteeOrder()") can be expanded before being split. \cs_generate_variant:Nn \clist_map_inline:nn { xn } % \ntforeachperson callback: keep just the name, ignore the other four % fields (title/position/institution/gender) -- this is the "short list" % view, matching what the "name" seq always stored. \cs_new_protected:Npn \@nt@person@collectname #1#2#3#4#5 { \clist_put_right:Nn \l_nt_master_clist {#2} } \NewDocumentCommand {\GetPersonsAsList} { m m o } { % --- 1. Build the Flat List --- \clist_clear:N \l_nt_master_clist % Map over the categories provided in #2 (e.g., a, c) \clist_map_inline:xn { #2 } { \ntforeachperson {#1} {##1} { \@nt@person@collectname } } % --- 2. Output or Storage --- \IfNoValueTF { #3 } { \raggedright \MemGetDefault { #1namefont } [] { #2 } % Convert clist back to comma-separated string for legacy formatter \cs_set:Npx \@advlist { \clist_use:Nn \l_nt_master_clist { , } } \FormatNamesAsList{\@advlist} } { % Store the result in the requested output macro \xdef #3 { \clist_use:Nn \l_nt_master_clist { , } } } } \cs_generate_variant:Nn \seq_set_from_clist:Nn { Ne } % Own scratch sequences rather than the kernel's shared \l_tmpa_seq/\l_tmpb_seq: % the second is built while mapping over the first, and the caller's code runs % in between, so a shared scratch could be clobbered mid-loop. \seq_new:N \l__nt_persons_names_seq \seq_new:N \l__nt_persons_out_seq % The separators carry their own surrounding spaces. This matters because % \MemSet trims stored values: the string files define e.g. "{ and }" and % "{, }", but memstore hands back "and" and "," with the spaces gone. The % defaults below therefore re-add them on BOTH sides, matching the documented % example ({,~} {~e~} {~e~}). Previously only the leading side was re-added % and the trailing space came from the per-name "#2~##1" below, which also put % a stray space in front of the very first name. \NewDocumentCommand { \FormatNamesAsList } { m % list of names O { } % prefix for each name O { \GetCommaString(\@LANG@MAIN) ~ } % middle-sep O { ~ \GetAndString(\@LANG@MAIN) ~ } % pair-sep O { ~ \GetAndString(\@LANG@MAIN) ~ } } % % \FormatNamesAsList {comma separated list of names} {name-prefix} {middle-sep} {pair-sep} {last-sep} % example: \FormatNamesAsList {Maria, Rita, Filomena} {Professora} {,~} {~e~} {~e~} { % 1. Convert the input string (comma list) into a sequence variable \seq_set_from_clist:Ne \l__nt_persons_names_seq { #1 } % 2. Create a temporary sequence to hold the modified names \seq_clear:N \l__nt_persons_out_seq % 3. Loop through the original names, prefixing each with #2 (e.g. "Prof."). % The separating space belongs to the prefix: emitting "#2~##1" % unconditionally would put a stray space in front of every name whenever % #2 is empty (the default), so that "Maria, Rita" rendered as " Maria, % Rita". Only insert it when there is actually a prefix to separate. \seq_map_inline:Nn \l__nt_persons_names_seq { \tl_if_empty:nTF { #2 } { \seq_put_right:Nn \l__nt_persons_out_seq { ##1 } } { \seq_put_right:Nn \l__nt_persons_out_seq { #2 ~ ##1 } } } % 4. Output the sequence with custom separators. % \seq_use:Nnnn {sep-between-two} {sep-between-more} {sep-before-last} % two items -> pair-sep (#4) % >2 items -> middle-sep (#3) between all but the last, last-sep (#5) % before the final name. \seq_use:Nnnn \l__nt_persons_out_seq { #4 } { #3 } { #5 } } \ExplSyntaxOff %============================================================ % Print persons list {advisers | committee} as one and two columns %============================================================ % \maxlen is the width reserved for the group title in % \@nt@ntprintpersongroup, which sizes the name block as % "\linewidth-\maxlen". It is a shared register, so it must be RESET before % every person list, even by layouts that do not reserve any title width; % otherwise a one-column list following a two-column one would inherit the % latter's value and come out too narrow. \newcommand{\@ntresetmaxlen}{% \defifundef{\stringlen}{\newdimen}% \defifundef{\maxlen}{\newdimen}% \setlength{\maxlen}{0pt}% } % \@ntgetmaxlen{}{} % #1 is iterated over; #2 is the person class handed to % \@nt@printgrouptitle@text{}{}. Note the order: roles first, % class second. \ExplSyntaxOn \clist_new:N \l__nt_getmaxlen_clist \cs_new_protected:Npn \@ntgetmaxlen #1#2 { \@ntresetmaxlen % #1 is a literal role-code list (e.g. "a,c"), fully expanded before % being split into items, matching the previous % \@for\myx:=\expanded{#1}\do{...} behaviour. Items are role codes, % not document text. \clist_set:Ne \l__nt_getmaxlen_clist {#1} \clist_map_inline:Nn \l__nt_getmaxlen_clist { \setlength{\stringlen}{\widthof{\@nt@printgrouptitle@text{#2}{##1}~~}} \ifdim\stringlen>\maxlen\relax\setlength{\maxlen}{\stringlen}\fi } } \ExplSyntaxOff \newcommand{\@ntprintpersonsasonecolumn}[4]{% % #1 = { full | name } % #2 = width in % of textwidth % #3 = adviser | committee % #4 = {a,c} | {c,r,a,m,g} % \@check@instring{#1}{full,name}{@ntprintpersonsasonecolumn}% \IfMemVoidT{#3stringpost}{}{\MemSet*{#3stringpost}{}{:\\[2pt]}}% \IfMemVoidT{#3namepost}{}{\MemSet*{#3namepost}{}{{\\}}}% \IfMemVoidT{#3remainderpre}{}{\MemSet*{#3remainderpre}{}{{\\[-2pt]}}}% \IfMemVoidT{personpost}{}{\MemSet*{personpost}{}{\\[2pt]}}% \IfMemVoidT{persongrouppost}{}{\MemSet*{persongrouppost}{}{\\[7pt]}}% \IfMemVoidT{committeetitlestringpost}{}{\MemSet*{committeetitlestringpost}{}{\\[5pt]}}% % No title width is reserved in this layout: the group title ends with a % line break (stringpost = ":\\"), so the names below start on a fresh line % and use the full width. Only reset the shared \maxlen (see % \@ntresetmaxlen). The two-column layout, whose title sits in a left % column (":&"), is the one that measures it with \@ntgetmaxlen. \@ntresetmaxlen% \begin{tabularx}{#2\linewidth}[t]{@{}>{\nt@tabularx@horz\raggedright\let\newline\\\arraybackslash\hspace{0pt}}X@{}}% \IfMemVoidTF{CommitteeTitleString}{\@LANG@COVER}{}{% \IfStrEq{#3}{committee}{% \MemGetDefault{#3titlestringpre}{}% \MemGetDefault{#3titlestringfont}{}% \GetCommitteeTitleString(\@LANG@COVER)% \MemGetDefault{#3titlestringpost}{}% }{% \MemGetDefault{committeetitlestringpre}{}% \MemGetDefault{committeetitlestringfont}{}% \strut% \MemGetDefault{committeetitlestringpost}{}% }% }% \expandnext{\forcsvlist{\@nt@ntprintpersongroup[#1]{1}{#3}}}{\expanded{#4}}% \end{tabularx}% } \newcommand{\@ntprintpersonsastwocolumns}[3]{% % #1 = { full | name } % #2 = adviser | committee % #3 = {a,c} | {c,r,a,m,g} % \typeout{NTPRINTPERSONSASTWOCOLUMNS 1=[#1] 2=[#2] 3=[#3]}\NTPRINTPERSONSASTWOCOLUMNS % \@check@instring{#1}{full,name}{@ntprintpersonsasonecolumn}% \IfMemVoidT{#2stringpost}{}{\MemSet*{#2stringpost}{}{:&}}% \IfMemVoidT{#2remainderpre}{}{\MemSet*{#2remainderpre}{}{{\\[-2pt]}}}% \IfMemVoidT{personpost}{}{\MemSet*{personpost}{}{\\[2pt]}}% \IfMemVoidT{persongrouppost}{}{\MemSet*{persongrouppost}{}{\\[14pt]}}% \IfMemVoidT{committeetitlestringpre}{}{\MemSet*{committeetitlestringpre}{}{&}}% \IfMemVoidT{committeetitlestringpost}{}{\MemSet*{committeetitlestringpost}{}{\\[5pt]}}% % Role-string column alignment: independent of \ntprintpersons's own #3 % (which positions the WHOLE block on the page, l/c/r). Defaults to "r" % (the historical hardcoded behaviour); a school can left-align it (e.g. % when role strings of varying length, like "Gutachter"/"Gutachterin", % should keep their leading number flush) by calling % \MemSet{adviserstringalign}{}{l} (or committeestringalign) in its own % -defaults.clo BEFORE \ntprintpersons runs. \IfMemVoidT{#2stringalign}{}{\MemSet*{#2stringalign}{}{r}}% % \typeout{NTPRINTPERSONSASTWOCOLUMNS 1=[#1] 2=[#2] 3=[#3] #2stringpost=[\csuse{the#2stringpost}()]}\NTPRINTPERSONSASTWOCOLUMNS \@ntgetmaxlen{#3}{#2}% % \IfStrEqCase doesn't expand its own comparand, so the stored value must % be pre-expanded via \edef first -- passing it inline compared the % literal, unexpanded macro call against "r"/"c"/"l" and always fell % through to the else-branch (\ClassError). \MemGetDefault itself can't be % used here since it's a plain \NewDocumentCommand (not expandable, unlike % \MemGet), so \edef can't unfold it; \MemGet is safe because the key is % never void at this point (just defaulted to "r" above if it was). \edef\@nt@stringalign{\MemGet{#2stringalign}{}}% \IfStrEqCase{\@nt@stringalign}{% {r}{\begin{tabular}[t]{@{}r@{~~}l@{}}}% {c}{\begin{tabular}[t]{@{}c@{~~}l@{}}}% {l}{\begin{tabular}[t]{@{}l@{~~}l@{}}}% }[\ClassError{novathesis}{Invalid alignment "\@nt@stringalign"}{}]% \IfStrEq{#2}{committee}{% \IfMemVoidF{CommitteeTitleString}{\@LANG@COVER}{% \MemGetDefault{#2titlestringpre}{}% \MemGetDefault{#2titlestringfont}{}% \GetCommitteeTitleString(\@LANG@COVER)% \MemGetDefault{#2titlestringpost}{}% }% }{}% \expandnext{\forcsvlist{\@nt@ntprintpersongroup[#1]{2}{#2}}}{\expanded{#3}}% \end{tabular}% } % Dispatcher for the Type-A adviser/committee string stores. % Needed because the store names are CamelCase but the group-type #1 is lowercase. % #1 = {adviser | committee} % #2 = role key (e.g. "a", "c") \newcommand{\@nt@getstrgroup}[2]{% \IfStrEq{#1}{adviser}% {\MemGetDefault{AdviserString}{#2,\@nt@advnumber,\@nt@advgender,\@LANG@COVER}}% {\MemGetDefault{CommitteeString}{#2,\@nt@advnumber,\@nt@advgender,\@LANG@COVER}}% } \newcommand{\@nt@printgrouptitle@text}[2]{% % #1 = {adviser | committee} % #2 = {a,c} | {c,r,a,m,g} \ifnum\ntpersoncount{#1}{#2}>0\relax% \ntpersongender{#1}{#2}{\@nt@advgender}% \@nt@reducenumber{\ntpersoncount{#1}{#2}}{\@nt@advnumber}% \bgroup% \MemGetDefault{#1stringfont}[]{#2}% \@nt@getstrgroup{#1}{#2}% \egroup% \fi% } \newcommand{\@nt@printgrouptitle}[2]{% % #1 = {adviser | committee} % #2 = {a,c} | {c,r,a,m,g} \ifnum\ntpersoncount{#1}{#2}>0\relax% \ntpersongender{#1}{#2}{\@nt@advgender}% \@nt@reducenumber{\ntpersoncount{#1}{#2}}{\@nt@advnumber}% \MemGetDefault{#1stringpre}[]{#2}% \bgroup% \MemGetDefault{#1stringfont}[]{#2}% \@nt@getstrgroup{#1}{#2}% \egroup% \MemGetDefault{#1stringpost}[]{#2}% \fi% } \newcommand{\@nt@ntprintpersongroup}[4][full]{% % #1 = { full | name | list } % #2 = {1 | 2} % #3 = {adviser | committee} % #4 = {a,c} | {c,r,a,m,g} \ifnum\ntpersoncount{#3}{#4}>0\relax% \@nt@printgrouptitle{#3}{#4}% \begin{varwidth}[t]{\linewidth-\maxlen}% \raggedright% \IfStrEq{#1}{list}{% \GetPersonsAsList{#3}{#4}% }{% \ntforeachperson{#3}{#4}{\@nt@printonename{#1}{#3}{#4}}% }% \end{varwidth}% \MemGet{persongrouppost}{}% \fi% } %============================================================ % Print person aux funcitons %============================================================ % #1=position #2=institution -> joined display text, matching the old % glued "position, institution" look (comma-space between the two only % when both are present). \newcommand{\@nt@person@remainder}[2]{% \IfStrEq{#1}{}{% \def\@nt@p@remainder{#2}% }{% \IfStrEq{#2}{}{% \def\@nt@p@remainder{#1}% }{% \def\@nt@p@remainder{#1, #2}% }% }% } \newcommand{\@nt@printonename}[8]{% % curried by \@nt@ntprintpersongroup: #1 = { full | name } % #2 = adviser | committee % #3 = {a,c} | {c,r,a,m,g} % from \ntforeachperson: #4=title #5=name #6=position #7=institution #8=gender \MemGetDefault{personpre}[]{#3}% \begingroup \raggedright% \MemGetDefault{#2namepre}[]{#3}% \MemGetDefault{#2namefont}[]{#3}% \IfStrEq{#4}{}{#5}{#4~#5}% title-prefixed name, when a title was given \IfStrEq{#1}{full}{% \@nt@person@remainder{#6}{#7}% \IfStrEq{\@nt@p@remainder}{}{}{% \MemGetDefault{#2remainderpre}[]{#3}% \bgroup% \MemGetDefault{#2remainderfont}[]{#3}% \@nt@p@remainder% \egroup% \MemGetDefault{#2remainderpost}[]{#3}% }% }{}% \endgroup \MemGetDefault{personpost}[]{#3}% } % -------------------------------------------------------- % Reduce Number Logic: Result = Max(Current, Min(Count, 2)) % -------------------------------------------------------- % #1: Current accumulated number (Optional, default 1) % #2: A raw count, already looked up by the caller (e.g. via \ntpersoncount) % #3: The macro to store the result in (globally) \ExplSyntaxOn \NewDocumentCommand \@nt@reducenumber { O{1} m m } { \xdef #3 { \int_max:nn {#1} { \int_min:nn {#2} {2} } } } \ExplSyntaxOff