概括

概括

请考虑以下示例:

\tracingonline=1
\tracingoutput=1
\gdef\makefootline{} \gdef\makeheadline{}
\insert\topins{\vbox to\vsize{\hrule height \vsize width 300pt}}
\end

日志文件包含以下内容:

Completed box being shipped out [1]
\vbox(643.20255+0.0)x300.0
.\vbox(643.20255+0.0)x300.0
..\vbox(643.20255+0.0)x300.0
...\rule(643.20255+0.0)x300.0


Completed box being shipped out [2]
\vbox(643.20255+0.0)x469.75499
.\vbox(643.20255+0.0)x469.75499, glue set 633.20255fill
..\glue(\topskip) 10.0
..\hbox(0.0+0.0)x469.75499
..\glue 0.0 plus 1.0fill

\topskip第 2 页上的元素来自哪里( 、\line{}和)尚不清楚\vfill。此行为未在第 15 章中记录TeXbook

答案1

我将尝试使用 TeXbook 中的引文来解释这一点。

概括

当主垂直列表在标记\end被消化时不为空时,TeX插入相当于\line{}\vfill\penalty-'10000000000进入主垂直列表,运行页面生成器并准备再次读取\end(TeXbook 第 264 和 283 页)。在您的示例中,这\line{}会将第一个框带到主垂直列表,因为插入项不是框项。因此,TeX 将\topskip粘连添加到此框,即10pt此处。此\topskip粘连是有效的断点,因为它紧接着插入项;相关成本为零。下一个合法断点是\vfill跟在 之后的与\hbox前面提到的 相对应的粘连\line{}。与此粘连相关的成本\vfill是无限的,因为相关的惩罚为零,并且如果在项目处发生中断,页面将会溢出\vfill(即,b = ∞;这是因为插入项加上\topskip粘连项加上空项\hbox总共高出 10 点,无法放在页面上)。因此 TeX 在目前看到的最佳断点处(即粘连项)分页\topskip。因此,页面 1 仅包含来自插入项的材料(例程\output将此材料包装在两个\vboxes 内)。页面 2 包含一个新的\topskip粘连项、空项\hbox\vfill粘连项。

分析

如上所述,如果在标记\end被消化的点处主垂直列表不为空,TeX 会将等同于的内容插入\line{}\vfill\penalty-'10000000000到主垂直列表中,运行页面生成器并准备\end再次读取。在您的示例中,这会将 10 个\topskip粘合点和一个空点带到\hbox to \hsize当前页面上(以及其他内容)。在此特定情况下,\topskip粘合项是有效的断点1,TeX 会立即计算相关成本(请记住,它正在运行页面生成器作为特殊\end处理的一部分);它发现此成本为零,因为插入正好很高\vsize。因此,开始新页面的条件尚未满足(TeXbook 第 112 页,§ 2)。

因此,TeX 继续隐式添加项目。清空之后\hbox\vfill粘合:它将这两个项目从“最近贡献”移动到“当前页面”列表。\hbox不是合法的断点,但\vfill项目是合法的,因为它紧接着一个不可丢弃的项目(\hbox)。TeX 计算成本C与第二个合法断点相关联,并发现C= ∞,因为当前页面上有10pt多余的材料,直到断点为止(当插入项移动到“当前页面”时,页面目标减少了\vsize并变为,并且页面总数随着粘连从变为,在空的之后保持不变)。根据 TeXbook 第 112 页给出的规则,这导致 TeX 决定是时候在最容易记住的断点处拆分新页面了,该断点是粘连处的断点(相关成本:0)。0pt0pt10pt\topskip\hbox\topskip

因此,来自 的这个粘贴项\topskip、空项\hbox\vfill粘贴项都放回到“最近贡献”列表的顶部,后面是\penalty-'10000000000自插入以来一直在那里等待的项\end(参见第 125 页的长段落)。由于默认情况下\holdinginserts0,插入项随后从“当前页面”中删除,其内容被附加到 ,\box\topins没有行间粘连,剩余在“当前页面”上的项(这里没有!)放在 中\box255,最后\output调用例程并发送第 1 页。

然后 TeX 开始第 2 页。它做的第一件事就是运行页面生成器,因为\output例程刚刚结束(参见第 122 页,§2,项目 (e),在脚注 3 中重现)。因此,TeX 尝试将项目从“最近贡献”移动到“当前页面”。\topskip放回“最近贡献”顶部的粘合项此时被丢弃(“当前页面”上还没有框)并立即被新的粘合项2\topskip替换,因为跟在它后面的项目将是当前页面上的第一个框。TeX 将此框移动到当前页面,然后是粘合项和。粘合是一个合法的断点,但它不满足第 112 页给出的完成页面的条件(相关值为和)。另一方面,后面的惩罚项确实满足这些条件(p ≤ −10000)。\hbox\vfill\penalty-'10000000000\vfillp=0c=100000

