
Contexto
Estoy intentando crear una macro (llamada aquí \myCommand
) que separa dos párrafos y agrega un espacio vertical diferente según el contexto. Pero supongo que este es un problema general que también puede ocurrir con macros como \section{...}
.
Normalmente, me gustaría insertar un espacio de valor \myLength
(para los propósitos de este ejemplo, tomaremos \myLength
= 30 pt) al principio \myCommand
y al final.
Si la macro cae en la parte superior de la página, después de un salto de página, \vspace{\myLength}
se ignora y no se agrega ningún espacio vertical, que es lo que quiero.
PERO el problema surge cuando, como en el caso anterior, la macro cae después de un salto de página y está precedida por uno o más flotadores superiores. En este caso, me gustaría que la macro insertara una vertical \vspace*{\dimexpr\myLength-\textfloatsep}
para mantener la coherencia.
Ejemplo
Aquí hay un ejemplo para ilustrar:
A continuación, la macro se comporta correctamente: cuando cae en la parte superior de la página después de un salto de página, no se inserta ningún espacio al inicio de la macro; y cuando no está en la parte superior de la página, \vspace{\myLength}
se inserta correctamente al inicio de la macro.
Sin embargo, cuando la macro cae en la parte superior de la página después de un salto de página y está precedida por un flotador superior, los espacios están totalmente desequilibrados.
Solo para mostrar que el problema es general y no específico \myCommand
, aquí está el mismo problema con \section{...}
:
Aquí hay un ejemplo mínimo que no funciona:
\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}
Meta
Para superar el problema mencionado anteriormente, me gustaría un espacio vertical con un valor de \vspace*{\dimexpr\myLength-\textfloatsep}
(para compensar el espacio vertical \textfloatsep
insertado por el flotador superior) solo en el siguiente caso:
al inicio de una página (después de un salto de página) Y un El flotador superior está presente.
En todos los demás casos, el uso \vspace{\myLength}
funciona perfectamente.
Ensayos
Mi enfoque (hasta ahora infructuoso) ha sido utilizar dos condicionales para insertar los espacios verticales correctos en el lugar correcto:
- Un primer condicional para probar la presencia de un flotador superior en la página actual,
- Un segundo condicional para comprobar que estamos en la parte superior de una nueva página, después de un salto de página.
Si se cumplen ambas condiciones, entonces estamos efectivamente al comienzo de una página (después de un salto de página) Y hay un flotador superior presente; luego insertamos \vspace*{\dimexpr\myLength-\textfloatsep}
. En todos los demás casos, insertamos \vspace{\myLength}
.
Gracias a una pregunta anterior, pude encontrar una manera de detectar la presencia de un flotador en la parte superior de una página:¿Cómo determinar a partir del cuerpo del documento si una página tiene o no un flotador superior?.
Sin embargo, el segundo condicional me plantea un problema y quería intentar usarlo \pagetotal
, pero no me parece adecuado:¿Cómo acceder alverdaderovalor de \pagetotal?.
Mi macro \myCommand
entonces se veía así:
\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
Ciertamente mi enfoque no es el correcto.
Respuesta1
Aquí hay una solución basada en LuaTeX. Todo lo que esté dentro del comando \removefromtop
se eliminará si se encuentra en la parte superior de la página y se mantendrá en caso contrario. Estoy usando una regla aquí para hacer que el efecto sea más obvio, pero puedes reemplazarla con \vspace
(o cualquier otro material en modo vertical) para el documento final.
\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}
El código es bastante complejo y no lo he probado mucho, por lo que esperaría algunos errores con documentos más complicados.
Cómo funciona
Primero, definimos un "atributo" personalizado que podemos usar para marcar el contenido de \removefromtop
.
A continuación, nos conectamos al buildpage_filter
, que se llama justo antes de que TeX agregue contenido a la "lista vertical principal". Lo complicado aquí es que llamamostex.triggerbuildpage()
adentroesta devolución de llamada, lo que hace que TeX borre sus “contribuciones recientes” e intente un salto de página.
Si se activa un salto de página, el control salta al archivo pre_output_filter
. Aquí, hemos agregado otra devolución de llamada que primero verifica si se trata de un salto de página "real" (y no un salto de página falso utilizado por LaTeX para vaciar los flotantes) y que no hay flotantes superiores en la página actual. Si este es el caso, recorremos el contenido de la página y eliminamos cualquier contenido inicial marcado con nuestro atributo personalizado. Y si marcamos algún contenido, lo indicamos a nuestro buildpage_filter
y devolvemos el contenido de la nueva página.
Una vez que pre_output_filter
finaliza, el control se reanuda después de tex.triggerbuildpage()
, excepto que por ahora estamos dentro de la rutina de salida. Si el más reciente pre_output_filter
eliminó con éxito algún contenido y estamos dentro de la primera rutina de salida, entonces eliminamos todos los tokens en la pila de entrada de TeX. Como estamos dentro de la rutina de salida, esto vacía la expansión actual de la \output
lista de tokens. Luego limpiamos \box255
(el contenido de la página actual), volvemos a mover su contenido a la lista de contribuciones recientes y regresamos.
¿Por qué hacerlo de esta manera?
No podemos saber si el \removefromtop
cuadro está en la parte superior de la página o no hasta que se active la rutina de salida. Sin embargo, cuando eliminamos los \removefromtop
cuadros, acortamos la página, por lo que si simplemente enganchamos pre_output_filter
para eliminar los cuadros, todas las páginas serán demasiado cortas.
En su lugar, dejamos que la rutina de salida se active normalmente, luego, si eliminamos cualquiera de las \removefromtop
casillas, volvemos a colocar el contenido de la página actual en la lista de contribuciones recientes, cancelamos la rutina de salida y dejamos que TeX proceda normalmente para agregar más contenido a la lista. final de la página.
Respuesta2
Creo que el problema está en el espacio vertical agregado después del flotador. Entonces, en mi sugerencia a continuación, modifico ese uso \myLength
y agrego un \baselineskip
también.
\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}
Respuesta3
Después de varios días de investigación, creo que encontré una solución a mi problema. La idea es la siguiente:
En la rutina de salida, el \@cflt
comando ensambla, en su caso, los flotadores superiores con el texto del cuerpo. Modifiqué este comando para registrar en el archivo auxiliar la coordenada Y que representa el inicio del cuerpo del texto (es decir, justo después de cualquier elemento flotante en la parte superior de la página). Cuando ejecuto \myCommand
, hago lo mismo y guardo la coordenada Y que representa el inicio de la macro en el archivo auxiliar.
En la siguiente compilación, comparo las dos coordenadas Y. Si son iguales, entonces \myCommand
está justo después de los flotadores superiores y puedo insertarlos \vspace*{\dimexpr\myLength-\textfloatsep}
según lo solicitado en la pregunta.
El resultado es el siguiente:
que es más armonioso que el resultado original (aunque se podrían hacer pequeños ajustes):
Sin embargo, durante mis pruebas encontré un problema inherente a la aplicación. En efecto, el espacio insertado por la macro depende de la posición de la macro en la página, que a su vez depende del espacio insertado por la macro, etc. Hay una dependencia circular. Para evitar esto, supongo que deberías evitar un salto de página antes o después \myCommand
dependiendo de cómo quieras usarlo. Estoy abierto a sugerencias de mejora.
Por ejemplo, en mi MWE, si para mi cuarto párrafo uso \lipsum[2]
en lugar de \lipsum[3]
, se produce inestabilidad y la compilación de los documentos da sucesivamente los casos A (izquierda) y B (derecha) a continuación:
Si comenzamos con el caso A, la macro termina en la parte superior del cuerpo del texto en la página 2.
Entonces, en la próxima compilación, insertaremos un espacio igual \myLength-\textfloatsep
al comienzo de \myCommand
. Ahora, el espacio vertical \myLength-\textfloatsep
es menor que el espacio vertical \myLength
, por lo que hay suficiente espacio para que el comando esté en la página 1 y estamos en el caso B.
En la próxima compilación, como ya no buscamos un flotador superior, \myCommand
inserta un espacio vertical igual a \myLength
. Esta vez, no hay suficiente espacio en la página 1 y \myCommand
se devuelve al principio de la página 2, por lo que volvemos al caso A.
El 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}