18

Is it possible to push or pop a length like parindent? I want to change it temporarily but reset it soon after.

I have a solution but looking for something a little nicer. I'll post it as an answer.

lockstep
  • 250,273
Uiy
  • 6,132
  • 4
    What's the intention of your question? For example you can change the length inside an environment or group and the effect is local. – Marco Daniel Apr 07 '12 at 10:14
  • 1
    @MarcoDaniel No, not necessarily, although that might be a another method. A stack. \push(\parindent) and \pop(\parindent). So I can change the parindent at will and restore it at will. – Uiy Apr 07 '12 at 11:09

5 Answers5

26

Just say, in your preamble,

\newlength{\savedparindent}

and, when you want to change the \parindent, say

\setlength{\savedparindent}{\parindent}
\setlength{\parindent}{<dimen>}

Then you can restore the previous \parindent by

\setlength{\parindent}{\savedparindent}

This is even unnecessary if you use the environment structure:

\newenvironment{otherparindent}[1]
  {\par\setlength{\parindent}{#1}}
  {\par}

so that the change to \parindent is confined to the environment. Of course it's not necessary to use such an environment: you can put the \setlength{\parindent}{<dimen>} code in the definition of any environment.

A stack based solution is easily obtained with LaTeX3:

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\seq_new:N \g_uiy_parindent_seq

\NewDocumentCommand{\pushparindent}{m}
 {
  \seq_gpush:Nx \g_uiy_parindent_seq {\the\parindent}
  \dim_gset:Nn \parindent {#1}
 }
\NewDocumentCommand{\popparindent}{}
 {
  \seq_gpop:NN \g_uiy_parindent_seq \l_tmpa_tl
  \dim_gset:Nn \parindent {\l_tmpa_tl}
 }
\ExplSyntaxOff

\begin{document}

\showthe\parindent

\pushparindent{100pt}

\showthe\parindent

\pushparindent{50pt}

\showthe\parindent

\popparindent

\showthe\parindent

\popparindent

\showthe\parindent

\end{document}

This will show in succession 15.0pt, 100.0pt, 50.0pt, 100.0pt, 15.0pt; the command \pushparindent stores the current value and sets \parindent to the value specified in the argument. With \popparindent you get the last stored value.

Here's a version with "standard LaTeX":

\newtoks\parindentstack
\newcommand{\pushparindent}[1]{%
  \edef\temp{\noexpand\listelement{\the\parindent}\the\parindentstack}%
  \global\parindentstack=\expandafter{\temp}%
  \global\parindent=#1\relax
}
\newcommand{\popparindent}{%
  \if\relax\detokenize\expandafter{\the\parindentstack}\relax
    \errmessage{Empty stack}%
  \else
    \expandafter\getlistelement\the\parindentstack\getlistelement
  \fi
}
\def\getlistelement\listelement#1#2\getlistelement{%
  \global\parindentstack{#2}\global\parindent=#1\relax}
egreg
  • 1,121,712
  • Well, yes, but this is basically my answer. The problem is such a method doesn't work with nesting. i.e., you can easily overwrite savedparindent and everything could get out of whack. Hence the need for a good stack based solution. One way would be to create \savedparindentk and use them... but of course automatically. – Uiy Apr 07 '12 at 10:51
  • 2
    @Uiy Saying "doesn't work with nesting" is not very helpful: without seeing at least a sketch of what you want to do it's difficult to say more than "use the built in group structure to confine the changes". – egreg Apr 07 '12 at 10:55
  • Nesting is when you do something inside of something else. It is a common problem. Your code basically like: – Uiy Apr 07 '12 at 10:58
  • x = 1; tmp1 = x; { x = 3; tmp2 = x; { x = 5; tmp3 = x; x = tmp3; } x = tmp2; } x = tmp1; print x; except with yours all the temp's are the same. – Uiy Apr 07 '12 at 11:03
  • so the final print should print 1 but with your code will print the last value stored in tmp, which is 5. The code makes more sense as a stack. x = 1; push(x) { push(x) { push(x) pop(x) } pop(x) } pop(x) print x; in this case x will be 1 like it should. – Uiy Apr 07 '12 at 11:04
  • The only way to deal with nesting is to use stacks(that is a mathematical fact(although you can linearize things or do other stuff but ultimately it's using an abstract stack structure). Basically as you move up a level in nesting you push the previous values on the stack. When you move down a level you pop them. This keeps all the higher levels from trampling on the lower level's values. (of course this assumes you want to nest stuff) – Uiy Apr 07 '12 at 11:08
  • There are two ways to deal with saved values in TeX. Either you always set them locally, and use TeX's grouping, or you manage it yourself and do global assignments. You can do that with a stack or for example a csname based approach using a global number. That's what we do for LaTeX3 'inline mappings', for example. – Joseph Wright Apr 07 '12 at 15:26
11

You can implement a stack without using LaTeX3 as well, by using a token list and some macros. That would work as follows:

\documentclass{article}
\newtoks\paridstack
\paridstack={\empty}
\def\push#1#2{%
   \begingroup\toks0={{#1}}%
   \edef\act{\endgroup\global#2={\the\toks0 \the#2}}\act
}% push #1 onto #2
\def\pop#1{%
   \begingroup
   \edef\act{\endgroup\noexpand\splitList\the#1(tail)#1}\act
}% pop from #1
\def\splitList#1#2(tail)#3{%
   \ifx#1\empty Can't pop an empty stack.\else#1\global#3={#2}\fi
}
\begin{document}
  \noindent
  \push{200pt}{\paridstack}%
  \pop{\paridstack}\\
  \push{200pt}{\paridstack}%
  \push{300pt}{\paridstack}%
  \push{400pt}{\paridstack}%
  \pop{\paridstack}\\
  \pop{\paridstack}\\
  \pop{\paridstack}\\
  \pop{\paridstack}
\end{document}

It will print 200pt 400pt 300pt 200pt Can't pop from an empty stack.. You can of course replace the error message with some error handling code, or some default value to be returned. This is all based on an example in chapter 14 of TeX by Topic. If you want to set \parindent directly instead of just printing the value, simply change #1 in the \else of \splitList to \parindent=#1.

David Carlisle
  • 757,742
Roelof Spijker
  • 17,663
  • 5
  • 55
  • 63
  • This would be rather difficult to manage if groups are involved and probably global assignments should be used. – egreg Apr 07 '12 at 12:56
  • @egreg: good point. I added two \globals. I think this should be sufficient to handle groups. Of course, the assignment to \parindent would have to be made global as well for it to persist after the end of a group. – Roelof Spijker Apr 07 '12 at 13:05
  • 1
    It's unclear, from what the OP says, whether the \parindent assignment should be global or not. – egreg Apr 07 '12 at 13:11
  • @egreg can you tell me to me to a resource that explains what a latex group is? There seems to be something special about it than my basic understanding... – Uiy Apr 08 '12 at 10:49
  • @RoelofSpijker Basically what I want assuming it all works out ;) – Uiy Apr 08 '12 at 10:49
  • @Uiy texdoc texbytopic – egreg Apr 08 '12 at 12:48
  • People do it, but making a global assignment to \toks0 can sometimes be risky. I have therefore edited your post to localize the assignment. Also, \push{\empty}{\paridstack} \pop{\paridstack} fails. You may want to change the marker \empty. – Ahmed Musa Apr 08 '12 at 17:06
  • @AhmedMusa: Out of curiosity, do you happen to have an example of when an assignment to \toks0 causes problems? As far at the \empty goes, I don't see that as a real problem, considering what the stack is meant for. – Roelof Spijker Apr 08 '12 at 17:52
  • @RoelofSpijker: For your first question, look at the code of graphicx package. If you don't know how the package uses \toks0 you could easily get into trouble when you try to hack or take from that code. Regarding the second issue, since your stack looks like a general-purpose bin, all obvious sources of difficulty should be avoided. Luckily it is easy to do so; see my second answer. – Ahmed Musa Apr 08 '12 at 18:39
  • @AhmedMusa: Thanks for the example. Better safe than sorry, same goes for the \empty marker of course. – Roelof Spijker Apr 08 '12 at 19:24
3

You can create a new macro length and add \parindent to it which creates a copy of \parindent

\newlength{\mylen}
\setlength\mylen{\parindent}
\setlength\parindent{0pt}

When you are ready to restore \parindent you can use

\setlength\parindent{\mylen}

Note this method does not allow nesting.

There is also a stack based solution but maybe less efficient.

With lualatex you can simply use a stack in lua with a tex macro wrapper. Should be pretty self-explanatory.

Uiy
  • 6,132
  • 2
    Why \addtolength and not \setlength? – egreg Apr 07 '12 at 10:12
  • @egreg: Either one will work. I used set at first but something didn't work. setlength would be more semantical. (they are equivalent since x = a + 0 is the same as x = a. (assuming \mylen is 0) – Uiy Apr 07 '12 at 10:44
  • Of course \addtolength will not work if you use this twice in the same group. – egreg Apr 07 '12 at 10:48
  • @egreg Well, I didn't realize that \newlength will not do anything if the length is already defined. Hence \setlength is correct. Usually latex complains about creating something new when it already exists. I thought the same would be true here but I guess not. Seems to be an inconsistency in design. Should have a \renewlength and similar stuff to other things. – Uiy Apr 07 '12 at 11:08
  • It's a common error to allocate again and again registers. You have to say \newlength{\mylen} only once. – egreg Apr 07 '12 at 11:10
  • @egreg: I'll I'm saying is that \newlength semantics doesn't jive with \newcommand. If I use \newcommand twice on the same macro name it complains... but doesn't do this with \newlength. – Uiy Apr 07 '12 at 11:14
  • 2
    Nowhere in the LaTeX manuals it is suggested that \newlength behaves like \newcommand: indeed it doesn't; it just allocates a register to store a length. – egreg Apr 07 '12 at 11:18
  • @egreg EXACTLY!! That is the point! – Uiy Apr 07 '12 at 12:08
  • 4
    saying "Note this method does not allow nesting." is rather misleading: it does allow nesting if you use the standard Tex mechanism for nesting using groups, either {} groups or a latex environment. You only need the explicit stack implementations if you need global definitions with a nested structure that doesn't use Tex groups. Needing this isn't that uncommon (for example in tables you often want a logical group corresponding to a row, but you can not use a Tex group there) But your question gives no indication that that is needed in your case. – David Carlisle Apr 07 '12 at 16:15
  • @DavidCarlisle But { } doens't keep the state of all the macros outside the group. if I do \parindent=3pt { \parindent=10pt } What is the parindent value here? 3pt or 10pt?] – Uiy Apr 07 '12 at 21:55
  • 1
    3pt the value is restored at the group end, as you would easily see by executimg \parindent=3pt { \parindent=10pt } \showthe\parindent all local assignments are restored at the end of a group. You can do \global\parindent=5pt then it is not restored – David Carlisle Apr 08 '12 at 08:07
2

Here is another solution for pushing and popping dimensions. It allows many measures to be pushed or popped at once.

\documentclass{article}
\usepackage{catoptions}
\makeatletter
\robust@def*\pushlengths#1{%
  \cptdocommalist{#1}{%
    \cptexpanded{%
      \gdef\noexpandcsn{pushlength@\cptgobblescape##1}{%
        \noexpand\pushstop{##1\the##1\relax}%
        \ifcsndefTF{pushlength@\cptgobblescape##1}{%
          \ifcsnnullTF{pushlength@\cptgobblescape##1}{%
            \noexpand\pushstop{}%
          }{%
            \expandcsnonce{pushlength@\cptgobblescape##1}%
          }%
        }{%
          \noexpand\pushstop{}%
        }%
      }%
    }%
  }%
}
\robust@def*\poplengths#1{%
  \cptdocommalist{#1}{%
    \cptexpanded{%
      \cpt@poplength\expandcsnonce{pushlength@\cptgobblescape##1}%
    }%
    \pushstopp##1%
  }%
}
\robust@def*\cpt@poplength\pushstop#1#2\pushstopp#3{%
  \ifblankTF{#1}{%
    \cpt@err{Can't pop empty stack
      \noexpandcsn{pushlength@\cptgobblescape#3}}\@ehd
  }{%
    #1\csn@gdef{pushlength@\cptgobblescape#3}{#2}%
  }%
}
\makeatother

% Tests:
\begin{document}
\parindent20pt
\pushlengths{\parindent,\textwidth,\textheight,\rightmargin,\leftmargin}
\parindent0pt
\pushlengths\parindent
\poplengths\parindent
\poplengths{\parindent,\textwidth,\textheight,\rightmargin,\leftmargin}
% \poplengths\parindent % -> Can't pop empty stack.
\end{document}
Ahmed Musa
  • 11,742
1

This solution is just a response to Roelof. I hope it is appropriate here. I replace \empty by \roelofstackbegin in his solution. This is safer than using \empty. Roelof's stack looks like an all-purpose stack. So any possible source of failure should be avoided.

\documentclass{article}
\makeatletter
\newtoks\roelofstack
\roelofstack={\roelofstackbegin}
\def\roelofstackbegin{\empty}
% push #1 onto #2:
\def\push#1#2{%
  \begingroup
  \toks0={{#1}}%
  \edef\act{\endgroup\global#2={\the\toks0 \the#2}}%
  \act
}
% pop from #1:
\def\pop#1{%
  \begingroup
  \edef\act{\endgroup\noexpand\SplitOff\the#1(tail)#1}%
  \act
}
\def\SplitOff#1#2(tail)#3{%
  \ifx#1\roelofstackbegin
    \errhelp{Attempting to pop empty stack #3.}%
    \errmessage{You can't pop an empty stack.}%
  \else
    #1\global#3={#2}%
  \fi
}
\makeatother

% Tests:
\begin{document}
\noindent
\push{\empty}{\roelofstack}
\pop{\roelofstack}

\push{200pt}{\roelofstack}
\pop{\roelofstack}\par
\push{200pt}{\roelofstack}
\push{300pt}{\roelofstack}
\push{400pt}{\roelofstack}
\pop{\roelofstack}\par
\pop{\roelofstack}\par
\pop{\roelofstack}\par
% \pop{\roelofstack} % Popping empty stack.
\end{document}
Ahmed Musa
  • 11,742