[Tex/LaTex] How to make continuous (but breakable) vertical rule along left side of paragraph

page-breakingplain-texrules

I'm looking for a way in Plain TeX to make a continuous vertical rule along the left side of a paragraph (or arbitrary block of text), such that it observes normal page-break rules. I started with the following, which looks exactly how I want, except of course it has the undesirable side-effect of preventing page breaks for large runs of text, since it uses \vbox:

% Definition
\def\codeblock#1{%
  \hbox{%
    {\vrule width .6pt}%
    \hskip 1em%
    \vbox{\tt #1}%
  }%
}

% Example
\codeblock{%
\hbox{void main(int argc, char *argv[]) \char123}%
\hbox{{\ }{\ }printf("Hello, world!{\char92}n");}%
\hbox{\char125}%
}

As a workaround, I'm currently resorting to breaking things into separate lines and a series of kludged non-continous-but-overlapping vertical rule segments:

% Definition
\def\codeline#1{%
  \hbox{%
    \vrule width .6pt height .85em depth .46em%
    \hskip 1em%
    \tt #1%
  }%
  \vskip -.11em%
}

% Example
\codeline{void main(int argc, char *argv[]) \char123}
\codeline{{\ }{\ }printf("Hello, world!{\char92}n");}
\codeline{\char125}

In a perfect world, I'd like to simply pass an arbitrary block of text to a macro and have it apply normal pagination rules to the block of text, while at the same time making a vertical rule down the left. I don't have much experience with TeX but I suspect the answer is that it's not trivial?

While typing this question, a similar question appeared in the margin. I will definitely give that a try — and it looks really powerful and well thought out — but it also seems quite heavyweight for something that I would think ought to be reasonably straightforward. Also, not that I have anything against LaTeX, but I'd prefer to use a solution as close to Plain TeX as possible, so that I can have some hope of understanding it.

Best Answer

Try the file below. You asked for Plain TeX - so this must be run with pdftex (not pdflatex). This file (vert.tex) and some test files are hosted on GitHub at https://github.com/m4dc4p/vert.

%% Justin Bailey 2011
%% jgbailey@codeslower.com
%%
%% To use: surround paragraphs to place a rule
%% with \startrule and \endrule.
%%
%% E.g.:
%%
%% \startrule 
%% This paragraph will have a rule around it.
%% \endrule
%%
%% Multiple paragraphs can be spanned as well. Rules will break across
%% pages. Unfortunately, glue between paragraphs will not stretch
%% in that case, but usually it's not noticeable.
%%
%% You can change the offset of the rule by setting \ruleoffset. Rules
%% are always offset from the left margin.
%%
%% A simple \codeblock environment is included too.
%%
%% To use it, surround the code with \codeblock{
%%
%% }
%%
%% Code must appear in the group. The group must immediately follow \codeblock.
%% Code is set ragged right, obeying newlines, using typewriter font.

\newdimen\ruleoffset \ruleoffset=-10pt %% Horizontal offset for the
                                  %% rule. This is the parameter that
                                  %% should be set by the user.

\newdimen\roffset %% For calculating offset from left margin for rule.
\newbox\savedbox \newif\ifoutputran \newbox\rulebox
\newdimen\splitheight \newdimen\interparskip
\newtoks\codetoks

%% The basic idea is to use TeX's output routines
%% to determine if our ruled box is too high. If so
%% we split the box, set a rule, and put the rest on 
%% the next page. \endrule does most of the work.
\def\startrule{%%
%% Clear our saved box.
  \setbox0=\hbox{\box\savedbox}\par%%
%% Save \ruleoffset for later use.
  \xdef\setroffset{\roffset=\the\leftskip \advance\roffset by \the\ruleoffset}%%
%% Save \prevdepth so we can use it to calculate interparagraph
%% glue.
  \xdef\theprevdepth{\the\prevdepth}%%
%% Save material proceeding \startrule unless we're at
%% the top of the page.
  \output={\accum}\ifnum\pagegoal<\maxdimen\relax%%
  \vfil\break\outputranfalse%%
%% Capture everything up to \endrule in a \vbox.
  \fi\setbox\rulebox=\vbox\bgroup}
