9

I'm quite new to LaTeX programming and currently looking into LaTeX3 and how to write a small ExplClass. This class should provide an allcaps command that I defined as follows:

\RequirePackage{expl3,xparse}
\ProvidesExplClass{foo}{2016/01/04}{0.1}{bar}
\LoadClass[11pt]{book}

\RequirePackage{fontspec,microtype}
\setmainfont{Minion Pro}

\NewDocumentCommand \allcaps {sO{200}m} {
  \tl_set:Nx \l_tmpa_tl { \tl_upper_case:n {#3} }
  \group_begin:
  \addfontfeature{Scale=0.9}
  \IfBooleanTF #1
    { \textls*[#2]{\l_tmpa_tl} }
    { \textls[#2]{\l_tmpa_tl} }
  \group_end:
}

Now I wanted to use that command in the class code itself and wondered if it might be better to write an auxiliary function and let the allcaps command also use this function (I think of DocumentCommand as a different layer and therefore not exactly intended for internal usage). If so - how do I cleanly handle the star variant and the optional argument within expl3?

  • 1
    1) You should declare a code level function \johannes_all_caps:nn which takes the letterspacing factor and the text as arguments and let the document level function \allcaps only handle the star. 2) I don't think that a class should load a font. You might want to make it a package instead. 3) I guess you also want to \LoadClassWithOptions. Otherwise the options in \documentclass[options]{foo} are just discarded. – Henri Menke Jan 4 '16 at 9:34
  • 3
    This is what I would do. It should comply with the expl3 guidelines. – Henri Menke Jan 4 '16 at 9:43
  • 1
    Nice! Don't you want to turn that into an answer? (2) and (3) are clear; I just wanted to make the snippet as small and self-contained as possible (with Minion being a bad choice in retrospect). So you would not go for a third (boolean) argument in the \johannes_all_caps function to handle the star? – Johannes Jan 4 '16 at 10:09
  • 3
    Aside: whilst we provide the idea of 'expl classes' that is probably not the best long-term idea (we really want to separate design and functionality more clearly). I'd place the code in a package which then isn't tied to the design of your document: for example, the scaling should be an additional argument or probably better a keyval set globally. – Joseph Wright Jan 4 '16 at 10:20
11

The philosophy of LaTeX3 is a strict separation of user interface and implementation. That is, why you should declare a code level function \johannes_all_caps:nn (implementation), which takes the letterspacing factor and the text as arguments and let the document level function \allcaps only handle the star (user interface).

As already mentioned, expl3 has the concept of document level functions (DLFs) and code level functions (CLFs). Roughly speaking, DLFs are intended to be used by the author typing the manuscript, whereas CLFs are meant to be used by the macro designer. The DLF calls the CLF(s) after preparing the user arguments, e.g. branching between CLFs.

In your case the DLF is \allcaps. Following the above guidelines, \allcaps has to call a CLF, e.g. \johannes_all_caps:nn. You use the star to branch between the starred and the unstarred version of \textls, thus I would define two CLFs \johannes_all_caps_kern:nn and \johannes_all_caps_nokern:nn. The resulting code would look like

\documentclass{article}
\usepackage{xparse,fontspec,microtype}
\setmainfont{Minion Pro}

\ExplSyntaxOn

\cs_new_protected:Npn \johannes_all_caps_kern:nn #1#2
 {
  \tl_set:Nx \l_tmpa_tl { \tl_upper_case:n { #2 } }
  \group_begin:
   \addfontfeature{Scale=0.9}
   \textls [ #1 ] { \l_tmpa_tl }
  \group_end:
 }

\cs_new_protected:Npn \johannes_all_caps_nokern:nn #1#2
 {
  \tl_set:Nx \l_tmpa_tl { \tl_upper_case:n { #2 } }
  \group_begin:
   \addfontfeature{Scale=0.9}
   \textls * [ #1 ] { \l_tmpa_tl }
  \group_end:
}

\NewDocumentCommand \allcaps { s O{200} m }
 {
  \IfBooleanTF { #1 }
   { \johannes_all_caps_kern:nn { #2 } { #3 } }
   { \johannes_all_caps_nokern:nn { #2 } { #3 } }
 }

\ExplSyntaxOff

\begin{document}
\end{document}

You might think: “Two CLFs with essentially the same code? This is highly redundant! Let us handle the star argument in the CLF.”

This is exactly what expl3 is after. A clean separation of user interface and macro implementation. User arguments should never be handled on the code level (that is why xparse defines all these really handy functions \If…TF).

Parsing optional arguments and branching is tedious and requires revising every function taking part in such a DLF-CLF chain. Therefore, expl3 encourages the usage of key=value interfaces. It makes setting optional arguments intuitive for the user, as you can give the arguments descriptive names and there is no rigid order of the arguments to obey. In the example below I added the scaling factor of \addfontfeature as an additional key to emphasise the extensibility (Thanks to Joseph Wright for this brilliant suggestion).

I this case, I would design \allcaps to accept an optional argument (the key=value list, by default empty) and the mandatory string. The initial values of the key=value list are chosen to resemble the same behaviour as above, i.e. unstarred \textls with a letterspacing factor of 200.

Inside \allcaps the keys are set within a group, because we do not want to have this specific choice of parameters leaking out into other parts of the document, especially not into other DLFs calling \johannes_all_caps:n.

\documentclass{article}
\usepackage{xparse,fontspec,microtype}
\setmainfont{Minion Pro}

\ExplSyntaxOn

\keys_define:nn { johannes_all_caps }
 {
  kern  .bool_set:N = \l__johannes_all_caps_kern_bool,
  kern  .initial:n  = { true },
  ls    .int_set:N  = \l__johannes_all_caps_ls_int,
  ls    .initial:n  = { 200 },
  scale .fp_set:N   = \l__johannes_all_caps_scale_fp,
  scale .initial:n  = { 0.9 }
 }

\cs_new_protected:Npn \johannes_all_caps:n #1
 {
  \tl_set:Nx \l_tmpa_tl { \tl_upper_case:n { #1 } }
  \addfontfeature{Scale=\fp_use:N \l__johannes_all_caps_scale_fp}
  \bool_if:NTF \l__johannes_all_caps_kern_bool
   { \textls * [ \int_use:N \l__johannes_all_caps_ls_int ] { \l_tmpa_tl } }
   { \textls [ \int_use:N \l__johannes_all_caps_ls_int ] { \l_tmpa_tl } }
 }

\NewDocumentCommand \allcaps { O{} m }
 {
  \group_begin:
   \keys_set:nn { johannes_all_caps } { #1 }
   \johannes_all_caps:n { #2 }
  \group_end:
 }

\ExplSyntaxOff

\begin{document}
abc \allcaps{abc} abc

abc \allcaps[kern=false]{abc} abc

abc \allcaps[ls=500]{abc} abc

abc \allcaps[ls=500,scale=2.0,kern=false]{abc} abc
\end{document}

For your \allcaps example a starred/unstarred version with one optional argument is perfectly reasonable (in my opinion). For commands with several optional arguments a key=value interface is definitely the preferred solution.

  • 2
    It would be helpful to include ` let the document level function \allcaps only handle the star` in the first paragraph. It is one of the most helpful things I've read about this framework. Your answer is also very good, but I would have voted the answer up just for this comment. (Since votes on comments don't 'count'.) – cfr Jan 5 '16 at 0:38
  • @cfr Thank you very much. I added an additional paragraph. – Henri Menke Jan 5 '16 at 11:02
  • Is there a question somewhere which asks why L3 wants this kind of separation even when it leads to redundancy? My code doesn't do this because I had no idea it was meant to until reading some comments here recently. Now I know it is 'supposed to' but, absent some reason why (beyond 'because The Powers That Be Say So'), I'm not especially motivated to implement it. Right now, it just looks like busy work. [I can't see why The Powers That Be would want to create busy work, but I don't have much insight into their motivation.] – cfr Jan 24 '16 at 23:42
  • @cfr I can certainly see the reasoning behind it (see tex.stackexchange.com/questions/285551/… for reference), but to discuss the guidelines in detail it is advantageous to ping egreg, JospehWright, and DavidCarlisle in chat. – Henri Menke Jan 25 '16 at 9:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.