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}%%
As the question is focussed on learning how these things may be done just using the primitives (I'd agree with David's answer that loading the color
package in plain is an easier route).
What I'll do here is implement much the same approach as is taken by the color
package, with appropriate tests for classical TeX (dvips
or dvipdfm(x)
drivers), pdfTeX/LuaTeX in PDF mode and XeTeX. As there is a bit going on, I'll intersperse the code with comments.
First, set up a conditional to test for direct PDF output
\newif\ifpdfmode
\begingroup\expandafter\expandafter\expandafter\endgroup
\expandafter\ifx\csname pdfoutput\endcsname\relax
\else
\ifnum\pdfoutput>0 %
\expandafter\expandafter\expandafter\pdfmodetrue
\fi
\fi
Define the current colour as black, using an \edef
so that once defined there are no conditionals about (the same idea applies to the rest of the code)
\edef\currentcolor{%
\ifpdfmode
0 g 0 G%
\else
gray 0%
\fi
}
Set up a pre-defined colour: I've just done one (red) as a demo:
\edef\colorred{%
\ifpdfmode
1 0 0 rg 1 0 0 RG%
\else
rgb 1 0 0%
\fi
}
For direct PDF output, there may be a colour stack available (since pdfTeX 1.40.0). A one-off test will tell us this: if there is no stack, just restore the colour manually. See the pdfTeX manual for the details here.
\begingroup\expandafter\expandafter\expandafter\endgroup
\expandafter\ifx\csname pdfcolorstack\endcsname\relax
\ifpdfmode
\def\pdfcolorstackpush{\pdfliteral{\currentcolor}}%
\let\pdfcolorstackpop\pdfcolorstackpush
\fi
\else
\chardef\colorstack=0 %
\def\pdfcolorstackpush{%
\pdfcolorstack\colorstackcnt push{\currentcolor}%
}%
\def\pdfcolorstackpop{%
\pdfcolorstack\colorstackcnt pop\relax%
}%
\fi
The main macro to set colour starts with a test: if the argument is the name of a pre-defined colour use that, otherwise assume a hard-coded engine-specific value. (A more sophisticated approach is to convert the colour to the correct format: as that is not asked for in the question I'll leave as an exercise). Once the colour is set up, insert the appropriate special (noting the \edef
will again mean at point of use there are no conditionals):
\edef\color#1{%
\begingroup\noexpand\expandafter\noexpand\expandafter\noexpand\expandafter\endgroup
\noexpand\expandafter\noexpand\ifx\noexpand\csname color#1\noexpand\endcsname\relax
\def\noexpand\currentcolor{#1}%
\noexpand\else
\noexpand\expandafter\let\noexpand\expandafter\noexpand\currentcolor
\noexpand\csname color#1\noexpand\endcsname
\noexpand\fi
\ifpdfmode
\noexpand\pdfcolorstackpush
\else
\special{color push \noexpand\currentcolor}%
\fi
\aftergroup\noexpand\resetcolor
}
Following the color
approach, a reset macro is also created using the appropriate special.
\edef\resetcolor{%
\ifpdfmode
\noexpand\pdfcolorstackpop
\else
\special{color pop}%
\fi
}
The demo itself. The implementation above relies on a level of grouping inside boxes, which in LaTeX would be done by the \savebox
'wrapper' for \hbox
(and so on). In plain that's not the case, so a colour-safe box needs a group. This could of course be put inside an appropriate set of wrapper macros:
\newbox\mybox
\setbox\mybox=\hbox{\begingroup\color{red}Red text\endgroup}
Surrounding text \box\mybox \space and more of it.
\bye
(Note: I've constructed the above in much the same way as I'd do using DocStrip for creating separate files. As DocStrip is not involved, this costs of some conditional/edef
work.)
Best Answer
The settings there use a finite amount of stretch so the text isn't too ragged (like
ragged2e
package\RaggedRight
). But left on its own that would mean that any line stretching would be shared between the margins and the inter word space, so the spaceskip settings freeze those at fixed values.The latex version uses infinite glue stretch in the margins so this naturally forces all the interword glue to its natural length as all the stretch is taken up at the margin.