
情境
我正在嘗試創建一個巨集(此處稱為\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
最終文件的 a (或任何其他垂直模式材料)。
\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
我認為問題在於浮動後添加的垂直空間。因此,在我下面的建議中,我修改了 using\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 中。這次,第 1 頁上沒有足夠的空間,並且被發送回第 2 頁的開頭,因此我們回到了情況 A。\myCommand
\myLength
\myCommand
氣象局:
\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}