语境
我正在尝试创建一个宏(此处称为\myCommand
),用于分隔两个段落并根据上下文添加不同的垂直空间。但我想这是一个普遍的问题,也可能发生在诸如这样的宏中\section{...}
。
通常情况下,我想在开始和结束处插入一个值为 的空格\myLength
(为了便于说明,我们将取= 30 pt)。 如果宏位于页面顶部,在分页符之后,则将被忽略,并且不会添加垂直空间,这正是我想要的。 但是,当宏位于分页符之后且前面有一个或多个顶部浮动时,就会出现问题,就像上一种情况一样。在这种情况下,我希望宏插入垂直线以保持一致性。\myLength
\myCommand
\vspace{\myLength}
\vspace*{\dimexpr\myLength-\textfloatsep}
例子
举个例子来说明一下:
下面,宏的行为正确:当它位于分页符后的页面顶部时,不会在宏的开头插入空格;而当它不在页面顶部时,\vspace{\myLength}
会正确地插入到宏的开头。
但是,当宏在分页符之后落在页面顶部并且前面有一个顶部浮动时,空间会完全不平衡。
为了说明该问题具有普遍性,而非特定于\myCommand
,以下是与 相同的问题\section{...}
:
这是一个不起作用的最小示例:
\documentclass{article}
\usepackage{lipsum, mwe}
\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate
\newcommand{\myCommand}{\par
\vspace{\myLength}
{\centering\LARGE * * *\par}
\vspace{\myLength}
}
\begin{document}
\lipsum[1]
\lipsum[1]
\lipsum[1]
\lipsum[2]
\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}
\myCommand
\lipsum[1]
\lipsum[2]
\myCommand
\lipsum[1]
\myCommand
\lipsum[1]
\end{document}
目标
为了克服上面提到的问题,我希望仅在以下情况下使用值为的垂直空间\vspace*{\dimexpr\myLength-\textfloatsep}
(以补偿顶部浮动插入的垂直空间): 在页面的开始处(分页符之后)并且存在顶部浮动。\textfloatsep
在所有其他情况下,使用都可以\vspace{\myLength}
完美地工作。
试验
我的(迄今为止未成功的)方法是使用两个条件在正确的位置插入正确的垂直空格:
- 第一个条件是测试当前页面上是否存在顶部浮动,
- 第二个条件是检查我们是否位于分页符之后的新页面的顶部。
如果两个条件都满足,那么我们确实位于页面的开头(分页符之后)并且存在顶部浮动;然后我们插入\vspace*{\dimexpr\myLength-\textfloatsep}
。在所有其他情况下,我们插入\vspace{\myLength}
。
感谢之前的一个问题,我已经能够找到一种检测页面中是否存在页面顶部浮动的方法:如何从文档主体判断页面是否有顶部浮动?。
然而,第二个条件对我来说确实带来了一个问题,我想尝试使用\pagetotal
,但它似乎不合适:如何访问真的\pagetotal 的值是多少?。
我的宏\myCommand
如下所示:
\makeatletter
\usepackage{refcount}
%From : https://tex.stackexchange.com/questions/712713/how-to-determine-from-the-document-body-whether-or-not-a-page-has-a-top-float
\def \@combinefloats {%
\ifx \@toplist\@empty%
\else%
\@cflt%
\immediate\write\@auxout{\string\global\string\@namedef{pageWithTopFloat-\thepage}{1}}%
\fi%
\ifx \@botlist\@empty \else \@cflb \fi%
}
\newcounter{ifTopFloatCnt}
\long\def\ifTopFloat#1#2{%
\global\advance\c@ifTopFloatCnt\@ne%
\label{\the\c@ifTopFloatCnt @ifTopFloat}\nopagebreak%
\ifcsname pageWithTopFloat-\getpagerefnumber{\the\c@ifTopFloatCnt @ifTopFloat}\endcsname%
#1%
\else%
#2%
\fi%
}
\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate
\newcommand{\mycommand}{\par%
\ifTopFloat{%
\ifdim\pagetotal=0pt% If there is a top float AND you are at the start of a new page
\vspace*{\dimexpr\myLength-\textfloatsep}
{\centering\LARGE * * *\par}
\else% If there is a top float BUT you are not at the start of a new page
\vspace{\myLength}
{\centering\LARGE * * *\par}
\fi
}{% If there is no top float
\vspace{\myLength}
{\centering\LARGE * * *\par}
}
\vspace{\myLength}
}
\makeatother
我的方法肯定不正确。
答案1
这是一个基于 LuaTeX 的解决方案。如果命令中的所有内容\removefromtop
位于页面顶部,则将被删除,否则将保留。我在这里使用规则以使效果更明显,但您可以将其替换为\vspace
(或任何其他垂直模式材料)作为最终文档。
\documentclass{article}
\usepackage{luacode}
\begin{luacode*}
local attr = luatexbase.new_attribute("removefromtop")
token.set_char("removefromtopattr", attr, "global")
local cancel_output = false
local last_cancelled = false
luatexbase.add_to_callback("pre_output_filter", function (head)
local outputpenalty = tex.outputpenalty
if outputpenalty <= -10002 and outputpenalty >= -10005 then
return true
end
if token.get_macro("@toplist") ~= "" then
return true
end
for n in node.traverse(head) do
if node.get_attribute(n, attr) == 1 then
head = node.remove(head, n)
cancel_output = true
elseif n.id ~= node.id("penalty") and
n.id ~= node.id("glue") and
n.id ~= node.id("kern") and
n.id ~= node.id("mark") and
n.id ~= node.id("whatsit") and
n.id ~= node.id("ins")
then
break
end
end
if last_cancelled then
last_cancelled = false
for n in node.traverse_id(node.id("glue"), head) do
if n.subtype == 10 then
n.width = -tex.baselineskip.width
end
end
end
if cancel_output then
return head
else
return true
end
end, "remove_from_top")
luatexbase.add_to_callback("buildpage_filter", function (info)
if status.output_active then
return
end
if not tex.output:match("%}%}") then
tex.runtoks(function()
tex.sprint[[\output\expandafter{\the\output}]]
end)
end
tex.triggerbuildpage()
if not (status.output_active and cancel_output) then
return
end
cancel_output = false
last_cancelled = true
local level = 0
repeat
local t = token.get_next()
if t.command == 1 then
level = level + 1
elseif t.command == 2 then
level = level - 1
end
until level == 0
local box = node.copy_list(tex.box[255])
node.write(box)
tex.setbox("global", 255, nil)
end, "remove_from_top")
\end{luacode*}
\newcommand{\removefromtop}[1]{%
\vbox attr \removefromtopattr=1 {%
#1%
}%
}
\usepackage{lipsum, mwe}
\newlength{\myLength}
\setlength{\myLength}{20pt}% exaggerated value to illustrate
\newcommand{\myCommand}{
\removefromtop{
\hrule height \myLength width \textwidth depth 0pt
}
\nobreak
\vbox{
{\centering\LARGE * * *\par}
\hrule height \myLength width \textwidth depth 0pt
}
}
\begin{document}
\lipsum[1]
\lipsum[1]
\lipsum[1]
\lipsum[2]
\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}
\myCommand
\lipsum[1]
\lipsum[2]
\myCommand
\lipsum[1]
\myCommand
\lipsum[1]
\lipsum[2]
\myCommand
\lipsum[1]
\myCommand
\lipsum[1-3]
\myCommand
\lipsum[1-9]
\myCommand
\lipsum[1]
\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}
\end{document}
代码相当复杂,我还没有对其进行过多测试,因此我预计更复杂的文档可能会出现一些错误。
怎么运行的
首先,我们定义一个自定义“属性”,用它来标记内容\removefromtop
。
接下来,我们进入buildpage_filter
,它在 TeX 将内容添加到“主垂直列表”之前被调用。这里的棘手之处在于我们调用tex.triggerbuildpage()
里面这个回调会触发 TeX 刷新其“最近的贡献”并尝试分页。
如果触发了分页符,则控件将跳转到pre_output_filter
。在这里,我们添加了另一个回调,它首先检查这是否是“真正的”分页符(而不是 LaTeX 用来刷新浮动的假分页符)以及当前页面上没有顶部浮动。如果是这种情况,我们将遍历页面的内容并删除任何用我们的自定义属性标记的初始内容。如果我们标记了任何内容,那么我们会将此信号返回给我们buildpage_filter
并返回新的页面内容。
一旦pre_output_filter
完成,控制权将在之后恢复tex.triggerbuildpage()
,但现在,我们在输出例程内。如果最近的pre_output_filter
成功删除了一些内容,并且我们在第一个输出例程内,那么我们将删除 TeX 输入堆栈上的所有标记。由于我们在输出例程内,这会清空标记\output
列表的当前扩展。然后我们清除\box255
(当前页面的内容),将其内容移回最近的贡献列表,然后返回。
为什么要这么做?
在触发输出例程之前,我们无法知道框是否\removefromtop
位于页面顶部。但是,当我们移除框时\removefromtop
,我们会缩短页面,因此如果我们只是挂起pre_output_filter
移除框,那么所有页面都会太短。
相反,我们让输出例程正常触发,然后如果我们删除任何框\removefromtop
,我们会将当前页面的内容推回到最近的贡献列表中,取消输出例程,并让 TeX 正常进行以将更多内容添加到页面底部。
答案2
我认为问题出在浮动后添加的垂直空间。因此,在下面的建议中,我修改了使用\myLength
并添加了 a \baselineskip
。
\documentclass{article}
\usepackage{lipsum, mwe}
\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate
% Below, I increase the separation between float and text and reduce
% the flexibility of the space
% You can see the default values by adding \the\textfloatsep inside
% the document environment
\newlength{\myFloatskip}
\setlength{\myFloatskip}{\the\myLength}
\addtolength{\myFloatskip}{\baselineskip}
\setlength{\textfloatsep}{\the\myFloatskip plus 0.0pt minus 0.0pt}
\newcommand{\myCommand}{\par%
\vspace{\myLength}%
{\centering\LARGE * * *\par}%
\vspace{\myLength}%
}
\begin{document}
\lipsum[1]
\lipsum[1]
\lipsum[1]
\lipsum[2]
\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}
\myCommand
\lipsum[2]
\lipsum[3]
\myCommand
\lipsum[1]
\myCommand
\lipsum[1]
\end{document}
答案3
经过几天的研究,我想我已经找到了解决问题的方法。思路如下:
在输出例程中,该\@cflt
命令在适当的情况下将顶部浮动与正文组合在一起。我修改了此命令,以便在辅助文件中记录表示文本正文开始位置(即,紧接着任何页面顶部浮动)的 Y 坐标。当我运行 时\myCommand
,我执行相同的操作,并将表示宏开始位置的 Y 坐标保存在辅助文件中。
在下一次编译时,我比较两个 Y 坐标。如果它们相等,则\myCommand
就在顶部浮点之后,我可以\vspace*{\dimexpr\myLength-\textfloatsep}
按照问题中的要求插入。
结果如下:
这比原来的结果更加和谐(尽管可以进行微小的调整):
然而,在测试过程中,我发现应用程序本身存在一个问题。实际上,宏插入的空间取决于宏在页面中的位置,而宏在页面中的位置又取决于宏插入的空间等。存在循环依赖。为了避免这种情况,我想您必须\myCommand
根据您想要如何使用它来阻止分页符在之前或之后出现。我愿意接受改进建议。
例如,在我的 MWE 中,如果对于我的第 4 段,我使用\lipsum[2]
而不是\lipsum[3]
,则会出现不稳定性,并且文档的汇编依次给出下面的情况 A(左)和 B(右):
如果我们从案例 A 开始,则宏将出现在第 2 页正文的顶部。
因此,在下一次编译时,我们将\myLength-\textfloatsep
在 的开头插入一个等于 的空格\myCommand
。现在,垂直空间\myLength-\textfloatsep
小于垂直空间\myLength
,因此有足够的空间让命令位于第 1 页,我们处于案例 B 中。
在下一次编译时,由于我们不再位于顶部浮动之后,因此\myCommand
插入一个等于 的垂直空间\myLength
。这一次,第 1 页上没有足够的空间,因此\myCommand
被发送回第 2 页的开头,因此我们回到案例 A。
妇女权利委员会:
\documentclass{article}
\usepackage{lipsum, mwe, refcount, iftex}
\ifluatex% For compatibility with LuaTeX, which I generally use.
\let\pdfsavepos\savepos
\let\pdflastypos\lastypos
\fi
\makeatletter
\def \@cflt{%
\let \@elt \@comflelt
\setbox\@tempboxa \vbox{}%
\@toplist
\setbox\@outputbox \vbox{%
\boxmaxdepth \maxdepth
\unvbox\@tempboxa
\vskip -\floatsep
\topfigrule
\vskip \textfloatsep
\pdfsavepos% <--- Added
\write\@auxout{\string\global\string\@namedef{@pageWithTopFloatYPos-\thepage}{\the\pdflastypos}}% <--- Added. We save the Y position of the top of "\@outputbox", i. e. the body of the text.
\unvbox\@outputbox
}%
\let\@elt\relax
\xdef\@freelist{\@freelist\@toplist}%
\global\let\@toplist\@empty
}
\newcounter{@ifTopFloatCnt}
\long\def\ifTopFloat#1#2{% A conditional to see if there is a top float on the current page. See https://tex.stackexchange.com/questions/712713/how-to-determine-from-the-document-body-whether-or-not-a-page-has-a-top-float.
\stepcounter{@ifTopFloatCnt}
\label{@ifTopFloat-\the@ifTopFloatCnt}\nopagebreak%
\ifcsname @pageWithTopFloatYPos-\getpagerefnumber{@ifTopFloat-\the@ifTopFloatCnt}\endcsname%
#1%
\else%
#2%
\fi%
}
\newlength{\myLength}
\setlength{\myLength}{30pt}% exaggerated value to illustrate
\newcounter{@myCommandCnt}
\newcounter{@myCommandCntAux}
\newcommand{\myCommand}{\par%
\stepcounter{@myCommandCnt}%
\pdfsavepos%
\write\@auxout{%
\string\stepcounter{@myCommandCntAux}%
^^J% New line in the auxiliary file
\string\global\string\@namedef{@myCommandYPos-\string\the@myCommandCntAux}{\the\pdflastypos}% Save the Y coordinate.
}%
\ifTopFloat{%
\ifnum \@nameuse{@myCommandYPos-\the@myCommandCnt} =
\csname @pageWithTopFloatYPos-\getpagerefnumber{@ifTopFloat-\the@ifTopFloatCnt}\endcsname% If there's a top float AND you're right after it
\vspace*{\dimexpr\myLength-\textfloatsep}
{\centering\LARGE * * * (a)\par}
\else% If there is a top float BUT you are not right after it
\vspace{\myLength}
{\centering\LARGE * * * (b)\par}
\fi
}{% If there is no top float
\vspace{\myLength}
{\centering\LARGE * * * (c)\par}
}
\vspace{\myLength}
}
\makeatother
\begin{document}
\lipsum[1]
\lipsum[1]
\lipsum[1]
\lipsum[3]% <--- If the current line is uncommented and the next line is commented, stability is achieved.
%\lipsum[2]% <--- If the previous line is commented and the current line is uncommented, instability occurs.
\begin{figure}[t]
\centering
\includegraphics{example-image}
\end{figure}
\myCommand
\lipsum[1]
\lipsum[2]
\begin{figure}[t]
\centering
\includegraphics{example-image-9x16}
\end{figure}
\myCommand
\lipsum[2]
\myCommand
\lipsum[1]
\myCommand
\lipsum[1]
\end{document}