
Contexto
Estou tentando criar uma macro (chamada aqui \myCommand
) que separe dois parágrafos e adicione um espaço vertical diferente dependendo do contexto. Mas suponho que este seja um problema geral que também pode ocorrer com macros como \section{...}
.
Normalmente, eu gostaria de inserir um espaço de valor \myLength
(para fins deste exemplo, usaremos \myLength
= 30 pt) no início \myCommand
e no final dele.
Se a macro cair no topo da página, após uma quebra de página, \vspace{\myLength}
é ignorada e nenhum espaço vertical é adicionado, que é o que desejo.
MAS o problema surge quando, como no caso anterior, a macro cai após uma quebra de página e é precedida por um ou mais pontos flutuantes superiores. Nesse caso, gostaria que a macro inserisse uma vertical \vspace*{\dimexpr\myLength-\textfloatsep}
para manter a consistência.
Exemplo
Aqui está um exemplo para ilustrar:
Abaixo, a macro se comporta corretamente: quando cai no topo da página após uma quebra de página, nenhum espaço é inserido no início da macro; e quando não está no topo da página, \vspace{\myLength}
está inserido corretamente no início da macro.
Porém, quando a macro cai no topo da página após uma quebra de página e é precedida por um float superior, os espaços ficam totalmente desequilibrados.
Apenas para mostrar que o problema é geral e não específico \myCommand
, aqui está o mesmo problema com \section{...}
:
Aqui está um exemplo mínimo que não 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 o problema mencionado acima, gostaria de um espaço vertical com valor de \vspace*{\dimexpr\myLength-\textfloatsep}
(para compensar o espaço vertical \textfloatsep
inserido pelo float superior) apenas no seguinte caso:
no início de uma página (após uma quebra de página) E a o flutuador superior está presente.
Em todos os outros casos, using \vspace{\myLength}
funciona perfeitamente.
Ensaios
Minha abordagem (até agora sem sucesso) foi usar duas condicionais para inserir os espaços verticais corretos no lugar certo:
- Uma primeira condicional para testar a presença de um float superior na página atual,
- Uma segunda condicional para verificar se estamos no topo de uma nova página, após uma quebra de página.
Se ambas as condições forem atendidas, então estamos de fato no início de uma página (após uma quebra de página) E um float superior está presente; então inserimos \vspace*{\dimexpr\myLength-\textfloatsep}
. Em todos os outros casos, inserimos \vspace{\myLength}
.
Graças a uma pergunta anterior, consegui encontrar uma maneira de detectar a presença de um ponto flutuante no topo da página em uma página:Como determinar no corpo do documento se uma página tem ou não um flutuador superior?.
A segunda condicional representa um problema para mim, entretanto, e eu queria tentar usar \pagetotal
, mas não parece adequado:Como acessar overdadeirovalor de \pagetotal?.
Minha macro \myCommand
então ficou assim:
\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
Minha abordagem certamente não é a correta.
Responder1
Aqui está uma solução baseada em LuaTeX. Qualquer coisa dentro do comando \removefromtop
será removida se cair no topo da página e mantida caso contrário. Estou usando uma regra aqui para tornar o efeito mais óbvio, mas você pode substituí-la por um \vspace
(ou qualquer outro material de modo vertical) para o 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}
O código é bastante complexo e não o testei muito, então esperaria alguns bugs em documentos mais complicados.
Como funciona
Primeiro, definimos um “atributo” personalizado que podemos usar para marcar o conteúdo de \removefromtop
.
Em seguida, nos conectamos ao buildpage_filter
, que é chamado logo antes do TeX adicionar conteúdo à “lista vertical principal”. A parte complicada aqui é que chamamostex.triggerbuildpage()
dentroesse retorno de chamada, que faz com que o TeX libere suas “contribuições recentes” e tente uma quebra de página.
Se uma quebra de página for acionada, o controle saltará para o arquivo pre_output_filter
. Aqui, adicionamos outro retorno de chamada que primeiro verifica se esta é uma quebra de página “real” (e não uma quebra de página falsa usada pelo LaTeX para liberar carros flutuantes) e se não há carros flutuantes superiores na página atual. Se for esse o caso, iteramos pelo conteúdo da página e removemos qualquer conteúdo inicial marcado com nosso atributo personalizado. E se marcamos algum conteúdo, sinalizamos isso de volta para a nossa página buildpage_filter
e retornamos o novo conteúdo da página.
Assim que pre_output_filter
terminar, o controle será retomado após o tex.triggerbuildpage()
, exceto por enquanto, estamos dentro da rotina de saída. Se o mais recente pre_output_filter
removeu algum conteúdo com sucesso e estamos dentro da primeira rotina de saída, removemos todos os tokens da pilha de entrada do TeX. Como estamos dentro da rotina de saída, isso esvazia a expansão atual da \output
lista de tokens. Em seguida, limpamos \box255
(o conteúdo da página atual), movemos seu conteúdo de volta para a lista de contribuições recentes e retornamos.
Por que fazer assim?
Não podemos saber se a \removefromtop
caixa está ou não no topo da página até que a rotina de saída seja acionada. Porém, quando removemos as \removefromtop
caixas, encurtamos a página, então se apenas engancharmos pre_output_filter
para remover as caixas, todas as páginas ficarão muito curtas.
Em vez disso, deixamos a rotina de saída disparar normalmente e, se removermos qualquer uma das \removefromtop
caixas, colocamos o conteúdo da página atual de volta na lista de contribuições recentes, cancelamos a rotina de saída e deixamos o TeX prosseguir normalmente para adicionar mais conteúdo ao Rodapé da página.
Responder2
Acho que o problema está no espaço vertical adicionado após o flutuador. Então, na minha sugestão abaixo, eu modifico isso usando \myLength
e adiciono um \baselineskip
também.
\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}
Responder3
Depois de vários dias de pesquisa, acho que encontrei uma solução para o meu problema. A ideia é a seguinte:
Na rotina de saída, o \@cflt
comando monta, quando apropriado, os flutuadores superiores com o corpo do texto. Modifiquei este comando para registrar no arquivo auxiliar a coordenada Y que representa o início do corpo do texto (ou seja, logo após qualquer flutuação no topo da página). Quando executo \myCommand
, faço a mesma coisa e salvo a coordenada Y que representa o início da macro no arquivo auxiliar.
Na próxima compilação, comparo as duas coordenadas Y. Se forem iguais, então \myCommand
fica logo após os flutuadores superiores, e posso inserir \vspace*{\dimexpr\myLength-\textfloatsep}
conforme solicitado na pergunta.
O resultado é o seguinte:
que é mais harmonioso que o resultado original (embora possam ser feitos pequenos ajustes):
Durante meus testes, porém, encontrei um problema inerente ao aplicativo. Na verdade, o espaço inserido pela macro depende da posição da macro na página, que por sua vez depende do espaço inserido pela macro, etc. Existe uma dependência circular. Para evitar isso, suponho que você teria que evitar uma quebra de página antes ou depois, \myCommand
dependendo de como deseja usá-la. Estou aberto a sugestões de melhorias.
Por exemplo, no meu MWE, se para o meu 4º parágrafo eu usar \lipsum[2]
em vez de \lipsum[3]
, ocorre instabilidade e a compilação dos documentos dá sucessivamente os casos A (esquerda) e B (direita) abaixo:
Se começarmos com o caso A, a macro terminará no topo do corpo do texto na página 2.
Portanto, na próxima compilação, inseriremos um espaço igual ao \myLength-\textfloatsep
início de \myCommand
. Agora, o espaço vertical \myLength-\textfloatsep
é menor que o espaço vertical \myLength
, então há espaço suficiente para o comando estar na página 1 e estamos no caso B.
Na próxima compilação, como não estamos mais atrás de um float superior, \myCommand
insere um espaço vertical igual a \myLength
. Desta vez, não há espaço suficiente na página 1 e \myCommand
é enviado de volta ao início da página 2, então voltamos ao caso A.
O 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}