\def\endrule{\egroup%%
%% Test if \rulebox + \savedbox will overflow
%% the page.
  \output{\test}%%
  \setinterparskip\setroffset%%
  \setbox4=\makerule\copy\rulebox/%%
%% \vfuzz and \vbadness set to avoid overfull/underfull warnings
%% while testing.
  \edef\thevfuzz{\the\vfuzz}\edef\thevbadness{\the\vbadness}%%
  \vfuzz=10in\box4\penalty0%%
%% Restore default output routine.
  \output={\savedout}\vfuzz=\thevfuzz%%
  \ifoutputran\outputranfalse%%
%% The material overflowed the page, so we split off what 
%% can fit (\splitheight) and put that on the page.
    \vfuzz=10in\vbadness=10000%%
    \setbox0=\vsplit\rulebox to \splitheight\vbadness=\thevbadness%%
    \makerule\box0/\break\vfuzz=\thevfuzz%%
%% Put remaining material in a ruled box unless
%% nothing was left (\ifvoid)
    \ifvoid\rulebox%%
      \else\startrule\unvbox\rulebox\endrule%%
    \fi%%
  \fi}

%% Ensures spaces at the beginning of the line are always
%% preserved. TABs will not be. Thanks to TeX for the Impatient
%% (eplain) for \alwayspace.
{\gdef\alwaysspace{\hglue\fontdimen2\the\font \relax}%%
  \obeyspaces\gdef {\alwaysspace}}

%% Define new lines so that in \codeblock they don't start a new
%% paragraph - they just insert a line break.
{\catcode`\^^M=\active \gdef^^M{\null\hfil\break} \global\let\ret=^^M}

\newtoks\codetoks
%% \codeblock must be followed by a group or it has no effect.
%% When followed by a group, the text found will be set on
%% individual lines as they appear in the group (i.e. new lines 
%% are obeyed). The entire group will be have a rule next to it.
%% The group is also set in typewriter font, with ragged-right
%% margins.
%%
%% Note that text in the group is NOT set verbatim.
\def\codeblock{\codetoks={}%%
%% Removes final lineskip if one was there.
  \gdef\endo{\unpenalty\endrule\prevdepth=0pt\relax}%%
%% Removes initial newline, if one was there. Otherwise, reinsert the
%% token captured.
  \gdef\ignorenewline{\ifx\next\ret%%
    \else\next%%                      
    \fi}%%                          
  \gdef\do{\ifx\next\bgroup%% 
    \codetoks={\startrule\noindent\bgroup%%
      \ttraggedright%%
      \parindent=0pt%%
      \tt%%
      \aftergroup\endo%%
      \ignorespaces%%
      \catcode`\^^M=\active\obeyspaces%%
      \afterassignment\ignorenewline\let\next= }%%
    \fi\the\codetoks}%%
  \ignorespaces\afterassignment\do\let\next= }

\edef\savedout{\the\output}
%% Accumulate vertical material into a box.
\def\accum{\global\setbox\savedbox=\vbox{\unvbox\savedbox\unvbox255\unskip}}
%% An output routine that tells us it ran and
%% throws away the page built.
\def\test{\global\outputrantrue%%
  \global\splitheight=\vsize%%
  \global\advance\splitheight by -\ht\savedbox%%
  \global\advance\splitheight by -\dp\savedbox%%
  \setbox0=\vbox{\box255}}

\def\makerule#1/{\vbox{\unvcopy\savedbox%%
    \vskip\interparskip\par\penalty0%%
    \hbox{\hskip \roffset \vrule \hss \hskip -\roffset \strut#1\strut}}}%%
\def\setinterparskip{\setbox2=\vtop{X\par}%%
  \setbox4=\vtop{\unvcopy\rulebox}%% 
%% Set \prevdepth so inter-paragraph glue is calculated based
%% on the paragraph that really preceded \startrule, not our
%% fake paragraph.
  \setbox0=\vbox{\copy2\par\prevdepth=\theprevdepth\copy4}%%
  \interparskip=\ht0 \advance\interparskip by -\ht4 \advance\interparskip by -\ht2}%%
Related Question