6

In the early days of this site we had a question Automatically capitalize first letters of words in titles? with one solution. Since expl3 has matured well since then I have worked out a partial solution using expl3. It fails on a couple of exceptions and I am looking at ways to improve it.

\documentclass{article}
\usepackage{expl3,xparse}
\usepackage{hyperref}
\ExplSyntaxOn
\clist_gset:Nn \title_words_not_capitalized_en 
    { a, an, the, at, by, for, in, of, on, to, up, and, as, but, it, or, nor, do, for, this, be,  
    A, An, The, At, By, For, In, Of, On, To, Up, And, As, But, It, Or, Nor, Do, For, This, Be }

\cs_new:Npn \ucfirst_aux:w #1#2 \q_stop { \uppercase { #1 } #2 }

\cs_new:Npn \ucfirst #1 {
     \exp_after:wN \ucfirst_aux:w #1 \q_stop
}

\cs_new:Npn \lowerfirst #1 {
       \tex_lowercase:D {#1}
 }

\DeclareDocumentCommand\UppercaseTitle {s +m }
    {
      \IfBooleanTF { #1 } { {\bfseries {#2} } }
        {
            \seq_set_split:Nnn \g_tmpa_seq {~} {#2}
            \seq_use:Nn   \g_tmpa_seq {~}\\

            \seq_pop_left:NN \g_tmpa_seq \l_tmpa_tl  

            {\bfseries\ucfirst \l_tmpa_tl \space} 
            \seq_map_inline:Nn \g_tmpa_seq 
               {
                  \clist_if_in:NnTF \title_words_not_capitalized_en { ##1 }
                  { {\bfseries \lowerfirst {##1}~}} { {\bfseries \ucfirst{##1}~ } }    

               }            

       }    
    } 

\ExplSyntaxOff
\begin{document}
\tableofcontents
\parindent0pt

\section{\UppercaseTitle {The l3seq package Sequence and stacks}}

 \UppercaseTitle {Top ten things to do in Paris}\\
 \UppercaseTitle {How to use {\LaTeX} sequence lists effectively}\\
 \UppercaseTitle {Senate Votes to Confirm Elena Kagan For U.S. Supreme Court}\\
 \UppercaseTitle {what would be a ``correct'' capitalization for the title of this question?}\\
 \UppercaseTitle* {How about {$e=mc^2$}? }\\

 \end{document}

Exceptions are:

  • fails to capitalize words in quotes.
  • Has issues with maths
  • Prefer not to bracket macros (not user friendly)
  • Would fail on two or more sentence titles where the first word of the second sentence starts with an exception word.
  • Exceptions such as "p-th root" of a "p-adic".

Comments on LaTeX3 coding are welcome. Still a rookie!

6
  • 2
    Have you seen the expandable case changer (\tl_upper_case:n, etc.)? It's intended for the lowest-level part of case changing and deals happily with for example math mode (though I need to do more revision). Will Robertson suggested on I think LaTeX-L an implementation of 'sentence case' build on top of it.
    – Joseph Wright
    May 20 '15 at 21:23
  • @JosephWright Aargh! I missed it. The 'sentence case' is not easy as is a hard NLP problem to distinguish betwen abbreviations and end of sentences. Some of the headings in interfaces3 need fixing e.g The 'l3tl package Token lists'. May 20 '15 at 21:30
  • Yes, I know the problem is hard (and language/editorial style dependent), which is why we've not tackled it! Even the case changer is still experimental: for example, at present I'm working a bit on the nature of what is expanded when.
    – Joseph Wright
    May 20 '15 at 21:33
  • @JosephWright I saw the code in the l3candidates. Interesting, approach. The unicode stuff is not easy. I see expl3 is detecting lua and have some code for it. Is the intention to move some functions over, if Lua is available? Most language relate stuff are better served with a json data structure. May 20 '15 at 22:04
  • Most of the Unicode part is OK, it's getting it right with math mode and LaTeX2e oddities that is hard. The only Lua that expl3 uses is where it emulates some pdfTeX primitives. The idea is that we should get the same behaviour on pdfTeX/XeTeX/LuaTeX as far as possible, so doing a Lua path just makes life more complex. Of course, there is the whole business about things that cannot be done in the same way, but at the moment we've not really tackled many of them. Note in this context that XeTeX and LuaTeX should work in the same way, but we accept pdfTeX has to be more restricted!
    – Joseph Wright
    May 21 '15 at 7:22
6

Joseph mentioned an implementation I cooked up at one point in time; here it is. Warning: this is pasted from an old email and may no longer function as intended :) I can't test (easily) while I'm at work…

Feel free to incorporate any ideas into your code, or to extend it, or to ignore it, all as you see fit! (One notable feature of this code is that it is expandable — or should be.)

\documentclass[a4paper]{article}
\usepackage{expl3}
\usepackage{etoolbox}

\begin{document}

\ExplSyntaxOn

% Make some commands engine-robust
\robustify\textbf

% Set up to exclude items from case changing
\tl_put_right:Nn \l_tl_case_change_exclude_tl { \NoChangeCase }
\cs_new:Npn \NoChangeCase #1 {#1}

% First split the input string into sentence chunks, where `sentence' means anything after . ? ! : etc.

\cs_new:Npn \titlecase #1
  {
    \tl_if_blank:nF {#1}
      { \titlecase_split_period:w #1 . \q_recursion_tail \q_recursion_stop }
  }

%% This is the basic idea of splitting:
%
%\cs_new:Npn \titlecase_split_period:w #1 . #2
%  {
%    \titlecase_split_colon:w #1 : \q_recursion_tail \q_recursion_stop
%    \quark_if_recursion_tail_stop:n {#2}
%    . \c_space_tl
%    \titlecase_split_period:w #2
%  }

% Use an obfuscated macro to avoid lengthy definitions:
\cs_new:Nn \titlecase_new_split_fn:NNNN
  {
    \cs_set:Npn #2 ##1 #1 ##2
      {
        #4 ##1 #3 \q_recursion_tail \q_recursion_stop
        \quark_if_recursion_tail_stop:n {##2}
        #1 \c_space_tl
        #2 ##2
      }

  }

% The reason to use an obfuscated macro is to pass in properly sanitised `characters':
% (Could possibly be done more easily with some clever expansion.)
\exp_after:wN \titlecase_new_split_fn:NNNN
\exp_after:wN .
\exp_after:wN \titlecase_split_period:w \token_to_str:N :
  \titlecase_split_colon:w
\exp_after:wN \titlecase_new_split_fn:NNNN
  \token_to_str:N : \titlecase_split_colon:w ! \titlecase_split_exclam:w
\titlecase_new_split_fn:NNNN ! \titlecase_split_exclam:w  ? \titlecase_split_question:w
\titlecase_new_split_fn:NNNN ? \titlecase_split_question:w  {~} \titlecase_first:w


%% These are the internals that do the processing of each word.

% The first and last words needs special attention;
% they should always be capitalised unless there's a special exception.
\cs_new:Npn \titlecase_firstlast:nn #1 #2
  {
    \titlecase_exact_exceptions:nn {#1} {#2}
    \tl_mixed_case:n {#1} #2
  }

\cs_new:Npn \titlecase_middle:nn #1 #2
  {
    \titlecase_exact_exceptions:nn {#1} {#2} % catch exact matches which will never change
    \titlecase_lc_exceptions:nn {#1} {#2} % catch words that should be lowercase
    \tl_mixed_case:n {#1} #2 % otherwise capitalise normally
  }


%% After all the splitting, we now enter the word-by-word titlecasing.

\cs_new:Npn \titlecase_first:w #1 ~ #2
  {
    \tl_if_blank:nT {#1} { \quark_if_recursion_tail_stop:n {#2} }
    \titlecase_catch_punct:Nn \titlecase_firstlast:nn {#1}
    \use_none_delimit_by_q_nil:w
    \q_nil
    \quark_if_recursion_tail_stop:n {#2}
    \c_space_tl
    \titlecase:w #2
  }

% The regular loop:
\cs_new:Npn \titlecase:w #1 ~ #2
  {
    \quark_if_recursion_tail_stop_do:nn {#2}
      {
        \titlecase_catch_punct:Nn \titlecase_firstlast:nn {#1}
        \use_none_delimit_by_q_nil:w
        \q_nil
      }

    \titlecase_catch_punct:Nn \titlecase_middle:nn {#1}
    \use_none_delimit_by_q_nil:w
    \q_nil
    \c_space_tl
    \titlecase:w #2
  }

\cs_new:Npn \__tl_last:n #1
  {
    \__tl_last:w #1 \q_recursion_tail \q_recursion_tail \q_recursion_stop
  }
\cs_new:Npn \__tl_last:w #1 #2
  {
    \quark_if_recursion_tail_stop_do:nn {#2} {#1}
    \__tl_last:w #2
  }

\cs_new:Npn \titlecase_catch_punct:Nn #1 #2
  {
    \str_if_eq_x:nnT { \__tl_last:n {#2} } {,}
      {
        \exp_args:No #1 { \titlecase_catch_comma:w #2 }{,}
        \use_none_delimit_by_q_nil:w
      }
    \str_if_eq_x:nnT { \__tl_last:n {#2} } {;}
      {
        \exp_args:No #1 { \titlecase_catch_semicolon:w #2 }{;}
        \use_none_delimit_by_q_nil:w
      }
    #1 {#2}
  }

\typeout{ LAST:~\__tl_last:n {foo bar .}}
\cs_new:Npn \titlecase_catch_comma:w #1, {#1}
\cs_new:Npn \titlecase_catch_semicolon:w #1; {#1}

% The exception catching functions contain series of functions like the following to match and if so jump to the end after their specific processing.

\cs_new:Npn \titlecase_exception:nnn #1 #2 #3
  {
    \str_if_eq_x:nnT { \tl_lower_case:n {#1} } {#3}
      { \tl_lower_case:n {#1} #2 \use_none_delimit_by_q_nil:w }
  }

\cs_new:Npn \titlecase_exact_exception:nnn #1 #2 #3
  {
    \str_if_eq:nnT {#1} {#3}
      { #1 #2 \use_none_delimit_by_q_nil:w }
  }

% These are the functions to define the exception catching functions.

\cs_new:Npn \titlecase_set_small_exceptions:n #1
  {
    \seq_set_from_clist:Nn \l_titlecase_seq {#1}
    \cs_set:Npx \titlecase_lc_exceptions:nn ##1 ##2
      {
        \seq_map_function:NN \l_titlecase_seq \titlecase_small_except:n
      }
  }

\cs_new:Npn \titlecase_set_exact_exceptions:n #1
  {
    \seq_set_from_clist:Nn \l_titlecase_seq {#1}
    \cs_set:Npx \titlecase_exact_exceptions:nn ##1 ##2
      {
        \seq_map_function:NN \l_titlecase_seq \titlecase_exact_except:n
      }
  }

\cs_new:Npn \titlecase_small_except:n #1
  {
    \exp_not:N \titlecase_exception:nnn {\exp_not:N ## 1} {\exp_not:N ## 2} {#1}
  }

\cs_new:Npn \titlecase_exact_except:n #1
  {
    \exp_not:N \titlecase_exact_exception:nnn {\exp_not:N ## 1} {\exp_not:N ## 2} {#1}
  }

\titlecase_set_small_exceptions:n {a,an,and,as,at,but,by,en,for,if,in,of,on,or,the,to,v,via,vs}
\titlecase_set_exact_exceptions:n {TV,iTunes,iPhone}% an example only

\ExplSyntaxOff

\def\TCTEST#1{{\titlecase{#1}}\par\bigskip}
%\def\TCTEST#1{\typeout{\titlecase{#1}}}

\TCTEST{Modern words like \NoChangeCase{\textbf{iPhone}} and iTunes, \NoChangeCase{eyeTV}, and even just TV are annoying}
\TCTEST{This is a sentence. But this is capitalised.}
\TCTEST{iTunes, here to stay like iPhone}
\TCTEST{Hello friends of the world, whom I am also friends of}
\TCTEST{}
\TCTEST{ }
\TCTEST{and}
\TCTEST{and and}
\TCTEST{and and and}
\TCTEST{This Was All Capitalised But Some Words Shouldn't Be If They're Small}
\TCTEST{Colon test: An example of an exception}
\TCTEST{A Question? An answer?}
\TCTEST{A Question! An answer!}
\TCTEST{iTunes and TV}
\TCTEST{David v Goliath and Goliath vs Mittelbach}
\TCTEST{Hard to say whether `quotes are too hard'}

\ExplSyntaxOn
\typeout{ [This~ is~ plain~ title_case~ with~ quotes]~ \tl_mixed_case:n {`Hello'} }

[This~ is~ plain~ title_case~ with~ quotes]~ \tl_mixed_case:n {`Hello'}

\par\bigskip [This~ is~ plain~ title_case~ with~ braces]~ \tl_mixed_case:n {{`Hello'}}

\par\bigskip [This~ is~ plain~ title_case~ with~ bold]~ \tl_mixed_case:n {\textbf{`Hello'}}

\par\bigskip [This~ is~ plain~ title_case~ with~ bold~ and~ braces]~ \tl_mixed_case:n {{\textbf{`Hello'}}}

\ExplSyntaxOff

\end{document}
5
  • I've edited here to update based on some changes we made to \tl_mixed_case:n. Key points: preventing case changing now needs a 'marker' command (I've called it \NoChangeCase) and only engine-robust commands are not expanded, so \textbf needs to be made robust.
    – Joseph Wright
    May 21 '15 at 7:20
  • 2
    Will thanks for popping in to answer it. We miss you:) May 21 '15 at 7:39
  • \TCTEST{Anno Domini (AD or A.D?)}. Although normally one would have an \AD command or \abbrv command. Thanks for the code, this is great. May 21 '15 at 7:51
  • I miss me too :). Thanks for the additions, Joseph — I definitely like new \NoChangeCase functionality vs the old approach of ignoring braced material. May 21 '15 at 7:52
  • 1
    @WillRobertson Thank David C.: he was not keen on the previous approach :-) I'll probably update the code over the next few days to address a few more issues.
    – Joseph Wright
    May 21 '15 at 9:03

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.