请考虑以下示例:
\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
将此材料包装在两个\vbox
es 内)。页面 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)。0pt
0pt
10pt
\topskip
\hbox
\topskip
因此,来自 的这个粘贴项\topskip
、空项\hbox
和\vfill
粘贴项都放回到“最近贡献”列表的顶部,后面是\penalty-'10000000000
自插入以来一直在那里等待的项\end
(参见第 125 页的长段落)。由于默认情况下\holdinginserts
是0
,插入项随后从“当前页面”中删除,其内容被附加到 ,\box\topins
没有行间粘连,剩余在“当前页面”上的项(这里没有!)放在 中\box255
,最后\output
调用例程并发送第 1 页。
然后 TeX 开始第 2 页。它做的第一件事就是运行页面生成器,因为\output
例程刚刚结束(参见第 122 页,§2,项目 (e),在脚注 3 中重现)。因此,TeX 尝试将项目从“最近贡献”移动到“当前页面”。\topskip
放回“最近贡献”顶部的粘合项此时被丢弃(“当前页面”上还没有框)并立即被新的粘合项2\topskip
替换,因为跟在它后面的项目将是当前页面上的第一个框。TeX 将此框移动到当前页面,然后是粘合项和。粘合是一个合法的断点,但它不满足第 112 页给出的完成页面的条件(相关值为和)。另一方面,后面的惩罚项确实满足这些条件(p ≤ −10000)。\hbox
\vfill
\penalty-'10000000000
\vfill
p=0
c=100000
因此,除断点之外的所有剩余项目(即\penalty-'10000000000
)都进入了第 2 页。这个惩罚项目被改为 a\penalty10000
并放回“最近贡献”的顶部(TeXbook 第 125 页),但在页面生成器运行时立即被丢弃(同样是由于第 122 页第 2 节第 (e) 项,如脚注 3 中所述)。因此,主垂直列表最终为空,并且由于对第 2 页\output
执行的例程是TeX第二次看到时。根据 TeXbook 第 283 页,这将终止作业。\shipout
\deadcycles
0
\end
通过实验验证分析
TeXbook 第 264 页:
当 TeX 看到
\end
命令时,只有当主垂直列表已完全输出并且 时,它才会终止作业\deadcycles=0
。否则它会插入相当于\line{} \vfill \penalty-'10000000000
进入主垂直列表,准备
\end
再次读取令牌。
笔记:
这对应于代码由 大卫·卡莱尔 发布。
在第 283 页上,Knuth 添加了页面构建器在将上述盒子/粘合/惩罚组合添加到主垂直列表后执行的精度。
我们可以证明,在您的示例中,TeX\end
在主垂直列表清空之前看到了标记。为了做到这一点,请在您的示例中插入\showlists
before \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
,好的。紧接着,\showlists
indeed 在当前页面上显示了此插入内容:
### 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.20255
points。现在,回想一下 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
(全部包裹在两个\vbox
es 中:一个来自\plainoutput
和一个来自\pagebody
)。
您想\shipout
在插入之后但在\end
添加之前对第 1 页进行一些操作\line{} \vfill \penalty-'10000000000
,对吗?第一个想法:在 之后附加一个\insert
强制分页的惩罚,例如使用\break
(这相当于\penalty -10000
)。唉,这行不通,因为:
在垂直模式下看到的A
\penalty
会导致 TeX 运行页面构建器 (TeXbook p.280),并因此尝试将内容从“最近贡献”移动到“当前页面”。这句话出自 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
。
脚注
我们正好处于 TeXbook 第 114 页描述的案例中:
如果插入发生在第一个框之前,则
\topskip
该框之前的粘连被视为有效的断点;这是完成的页面可能不包含框的唯一情况。\output=\expandafter{\the\output\global\topskip=6pt}
这可以通过之前插入的代码\end
并观察打印的 TeX 消息来验证\tracingonline=1\tracingpages=1\tracingoutput=1\relax
(来自 Igor 的想法)。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;