12

If I box something with non-zero \parskip and split it using \vsplit, the resulting split box will have the exact requested height. This is ususally more than I want, so I \unvbox the split box and re-\vbox it immediately to remove superfluous whitespace on the bottom. This approach usually works, unless you have non-zero \parskip and your split point is in between paragraphs. Consider the MWE:

\parskip=7pt

\setbox0=\vbox{\input knuth\par}

\splittopskip=0pt
\setbox1=\vsplit 0 to 2\baselineskip
\setbox1=\vbox{\unvbox1}
\ifdim\dp1<\dp\strutbox
  \dp1=\dp\strutbox
\fi

\box1
\box0

\bye

As you can see in the output, the \parskip is missing between the first two paragraphs, because that is where the split was taken.

enter image description here

Is there a way to detect whether the last item of the split box would be \parskip? LuaTeX solutions are also welcome.


EDIT: I realised that \vsplit immediately discards \parskip if the break lies at that point. This is easy to illustrate with this simple example:

\parskip=7pt

\setbox0=\vbox{\input knuth\par}

\setbox1=\vsplit 0 to 2\baselineskip
\vbox{\unvbox1 \unvbox0}

\bye

enter image description here

  • It may be possible to give up \vsplit entirely and do it in LuaTeX... perhaps that may be interesting as then one could manually remove the last glue or whatever, after checking that it isn't the parskip glue. (Just guesswork, haven't tried anything and don't even know much about \vsplit, and this sort of solution may not work for you...) – ShreevatsaR Dec 10 '18 at 3:52
  • 1
    @ShreevatsaR Good idea, unfortunately I don't know what is the boilerplate required to write my own box splitter. However, inspecting the node lists of the split boxes with LuaTeX I found that \parskip is already discarded after \vsplit and not only at \unvbox as I had suspected. That complicates the matter a bit. – Henri Menke Dec 10 '18 at 5:20
  • 1
    What about using \splitdiscards and examining its content? – Frank Mittelbach Dec 11 '18 at 19:26
  • I think you should explain in greater detail what you are trying to achieve, and in particular why you wan to split your text. Moreover, do you want to reinsert only the \parskip glue or every kind of glue found between paragraphs? For instance, if two paragraphs are separated by a \bigskip, do you want to reinsert also that glue? A final hint: @FrankMittelbach’s suggestion of using \splitdiscards (an e-TeX extension) is probably the key to the simplest, most general, and most elegant solution. – GuM Dec 14 '18 at 21:33
8
+100

If you do want the box to have the exact height, LuaTeX supports

\setbox1=\vsplit 0 upto 2\baselineskip

as a built-in alternative to

\setbox1=\vsplit 0 to 2\baselineskip
\setbox1=\vbox{\unvbox1}

As you already seen, this does not really fix the problem because the parskip is already discarded by the \vsplit. But LuaTeX also allows saving and inspecting the discarded elements from a split from Lua, so we can reinsert the parskip:

\directlua{
  function my_split()
    % Scan the parameters
    local result_box = token.scan_int()
    token.scan_keyword'='
    local original_box = token.scan_int()
    token.scan_keyword'upto'
    local height = token.scan_dimen()

    % Temporarly set tex.savingvdiscards
    local savediscards = tex.savingvdiscards
    tex.savingvdiscards = 1

    % Do the actual split
    local b = tex.splitbox(original_box, height, 'additional')
    tex.savingvdiscards = savediscards

    % Analyze tex.lists.split_discards_head to find the parskip
    local current = tex.lists.split_discards_head
    if current then current.prev = nil end
    while current do
      if current.id == 12 and current.subtype == 3 then
        local this = current
        tex.lists.split_discards_head, current = node.remove(tex.lists.split_discards_head, current)
        b.head = node.insert_after(b.head, nil, this)
        b.height = b.height + this.width
      else
        current = current.next
      end
    end
    % Save the result 
    tex.box[result_box] = b
  end
}
\protected\def\setparsplit{\relax\directlua{my_split()}}

This \setparsplit can be used as

\directlua{
  function my_split()
    % Scan the parameters
    local result_box = token.scan_int()
    token.scan_keyword'='
    local original_box = token.scan_int()
    token.scan_keyword'upto'
    local height = token.scan_dimen()

    % Temporarly set tex.savingvdiscards
    local savediscards = tex.savingvdiscards
    tex.savingvdiscards = 1

    % Do the actual split
    local b = tex.splitbox(original_box, height, 'additional')
    tex.savingvdiscards = savediscards

    % Analyze tex.lists.split_discards_head to find the parskip
    local current = tex.lists.split_discards_head
    if current then current.prev = nil end
    while current do
      if current.id == 12 and current.subtype == 3 then
        local this = current
        tex.lists.split_discards_head, current = node.remove(tex.lists.split_discards_head, current)
        b.head = node.insert_after(b.head, nil, this)
        b.height = b.height + this.width
      else
        current = current.next
      end
    end
    % Save the result 
    tex.box[result_box] = b
  end
}
\protected\def\setparsplit{\relax\directlua{my_split()}}
\parskip=7pt

\setbox0=\vbox{\input knuth\par}

\splittopskip=0pt
\setparsplit1=0 upto 2\baselineskip

\ifdim\dp1<\dp\strutbox
  \dp1=\dp\strutbox
