
Kontext
Ich versuche, ein Makro (hier genannt \myCommand
) zu erstellen, das zwei Absätze trennt und je nach Kontext einen anderen vertikalen Abstand hinzufügt. Aber ich vermute, dass dies ein allgemeines Problem ist, das auch bei Makros wie auftreten kann \section{...}
.
Normalerweise möchte ich am Anfang und am Ende einen Leerraum mit dem Wert \myLength
(für dieses Beispiel nehmen wir = 30 pt) einfügen. Wenn das Makro am oberen Seitenrand steht, wird es nach einem Seitenumbruch ignoriert und es wird kein vertikaler Leerraum hinzugefügt, was ich auch möchte. ABER das Problem entsteht, wenn das Makro, wie im vorherigen Fall, nach einem Seitenumbruch steht und von einem oder mehreren oberen Floats eingeleitet wird. In diesem Fall möchte ich, dass das Makro einen vertikalen Leerraum einfügt, um die Konsistenz zu wahren.\myLength
\myCommand
\vspace{\myLength}
\vspace*{\dimexpr\myLength-\textfloatsep}
Beispiel
Hier ist ein Beispiel zur Veranschaulichung:
Nachfolgend verhält sich das Makro korrekt: Wenn es nach einem Seitenumbruch am oberen Seitenrand steht, wird am Anfang des Makros kein Leerzeichen eingefügt, und wenn es nicht am oberen Seitenrand steht, \vspace{\myLength}
wird es am Anfang des Makros korrekt eingefügt.
Wenn das Makro jedoch nach einem Seitenumbruch oben auf der Seite steht und ihm ein Top-Float vorangestellt ist, sind die Abstände völlig unausgeglichen.
Um zu zeigen, dass das Problem allgemein und nicht spezifisch ist \myCommand
, hier das gleiche Problem mit \section{...}
:
Hier ist ein nicht funktionierendes Minimalbeispiel:
\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}
Ziel
Um das oben genannte Problem zu lösen, hätte ich gerne einen vertikalen Abstand mit einem Wert von \vspace*{\dimexpr\myLength-\textfloatsep}
(um den vertikalen Abstand auszugleichen \textfloatsep
, der durch den oberen Float eingefügt wird) nur in folgendem Fall:
am Anfang einer Seite (nach einem Seitenumbruch) UND ein oberer Float ist vorhanden.
In allen anderen Fällen \vspace{\myLength}
funktioniert die Verwendung einwandfrei.
Versuche
Mein (bisher erfolgloser) Ansatz bestand darin, zwei Bedingungssätze zu verwenden, um die richtigen vertikalen Leerzeichen an der richtigen Stelle einzufügen:
- Eine erste Bedingung, um das Vorhandensein eines Top-Floats auf der aktuellen Seite zu testen.
- Eine zweite Bedingung, um zu überprüfen, ob wir uns nach einem Seitenumbruch am Anfang einer neuen Seite befinden.
Wenn beide Bedingungen erfüllt sind, dann befinden wir uns tatsächlich am Anfang einer Seite (nach einem Seitenumbruch) UND es ist ein Top-Float vorhanden; dann fügen wir ein \vspace*{\dimexpr\myLength-\textfloatsep}
. In allen anderen Fällen fügen wir ein \vspace{\myLength}
.
Dank einer früheren Frage konnte ich eine Möglichkeit finden, das Vorhandensein eines Top-of-Page-Floats auf einer Seite zu erkennen:Wie kann man anhand des Dokumenttexts feststellen, ob eine Seite einen Top-Float hat oder nicht?.
Der zweite Konditional stellt für mich allerdings ein Problem dar und ich wollte versuchen, ihn zu verwenden \pagetotal
, aber das scheint nicht geeignet:So greifen Sie auf dieWAHRWert von \pagetotal?.
Mein Makro \myCommand
sah dann so aus:
\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
Mein Ansatz ist sicherlich nicht der richtige.
Antwort1
Hier ist eine LuaTeX-basierte Lösung. Alles innerhalb des Befehls \removefromtop
wird entfernt, wenn es oben auf der Seite liegt, und andernfalls beibehalten. Ich verwende hier eine Regel, um den Effekt deutlicher zu machen, aber Sie können sie \vspace
für das endgültige Dokument durch ein (oder ein beliebiges anderes Material im vertikalen Modus) ersetzen.
\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}
Der Code ist ziemlich komplex und ich habe ihn nicht sehr ausführlich getestet. Daher würde ich bei komplizierteren Dokumenten mit einigen Fehlern rechnen.
Wie es funktioniert
Zuerst definieren wir ein benutzerdefiniertes „Attribut“, mit dem wir den Inhalt von markieren können \removefromtop
.
Als nächstes haken wir uns in den ein buildpage_filter
, der aufgerufen wird, kurz bevor TeX Inhalte zur „Hauptvertikalliste“ hinzufügt. Der schwierige Teil dabei ist, dass wirtex.triggerbuildpage()
innenDieser Rückruf veranlasst TeX, seine „letzten Beiträge“ zu löschen und einen Seitenumbruch zu versuchen.
Wenn ein Seitenumbruch ausgelöst wird, springt die Steuerung zum pre_output_filter
. Hier haben wir einen weiteren Rückruf hinzugefügt, der zunächst prüft, ob es sich um einen „echten“ Seitenumbruch handelt (und nicht um einen falschen Seitenumbruch, der von LaTeX zum Leeren von Floats verwendet wird) und ob auf der aktuellen Seite keine Top-Floats vorhanden sind. Wenn dies der Fall ist, durchlaufen wir den Inhalt der Seite und entfernen alle anfänglichen Inhalte, die mit unserem benutzerdefinierten Attribut markiert sind. Und wenn wir Inhalte markiert haben, signalisieren wir dies an unseren zurück buildpage_filter
und geben den neuen Seiteninhalt zurück.
Sobald der pre_output_filter
abgeschlossen ist, wird die Steuerung nach dem wieder aufgenommen tex.triggerbuildpage()
, außer dass wir uns jetzt in der Ausgaberoutine befinden. Wenn der letzte pre_output_filter
erfolgreich Inhalte entfernt hat und wir uns in der ersten Ausgaberoutine befinden, entfernen wir alle Token auf dem Eingabestapel von TeX. Da wir uns in der Ausgaberoutine befinden, wird die aktuelle Erweiterung der \output
Tokenliste geleert. Dann löschen wir \box255
(den Inhalt der aktuellen Seite), verschieben ihren Inhalt zurück in die Liste der letzten Beiträge und kehren zurück.
Warum so?
Wir können nicht wissen, ob sich die \removefromtop
Box oben auf der Seite befindet oder nicht, bis die Ausgaberoutine ausgelöst wird. Wenn wir die \removefromtop
Boxen jedoch entfernen, verkürzen wir die Seite. Wenn wir also nur den Haken setzen würden, pre_output_filter
um die Boxen zu entfernen, wären alle Seiten zu kurz.
Stattdessen lassen wir die Ausgaberoutine wie gewohnt auslösen. Wenn wir dann eines der \removefromtop
Kästchen entfernen, schieben wir den Inhalt der aktuellen Seite zurück in die Liste der letzten Beiträge, brechen die Ausgaberoutine ab und lassen TeX wie gewohnt fortfahren, um weiteren Inhalt am unteren Ende der Seite hinzuzufügen.
Antwort2
Ich denke, das Problem liegt an dem vertikalen Abstand, der nach dem Float hinzugefügt wird. In meinem Vorschlag unten ändere ich das also \myLength
und füge \baselineskip
auch ein hinzu.
\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}
Antwort3
Nach mehreren Tagen der Recherche glaube ich, eine Lösung für mein Problem gefunden zu haben. Die Idee ist folgende:
In der Ausgaberoutine \@cflt
fügt der Befehl, wo angemessen, die oberen Floats mit dem Haupttext zusammen. Ich habe diesen Befehl so geändert, dass in der Hilfsdatei die Y-Koordinate aufgezeichnet wird, die den Anfang des Haupttexts darstellt (also direkt nach allen Floats am oberen Seitenrand). Wenn ich ausführe \myCommand
, mache ich dasselbe und speichere die Y-Koordinate, die den Anfang des Makros darstellt, in der Hilfsdatei.
Bei der nächsten Kompilierung vergleiche ich die beiden Y-Koordinaten. Wenn sie gleich sind, dann \myCommand
steht gleich nach oben Floats, und ich kann sie \vspace*{\dimexpr\myLength-\textfloatsep}
wie in der Frage gewünscht einfügen.
Das Ergebnis ist wie folgt:
das harmonischer ist als das ursprüngliche Ergebnis (obwohl kleinere Anpassungen vorgenommen werden könnten):
Bei meinen Tests stieß ich jedoch auf ein inhärentes Problem der Anwendung. Tatsächlich hängt der vom Makro eingefügte Abstand von der Position des Makros auf der Seite ab, die wiederum vom vom Makro eingefügten Abstand usw. abhängt. Es besteht eine zirkuläre Abhängigkeit. Um dies zu vermeiden, müssten Sie vermutlich einen Seitenumbruch davor oder danach verhindern, \myCommand
je nachdem, wie Sie ihn verwenden möchten. Ich bin offen für Verbesserungsvorschläge.
Wenn ich beispielsweise in meinem MWE für meinen vierten Absatz \lipsum[2]
anstelle von verwende, \lipsum[3]
tritt Instabilität auf und die Zusammenstellung der Dokumente ergibt nacheinander die folgenden Fälle A (links) und B (rechts):
Wenn wir mit Fall A beginnen, landet das Makro oben im Haupttext auf Seite 2.
Bei der nächsten Kompilierung fügen wir daher \myLength-\textfloatsep
am Anfang von ein Leerzeichen ein, das gleich ist \myCommand
. Jetzt ist der vertikale Abstand \myLength-\textfloatsep
kleiner als der vertikale Abstand \myLength
, sodass genügend Platz für den Befehl auf Seite 1 vorhanden ist und wir uns im Fall B befinden.
Da wir bei der nächsten Kompilierung nicht mehr nach einem Top-Float suchen, \myCommand
fügt ein vertikales Leerzeichen ein, das gleich ist \myLength
. Dieses Mal ist auf Seite 1 nicht genügend Platz vorhanden und es \myCommand
wird an den Anfang von Seite 2 zurückgeschickt, sodass wir uns wieder im Fall A befinden.
Das MWE:
\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}