1

I'm trying to make a class for a cv document. As part of that document, there will be a table that contains all the education information. What I'm trying to achieve is to have some sort of template that specifies what one of these blocks of information looks like, and then have a macro that instantiates one of those blocks with the relevant data. My current attempt is trying to use pgfkeys to handle passing the data into the macro. A stripped down version of the class looks like this:

\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{cv}

\LoadClass[10pt,a4paper]{article}

\RequirePackage{tabularx}
\RequirePackage{calc}
\RequirePackage{xparse}

\makeatletter
\protected\def\z#1{\pdfsavepos\write\@auxout{\gdef\string\tpos#1{\the\pdflastxpos}}}%
\def\foo#1#2{\ifcsname tpos#1\endcsname\the\dimexpr\csname tpos#2\endcsname sp -\dimexpr\csname tpos#1\endcsname sp\relax\fi}
\makeatother

\RequirePackage{pgfkeys}
\pgfkeys{
    /education/.is family, /education,
    title/.estore in=\educationtitle,
    programme/.estore in=\educationprogramme,
    notes/.estore in=\educationnotes,
    notes/.default={},
    institute/.estore in=\educationinstitute,
    thesisheader/.estore in=\educationthesisheader,
    thesisheader/.default=Thesis:,
    thesistitle/.estore in=\educationthesistitle,
    selectedcourses/.estore in=\educationselectedcourses,
    selectedcourses/.default={},
    dates/.estore in=\educationdates,
}

\newenvironment{education}{
    \setlength{\tabcolsep}{5pt}
    \setlength{\extrarowheight}{0pt}
    \begin{tabular}{!{\z{a}}r!{\z{b}}p{(\textwidth-\foo{a}{b})-\tabcolsep}}
}{
    \end{tabular}
}