\fi

\box1
\box0

\bye
  • Note to others: ID 12 is a glue node, subtype 3 is parskip. – Henri Menke Dec 11 '18 at 20:20
7

First of all, your example includes inexact dimension measurement. You set \splittopskip to 0pt, so the result of box1 includes two lines with height of first line plus one baselineskip. But you say \vsplit to2\baselineskip, so the rest to two baselineskips creates an empty space under these two lines inside the underfull box1. If you use \setbox1=\vbox{\unvbox1} then this empty space is removed.

Moreover, the next box0 begins with the line of its variable height, so you cannot keep the uniform baseline distance between second and third line in our example even if you corrected the depth of box1. So, the solution of these problems are

  • keep \splittopskip value 10pt
  • use trick \penalty0 as first element and \vsplit0 to0pt as first splitting operation: it adds topskip in the first line, so the first baselineskip is exactly 10pt from the top.
  • use more exactly calculated split point as 1 topskip plus (n-1) baselineskips.

The code, which does not solve the problem formulated by you but solves the problems mentioned by me follows:

\parskip=7pt

\setbox0=\vbox{\penalty0 \input knuth\par}

\splittopskip=10pt
\setbox1=\vsplit0 to0pt

\dimen0=\splittopskip \advance\dimen0 by 1\baselineskip
\setbox1=\vsplit 0 to \dimen0
\setbox1=\vbox{\unvbox1}
\ifdim\dp1<\dp\strutbox
  \dp1=\dp\strutbox
\fi

\box1
\box0

\bye

Now, we can solve your problem. You can redefine \par locally when data are read into box0 and you can check if the last penalty is 10001. If this is true then you can add \vskip\parskip:

\parskip=7pt

\def\redefpar{\def\par{\endgraf \penalty10001 \penalty0 }}

\setbox0=\vbox{\penalty0 \redefpar \input knuth\par}

\splittopskip=10pt
\setbox1=\vsplit0 to0pt

\dimen0=\splittopskip \advance\dimen0 by 1\baselineskip
\setbox1=\vsplit 0 to \dimen0
\setbox1=\vbox{\unvbox1
  \ifnum\lastpenalty=10001 \penalty0 \vskip\parskip\fi}
\ifdim\dp1<\dp\strutbox
  \dp1=\dp\strutbox
\fi

\box1
\box0

\bye
  • The \penalty0 trick to add \topskip to a box is neat. I'm always a bit reserved with respect to redefining \par, though. – Henri Menke Dec 10 '18 at 6:28
  • The \par is redefined only during \input data (\input knuth in our example). We can expected that data include only plain text with empty lines between paragraphs, nothing special, nothing what manipulates with \par by special way. – wipet Dec 10 '18 at 6:54
3

This is heavily based on @wipet answer. I did a bit of experimenting with it and here is my version for better reconstructing vertical spacing.

(pdftex by default has the e-TeX extensions which I use (unnecessarily) here once)

\parskip=7pt

\def\redefpar{\def\par{\endgraf \penalty10001 \penalty0 }}

\setbox0=\vbox{\penalty0 \redefpar \input knuth\par}

\splittopskip=10pt
\setbox1=\vsplit0 to0pt

\dimen0=\splittopskip \advance\dimen0 by 1\baselineskip
\setbox1=\vsplit 0 to \dimen0
\dimen0\dp1
\setbox1=\vbox{\unvbox1
  \ifnum\lastpenalty=10001 \penalty0 \vskip\parskip\fi
  }

\box1
\nointerlineskip
\kern-\dimen0
\vskip\dimexpr\baselineskip-\splittopskip\relax
\box0

\tracingoutput 1 %\showboxbreadth \maxdimen \showboxdepth \maxdimen

\bye

By the way, I am using this knuth.tex file

Thus, I came to the conclusion that the designer of a new system must not only
be the implementer and first large--scale user; the designer should also write
the first user manual.

The separation of any of these four components would have hurt \TeX\
significantly. If I had not participated fully in all these activities,
literally hundreds of improvements would never have been made, because I would
never have thought of them or perceived why they were important.

But a system cannot be successful if it is too strongly influenced by a single
person. Once the initial design is complete and fairly robust, the real test
begins as people with many different viewpoints undertake their own
experiments.

I copied the source from the PDF documentation of ConTeXT where it appears in small print. I don't know where the original source can be found.

  • 1
    (Responding to the "by the way" at the end.) In my TeX Live distribution on Linux, knuth.tex is found at /usr/share/texmf/tex/context/sample/common/knuth.tex (and though the path mentions context, \input knuth works also in plain TeX and LaTeX). The original source of that text is Knuth's paper The Errors of TeX (1989, p. 622, DOI 10.1002/spe.4380190702, a copy online here. (DEK numbers this paper P124, and it's also reprinted with slight changes in the collection Literate Programming.) – ShreevatsaR Dec 10 '18 at 18:38
  • To link to the actual file: here. (The directory has other samples; also listed here.) – ShreevatsaR Dec 10 '18 at 19:01
  • @ShreevatsaR very good, thanks. Yes I admit, I don't have context installed in my TeXLive... (I should at least have searched on CTAN for a file knuth.tex... but I don't have in my partial TL2018 install... I have it in my TL2016, did not think of checking that) – user4686 Dec 10 '18 at 20:15

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.