因此,除断点之外的所有剩余项目(即\penalty-'10000000000)都进入了第 2 页。这个惩罚项目被改为 a\penalty10000并放回“最近贡献”的顶部(TeXbook 第 125 页),但在页面生成器运行时立即被丢弃(同样是由于第 122 页第 2 节第 (e) 项,如脚注 3 中所述)。因此,主垂直列表最终为空,并且由于对第 2 页\output执行的例程是TeX第二次看到时。根据 TeXbook 第 283 页,这将终止作业。\shipout\deadcycles0\end

通过实验验证分析

TeXbook 第 264 页:

当 TeX 看到\end命令时,只有当主垂直列表已完全输出并且 时,它才会终止作业\deadcycles=0。否则它会插入相当于

\line{} \vfill \penalty-'10000000000

进入主垂直列表,准备\end再次读取令牌。

笔记:

  • 这对应于代码由 大卫·卡莱尔 发布

  • 在第 283 页上,Knuth 添加了页面构建器在将上述盒子/粘合/惩罚组合添加到主垂直列表后执行的精度。

我们可以证明,在您的示例中,TeX\end在主垂直列表清空之前看到了标记。为了做到这一点,请在您的示例中插入\showlistsbefore \end,您将看到:

### vertical mode entered at line 0
### current page:
\insert253, natural size 643.20255; split(10.0,16383.99998); float cost 0
.\vbox(643.20255+0.0)x300.0
..\rule(643.20255+0.0)x300.0
total height 0.0
 goal height 0.0
\insert253 adds 643.20255
prevdepth ignored

./insert.tex:5: OK.
l.5 \showlists

相反,如果你在\null\break\showlists之前插入\end,你会看到:

### vertical mode entered at line 0
prevdepth 0.0

./insert.tex:5: OK.
l.5 \null\break\showlists

是一个空的垂直列表(在页面 1 和未满的页面 2 发送出去后显示)。所以,回到你的例子:当 TeX 看到时\end,它还没有考虑到页面已经满了(使用一个框后跟负的字距或跳过确实可以“倒回”),所以它还没有调用该\output例程。我们可以插入一些诊断工具来确认我在这里添加的精度:

\tracingonline=1
\tracingoutput=1
\tracingmacros=2
\tracingpages=1
\gdef\makefootline{} \gdef\makeheadline{}
\insert\topins{\vbox to\vsize{\hrule height \vsize width 300pt}}
\showlists
\end

打印结果为:

%% goal height=643.20255, max depth=4.0

### vertical mode entered at line 0
### current page:
\insert253, natural size 643.20255; split(10.0,16383.99998); float cost 0
.\vbox(643.20255+0.0)x300.0
..\rule(643.20255+0.0)x300.0
total height 0.0
 goal height 0.0
\insert253 adds 643.20255
prevdepth ignored

./insert.tex:7: OK.
l.7 \showlists

% t=0.0 g=0.0 b=0 p=0 c=0#
% t=10.0 g=0.0 b=* p=0 c=*
\output->{\plainoutput }

\plainoutput ->\shipout \vbox {\makeheadline \pagebody \makefootline }\advancepageno \ifnum \outputpenalty >-\@MM \else \dosupereject \fi 

(...)

Completed box being shipped out [1]
\vbox(643.20255+0.0)x300.0
.\vbox(643.20255+0.0)x300.0
..\vbox(643.20255+0.0)x300.0
...\rule(643.20255+0.0)x300.0


\advancepageno ->\ifnum \pageno <\z@ \global \advance \pageno \m@ne \else \global \advance \pageno \@ne \fi 
%% goal height=643.20255, max depth=4.0
% t=10.0 g=643.20255 b=10000 p=0 c=100000#
% t=10.0 plus 1.0fill g=643.20255 b=0 p=-1073741824 c=-1073741824#
\output->{\plainoutput }

\plainoutput ->\shipout \vbox {\makeheadline \pagebody \makefootline }\advancepageno \ifnum \outputpenalty >-\@MM \else \dosupereject \fi 

(...)

Completed box being shipped out [2]
\vbox(643.20255+0.0)x469.75499
.\vbox(643.20255+0.0)x469.75499, glue set 633.20255fill
..\glue(\topskip) 10.0
..\hbox(0.0+0.0)x469.75499
..\glue 0.0 plus 1.0fill

%% goal height=643.20255, max depth=4.0当以下情况时打印该行

第一个框或插入进入当前页面列表

(TeXbook 第 113 页)。这就是你的了\insert,好的。紧接着,\showlistsindeed 在当前页面上显示了此插入内容:

### vertical mode entered at line 0
### current page:
\insert253, natural size 643.20255; split(10.0,16383.99998); float cost 0
.\vbox(643.20255+0.0)x300.0

(...)

这是因为,根据 TeXbook 第 281 页:

\insert在以垂直模式附加内容后,TeX 还会运行页面构建器(见下文) 。

所以,页面构建器已运行在 TeX 读取之前\showlists。 已经\insert从“最近贡献”移至“当前页面”。但 TeX 尚未考虑页面必须完成,它还不会调用该\output例程!确实,请记住日志中接下来的内容:

% t=0.0 g=0.0 b=0 p=0 c=0#
% t=10.0 g=0.0 b=* p=0 c=*
\output->{\plainoutput }

第一行g=0清楚地表明\insert已经将 放在了当前页面上,否则页面目标仍然是643.20255points。现在,回想一下 TeXbook 第 114 页中所说的内容:

TeX 在每页的第一个框之前插入特殊粘连。此特殊粘连等于\topskip,只是自然空间减少了第一个框的高度,或者被设置为零而不是负值。

% t=0.0 g=0.0 b=0 p=0 c=0#行对应于\topskip在相当于 的框之前自动插入的粘连\line{}。根据下面脚注 1 的引用,它是一个合法的断点,并且在页面生成器作为 的特殊处理的一部分运行时打印\end(否则,根据 TeXbook 第 282 页,它将在\hbox命令被 TeX 消化后打印)。但是,该行末尾的p=0和的连词意味着 TeX 认为c=0没有理由开辟新的一页在迄今为止看到的最佳断点处(标记为#:此处为第一个合法断点)。事实上,回想一下第 112 页的第 2 段:

如果结果C小于或等于当前页面上迄今为止看到的最小成本,TeX 会记住当前断点为迄今为止最佳。如果C = ∞ 或者如果 ≤ −10000,TeX 掌握主动权并在最能记住的断点处中断页面。

(最后一个“if”很可能是“当且仅当”)。\topskip断点是一个粘合项,其相关值为 0;由于插入恰好\vsize很高,因此在此\topskip项目处中断将导致页面不良度等于零(b=0),因此成本C与第一个断点相关的确实是c=0根据b +  + 来自第 111 页的表达式(\insertpenalties之后为零\insert)。

因此,在\topskip由于特殊\end处理而将粘连放在“当前页面”上并计算与此潜在断点相关的成本后,TeX 仍在等待更多材料,然后才决定必须进行分页。因此,它将插入“最近贡献”中的下一个项目取出\end,并将它们逐一移动到“当前页面”。不是\hbox断点,它只是被移动了;\vfill也被移动了——没有理由丢弃它——并且合法断点,因为它前面有一个不可丢弃的项目(\hbox)。页面构建器计算其相关成本:

% t=10.0 g=0.0 b=* p=0 c=*

此行是为胶水项目打印的,相关值为0,因此 p < 10000。由于存在\topskip粘合项(10 个点)且为空\hbox,因此在\vfill粘合项处分页会导致页面过满,因此b=*计算出的成本为c=*。这决定 TeX 在最容易记住的断点( )处分页\topskip,并最终调用\output第 1 页的例程:

\output->{\plainoutput }

\plainoutput ->\shipout \vbox {\makeheadline \pagebody \makefootline }\advancepageno \ifnum \outputpenalty >-\@MM \else \dosupereject \fi 

(...)

因此,您将获得第二页,其中包含(新的)\topskip粘合项、一个空的\hbox等效项\line{}和另一个来自的粘合项\vfill(全部包裹在两个\vboxes 中:一个来自\plainoutput和一个来自\pagebody)。

您想\shipout在插入之后但在\end添加之前对第 1 页进行一些操作\line{} \vfill \penalty-'10000000000,对吗?第一个想法:在 之后附加一个\insert强制分页的惩罚,例如使用\break(这相当于\penalty -10000)。唉,这行不通,因为:

  1. 在垂直模式下看到的A\penalty会导致 TeX 运行页面构建器 (TeXbook p.280),并因此尝试将内容从“最近贡献”移动到“当前页面”。

  2. 这句话出自 TeXbook 第 112 页:

    每当 TeX 将某个项目从“最近贡献”的顶部移动到“当前页面”的底部时,如果当前页面不包含任何框,它会丢弃一个可丢弃的项目(粘合、字距或惩罚)。

    这里就是这种情况(“当前页面”上的插入项不是框项)。

事实上,如果我们尝试:

\tracingonline=1
\gdef\makefootline{} \gdef\makeheadline{}
\insert\topins{\vbox to\vsize{\hrule height \vsize width 300pt}}
\break
\showlists
\end

我们可以看到:

### vertical mode entered at line 0
### current page:
\insert253, natural size 643.20255; split(10.0,16383.99998); float cost 0
.\vbox(643.20255+0.0)x300.0
..\rule(643.20255+0.0)x300.0
total height 0.0
 goal height 0.0
\insert253 adds 643.20255
prevdepth ignored

./insert.tex:5: OK.
l.5 \showlists

和前面一样,“当前页面”在执行的位置有插入\showlists,但是惩罚已被丢弃,这可以从前面的引文中猜出。比较一下:

\tracingonline=1
\tracingpages=1
\gdef\makefootline{} \gdef\makeheadline{}
\insert\topins{\vbox to\vsize{\hrule height \vsize width 300pt}}
\topskip=0pt\null\penalty100
\showlists
\end

这使:

%% goal height=643.20255, max depth=4.0
% t=0.0 g=0.0 b=0 p=0 c=0#
% t=0.0 g=0.0 b=0 p=100 c=100

### vertical mode entered at line 0
### current page:
\insert253, natural size 643.20255; split(10.0,16383.99998); float cost 0
.\vbox(643.20255+0.0)x300.0
..\rule(643.20255+0.0)x300.0
\glue(\topskip) 0.0
\hbox(0.0+0.0)x0.0
\penalty 100
total height 0.0
 goal height 0.0
\insert253 adds 643.20255
prevdepth 0.0

由于\null(一个空的\hbox),惩罚这次没有被丢弃;它进入当前页面并被标记为合法断点(t=0.0 g=0.0 b=0 p=100 c=100),尽管不是目前见过的最好的断点(那个断点有尾随的#)。多亏了\topskip=0pt,我们在第 1 页上得到了所有这些(如果为正数\topskip,我们将有 c = ∞ 作为\penalty100断点,因此再次在粘合处分页\topskip,然后是第二页)。使用\topskip=0pt,插入、\topskip粘合和\null框都适合第一页。因此,为了只获得一页输出(您可能想要的),我们需要做的就是在框后触发分页符\null。我们可以使用惩罚来做到这一点,这次不会被丢弃,因为已经有一个盒子在“当前页面列表”中。现在开始:

\tracingonline=1
\tracingpages=1
\gdef\makefootline{} \gdef\makeheadline{}
\insert\topins{\vbox to\vsize{\hrule height \vsize width 300pt}}
\topskip=0pt\null\break
\end

输出结果如下:

%% goal height=643.20255, max depth=4.0
% t=0.0 g=0.0 b=0 p=0 c=0#
% t=0.0 g=0.0 b=0 p=-10000 c=-10000#
[1] )
Output written on insert.pdf (1 page, 968 bytes).
Transcript written on insert.log.

TeX Output finished at Mon Jul  8 10:11:58

一页输出,\penalty -10000从我们的处断开\break


脚注

  1. 我们正好处于 TeXbook 第 114 页描述的案例中:

    如果插入发生在第一个框之前,则\topskip该框之前的粘连被视为有效的断点;这是完成的页面可能不包含框的唯一情况。

  2. \output=\expandafter{\the\output\global\topskip=6pt}这可以通过之前插入的代码\end并观察打印的 TeX 消息来验证\tracingonline=1\tracingpages=1\tracingoutput=1\relax(来自 Igor 的想法)。

  3. TeXbook 第 122 页第 2 段给出了页面构建器的运行条件:

    每当 TeX 尝试将某些内容从最近贡献列表移动到当前页面时,它都可能调用输出例程,因为它可能会发现带有C = ∞ 则。以下是可能发生这种情况的时间列表:(a)在段落的开头或结尾,前提是此段落正在添加到主垂直列表中。(b)在此类段落中显示的等式的开头或结尾。(c)\halign在垂直模式下完成之后。(d)在向主垂直列表添加一个框或惩罚或插入之后。(e)在例程\output结束后。

答案2

这是 tex-the-program 的最终结局,以清除插入内容:

tex.web 有

@ We don't want to leave |main_control| immediately when a |stop| command
is sensed, because it may be necessary to invoke an \.{\\output} routine
several times before things really grind to a halt. (The output routine
might even say `\.{\\gdef\\end\{...\}}', to prolong the life of the job.)
Therefore |its_all_over| is |true| only when the current page
and contribution list are empty, and when the last output was not a
``dead cycle.''

@<Declare act...@>=
function its_all_over:boolean; {do this when \.{\\end} or \.{\\dump} occurs}
label exit;
begin if privileged then
  begin if (page_head=page_tail)and(head=tail)and(dead_cycles=0) then
    begin its_all_over:=true; return;
    end;
  back_input; {we will try to end again after ejecting residual material}
  tail_append(new_null_box);
  width(tail):=hsize;
  tail_append(new_glue(fill_glue));
  tail_append(new_penalty(-@'10000000000));@/
  build_page; {append \.{\\hbox to \\hsize\{\}\\vfill\\penalty-'10000000000}}
                         ^^^^^^^^^^^^^^^^^^^^^^
  end;
its_all_over:=false;
exit:end;

相关内容