
Контекст
Я пытаюсь создать макрос (называемый здесь \myCommand
), который разделяет два абзаца и добавляет разный вертикальный интервал в зависимости от контекста. Но я предполагаю, что это общая проблема, которая может также возникнуть с такими макросами, как \section{...}
.
Обычно я хотел бы вставить пробел со значением \myLength
(для целей этого примера мы возьмем \myLength
= 30 pt) в начале \myCommand
и в конце.
Если макрос попадает в верхнюю часть страницы, после разрыва страницы, \vspace{\myLength}
он игнорируется и вертикальный пробел не добавляется, что мне и нужно.
НО проблема возникает, когда, как в предыдущем случае, макрос попадает после разрыва страницы и ему предшествует один или несколько верхних float. В этом случае я хотел бы, чтобы макрос вставлял вертикальный пробел \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
Я думаю, проблема в вертикальном пробеле, добавленном после float. Поэтому в моем предложении ниже я изменяю это с помощью \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.
При следующей компиляции, поскольку мы больше не ищем верхний float, \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}