\NewDocumentCommand{\educationitem}{o}{
    \pgfkeys{/education,notes,selectedcourses,thesisheader,#1}
    \educationdates{}           & \textbf{\educationtitle{}}        \\
                                & \educationprogramme{}             \\
                                & \educationnotes{}                 \\
                                & \textit{\educationinstitute{}}    \\
    \educationthesisheader{}    & \educationthesistitle{}           \\
    Selected courses:           & \educationselectedcourses{}       \\
}

(There is some special stuff going on with the width of the columns, such that the second column fills out the remaining available width. I took this from some other answer here some time ago.)

I would ideally then use the class something like this:

\documentclass{cv}

\begin{document}
\begin{education}
\educationitem[
    title={Master of Science},
    programme={Some programme},
    notes={Some notes about the degree},
    institute={Some institute},
    dates={2014--2017},
    thesistitle={The title of the thesis},
    selectedcourses={Some list of courses that might get pretty long, filling out the usable space of the row.},
]{}


\educationitem[
    title={Bachelor of Science},
    programme={Some other programme},
    notes={Some more notes about the degree},
    institute={Some other institute},
    dates={2011--2014},
    thesisheader={Honours thesis:},
    thesistitle={The title of the thesis},
    selectedcourses={Some list of courses that might get pretty long, filling out the usable space of the row.},
]{}
\end{education}
\end{document}

This should hopefully yield a table looking like this:

enter image description here

Unfortunately, instead I get an Undefined control sequence error on usage of the \educationitem macro. I just asked a question about this, but I simplified the situation a bit too much, leading to answers that don't work for my actual intent. (The XY-problem strikes again...)

So here's my real question: how can I construct a macro that can be used roughly as above to give the result I want? Solutions using pgfkeys are slightly preferred, but I'm not particularly attached to what I have so far. Hopefully I've supplied the correct amount of information this time!

3 Answers 3

2

You can do the

argument forwarding

with PGFkeys, too.

I also prefer value-keys, i.e. keys that store the value directly. That means, the value of a key /path/to/key can simply be retrieved via

\pgfkeysvalueof{/path/to/key}

instead of

\macroForKey

(Internally, this is of course also just a macro namely \pgfk@/path/to/key.)

PGFkeys already uses a macro for what a key should do with its value why not also use PGFkeys for storing the actual value?

The handler to install such a key is .initial which with you also declare its initial value. This type of key also allows us to use /.add, /.prefix and /.append which we can use to put “more” value in a key.

I'm also using

\pgfqkeys{/education}{<kv>}

which is a slightly faster version of

\pgfkeys{/education/.cd,<kv>}

in the definition of \educationset.


Then, inside the definition of \educationitem we use #1 in every cell in combination with \pgfkeys in the /education path.

This means, for every cell, all values are set again and again (and forgotten by TeX by the end of the cell) and only one – the one we need in that cell – is actualy retrieved.

PGFkeys also comes with a key-filtering mechanism which we could use here (see Option A in the code below), though I doubt we gain something from it since all our keys do is just setting a value. (If the keys would do complicated calculations and assignments and whatnot we certainly wouldn't want to have to do this nine times.)

I'm not familiar with xparse which is why I'm using plain LaTeX2e \providecommand.

cv.cls

\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{cv}
\LoadClass[10pt,a4paper]{article}
\RequirePackage{tabularx}
\RequirePackage{pgfkeys}
\providecommand\educationvaluefilter[2]{%
  % Option A: with a filter, discards all other keys
  % probably quicker when there's much more
  % to do than just setting a value
%  \pgfkeys{/pgf/key filters/equals/.install key filter=/education/#2}%
%  \pgfqkeysfiltered{/education}{#1}%
  % Option B: just setting all values
  \educationset{#1}%
  % and then type it:
  \pgfkeysvalueof{/education/#2}}
\providecommand*\educationset{\pgfqkeys{/education}}
\educationset{
  title/.initial=,
  programme/.initial=,
  notes/.initial=,
  institute/.initial=,
  thesisheader/.initial=Thesis:,
  thesistitle/.initial=,
  selectedcourses/.initial=,
  dates/.initial=,
}

\newenvironment{education}{
  \setlength{\tabcolsep}{5pt}%
  \setlength{\extrarowheight}{0pt}%
  \tabularx{\linewidth}{rX}
    }{
  \endtabularx
}
\providecommand{\educationitem}[1][]{
  \educationvaluefilter{#1}{dates}        & \textbf{\educationvaluefilter{#1}{title}}     \\
                                          & \educationvaluefilter{#1}{programme}          \\
                                          & \educationvaluefilter{#1}{notes}              \\
                                          & \textit{\educationvaluefilter{#1}{institute}} \\
  \educationvaluefilter{#1}{thesisheader} & \educationvaluefilter{#1}{thesistitle}        \\
  Selected courses:                       & \educationvaluefilter{#1}{selectedcourses}    \\
}
5
  • I've accepted this answer over the others because it lets me keep using pgfkeys. Thank you! Commented Aug 24, 2022 at 14:15
  • The difference with this kind of argument forwarding and what expkv-cs does is that expkv-cs only needs to parse the key=value list once, what's being forwarded in the \ekvcHash-variant is a preparsed list in which access to a specific key's value is relatively fast. For comparison (using the education environment as defined in your answer): Your method A takes roughly 2.5 times as long as my \educationitem, and method B takes roughly 1.7 times as long; and using the faster yet harder to adapt \ekvcSplit-variant takes only ca. 0.7 times as long as the code from my answer.
    – Skillmon
    Commented Aug 25, 2022 at 7:48
  • 1
    Also, there is an error (was in my answer as well) with the \NewDocumentCommand call. That one should be \NewDocumentCommand{\educationitem}{O{}} (with o you'll get an error from pgfkeys if no optional argument is used).
    – Skillmon
    Commented Aug 25, 2022 at 7:51
  • @Skillmon I can't reproduce these times. With two \educationitems they all take roughly the same time to compile on my computer. Commented Aug 26, 2022 at 15:51
  • @Qrrbrbirlbel timing done with l3benchmark, typesetting the whole tabularx in a vbox :) With a single usage the document structure and package load times dominate over the time needed for \educationitem. My numbers only compare the environment+\educationitem directly against each other.
    – Skillmon
    Commented Aug 26, 2022 at 18:17
1

Your issue is that inside of a tabular every cell forms a group, so your definitions of your macros are lost after the first & in \educationitem since pgfkeys only provides local assignments.

The following uses another key=value package, namely expkv-cs, that doesn't have to face this limitation since it works by argument forwarding instead of assignments, making table building much easier (though the interface is somewhat limited compared to the multitude of different key-handlers in pgfkeys, but for simply collecting and reordering values it suffices).

I've also used tabularx for your education table, that seems much simpler than the approach using \pdfsavepos. Also, it's a bad idea to define macros such as \z or \foo to do very specific things in your class (and defining one-letter macros is most of the time a bad idea as well).

Additional the following throws an error if title, institute, or thesistitle are omitted, and doesn't print an empty line if the non-mandatory keys are empty.

cv.cls

\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{cv}

\LoadClass[10pt,a4paper]{article}

\RequirePackage{tabularx}
\RequirePackage{xparse}

\RequirePackage{expkv-cs}
% to grab an optional argument
\NewDocumentCommand\educationitem{O{}}{\educationitem@kv{#1}}
\newcommand\educationitem@ifnotempty[2]
  {%
    \if\relax\detokenize\expanded{{\ekvcValue{#1}{#2}}}\relax
      \expandafter\@gobbletwo
    \fi
    \@firstofone
  }
\newcommand*\educationitem@required[1]
  {\ClassError{cv}{The `#1' key is required}{}}
\ekvcHash\educationitem@kv
  {% list of default values, you could later change them using
   % \ekvcChange\educationitem@kv{<key>=<value>-list}
     title = \educationitem@required{title}
    ,programme = {}
    ,notes = {}
    ,institute = \educationitem@required{institute}
    ,thesisheader = Thesis:
    ,thesistitle = \educationitem@required{thesistitle}
    ,selectedcourses = {}
    ,dates = {}
  }
  {% to be used inside a tabular(x), so no need to pay attention for stray
   % spaces
    \ekvcValue{dates}{#1}        & \ekvcValue{title}{#1}              \\
    \educationitem@ifnotempty{programme}{#1}{& \ekvcValue{programme}{#1} \\}
    \educationitem@ifnotempty{notes}{#1}{& \ekvcValue{notes}{#1} \\}
                                 & \textit{\ekvcValue{institute}{#1}} \\
    \ekvcValue{thesisheader}{#1} & \ekvcValue{thesistitle}{#1}        \\
    \educationitem@ifnotempty{selectedcourses}{#1}
      {Selected courses: & \ekvcValue{selectedcourses}{#1} \\}
  }

\NewDocumentEnvironment{education}{+b}
  {%
    \setlength{\tabcolsep}{5pt}%
    \setlength{\extrarowheight}{0pt}%
    \par\noindent
    \begin{tabularx}{\linewidth}{rX}%
      #1%
    \end{tabularx}%
  }{}

main.tex

\documentclass{cv}

\begin{document}
\begin{education}
\educationitem[
    title={Master of Science},
    programme={Some programme},
    notes={Some notes about the degree},
    institute={Some institute},
    dates={2014--2017},
    thesistitle={The title of the thesis},
    selectedcourses={Some list of courses that might get pretty long, filling out the usable space of the row.},
]


\educationitem[
    title={Bachelor of Science},
    programme={Some other programme},
    notes={Some more notes about the degree},
    institute={Some other institute},
    dates={2011--2014},
    thesisheader={Honours thesis:},
    thesistitle={The title of the thesis},
    selectedcourses={Some list of courses that might get pretty long, filling out the usable space of the row.},
]
\end{education}
\end{document}

Result

enter image description here

1
  • Thank you for the answer. I've accepted the answer that used pgfkeys, but I did adapt your idea to prevent printing empty lines in the table when optional arguments are omitted. Commented Aug 24, 2022 at 14:16
0

The problem with \educationitem is that it is carried out inside a table-cell while a table-cell forms a local scope. Thus all the local definitions in terms of .estore are gone after the first &. You can use \expanded-\unexpanded-trickery for obtaining the toplevel-expansion of all your scratch-macros before the tabular-environment "sees" the &—usually\noexpand is not needed with &, but I saw code where & was made active, so ...

\documentclass[10pt,a4paper]{article}

\RequirePackage{tabularx}
\RequirePackage{calc}
\RequirePackage{xparse}

\makeatletter
\protected\def\z#1{\pdfsavepos\write\@auxout{\gdef\string\tpos#1{\the\pdflastxpos}}}%
\def\foo#1#2{\ifcsname tpos#1\endcsname\the\dimexpr\csname tpos#2\endcsname sp -\dimexpr\csname tpos#1\endcsname sp\relax\fi}
\makeatother

\RequirePackage{pgfkeys}
% Empty defaults in case keys for defining the macros are not used at all:
\newcommand*\educationtitle{}%
\newcommand*\educationprogramme{}%
\newcommand*\educationnotes{}%
\newcommand*\educationinstitute{}%
\newcommand*\educationthesisheader{}%
\newcommand*\educationthesistitle{}%
\newcommand*\educationselectedcourses{}%
\newcommand*\educationdates{}%
\pgfkeys{
    /education/.is family, /education,
    title/.estore in=\educationtitle,
    programme/.estore in=\educationprogramme,
    notes/.estore in=\educationnotes,
    notes/.default={},
    institute/.estore in=\educationinstitute,
    thesisheader/.estore in=\educationthesisheader,
    thesisheader/.default=Thesis:,
    thesistitle/.estore in=\educationthesistitle,
    selectedcourses/.estore in=\educationselectedcourses,
    selectedcourses/.default={},
    dates/.estore in=\educationdates,
}

\newenvironment{education}{%
    \setlength{\tabcolsep}{5pt}%
    \setlength{\extrarowheight}{0pt}%
    \begin{tabular}{!{\z{a}}r!{\z{b}}p{(\textwidth-\foo{a}{b})-\tabcolsep}}%
}{%
    \end{tabular}%
}
\NewDocumentCommand{\educationitem}{o}{%
  %\begingroup
  \pgfkeys{/education,notes,selectedcourses,thesisheader,#1}%%%%%%%%
  \expanded{%
    %\endgroup
    \unexpanded\expandafter{\educationdates}\noexpand&\noexpand\textbf{\unexpanded\expandafter{\educationtitle}}\noexpand\\
    \noexpand&\unexpanded\expandafter{\educationprogramme}\noexpand\\
    \noexpand&\unexpanded\expandafter{\educationnotes}\noexpand\\
    \noexpand&\noexpand\textit{\unexpanded\expandafter{\educationinstitute}}\noexpand\\
    \unexpanded\expandafter{\educationthesisheader}\noexpand&\unexpanded\expandafter{\educationthesistitle}\noexpand\\
    \unexpanded{Selected courses:}\noexpand&\unexpanded\expandafter{\educationselectedcourses}\noexpand\\
  }%
}


\begin{document}
\begin{education}
\educationitem[
    title={Master of Science},
    programme={Some programme},
    notes={Some notes about the degree},
    institute={Some institute},
    dates={2014--2017},
    thesistitle={The title of the thesis},
    selectedcourses={Some list of courses that might get pretty long, filling out the usable space of the row.},
]
\\
\educationitem[
    title={Bachelor of Science},
    programme={Some other programme},
    notes={Some more notes about the degree},
    institute={Some other institute},
    dates={2011--2014},
    thesisheader={Honours thesis:},
    thesistitle={The title of the thesis},
    selectedcourses={Some list of courses that might get pretty long, filling out the usable space of the row.},
]
\end{education}
\end{document}

enter image description here

1
  • 1
    With something like \newcommand*\pgfkeysunexpanded[1]{\expandafter\expandafter\expandafter\unexpanded\expandafter\expandafter\expandafter{\csname pgfk@/education/#1\endcsname}} we can also use value-keys, e.g. \pgfkeysunexpanded{dates} & \noexpand\textbf{\pgfkeysunexpanded{title}}\noexpand\\. Commented Aug 26, 2022 at 15:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .