How to preserve parskip in vsplit?

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

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

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.