TL;DR:我怎样才能tikzmark
在实际的一段文本的开始或结束,忽略其他环境添加的虚假换行符(见下图)?
为了简单起见,假设我想定义一个环境,其目的是将一个或多个段落括在水平线之间。为此,我想tikzmark
在此环境的开头和结尾创建引用,然后使用tikzpagenodes
库提供的坐标来绘制线条。一个非常简单的实现看起来像这样。
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{tikzmark}
\usepackage{tikzpagenodes}
\usepackage{lipsum}
\newenvironment{myenv}{
\par\tikzmark{begin}%
\tikz[overlay,remember picture] \draw
([yshift=\baselineskip]{{pic cs:begin}-|current page text area.west}) --
([yshift=\baselineskip]{{pic cs:begin}-|current page text area.east});%
\ignorespaces
}{
\tikzmark{end}%
\tikz[overlay,remember picture] \draw
([yshift=-depth("g")]{{pic cs:end}-|current page text area.west}) --
([yshift=-depth("g")]{{pic cs:end}-|current page text area.east});%
\ignorespaces
}
事实上,如果我尝试使用我刚刚定义的环境,一切都会顺利运行(为了清楚起见添加了红点)。
\begin{document}
\begin{myenv}
\lipsum[1]
\end{myenv}
\tikz[overlay,remember picture] \fill[red]
(pic cs:begin) circle(2pt) (pic cs:end) circle(2pt);
\end{document}
然而,当我尝试在myenv
.中嵌套某些环境时,我很快就遇到了问题itemize
,但实际上是任何其他以换行符开始或结束的环境。
\begin{document}
\begin{myenv}
\begin{itemize}
\item \lipsum[1]
\end{itemize}
\end{myenv}
\tikz[overlay,remember picture] \fill[red]
(pic cs:begin) circle(2pt) (pic cs:end) circle(2pt);
\end{document}
从图片中可以看出,由于节点tikzmark
放错了位置(或者更准确地说,它们不在我想要的位置),所以存在一些虚假的垂直空间。所以问题是:我如何确保在tikzmark
环境的每种可能(合理)用途中,s 都放置在“正确”的位置myenv
?
几点评论。
- 是的,上面介绍的实现
myenv
很糟糕。我实际的实现要复杂得多,但为了简洁起见,我将其精简为 MWE。 - 我确信有更好、更简单、更强大的方法来实现我提出的简单任务,但我确实需要
tikzmark
节点来满足我(更)雄心勃勃的环境,这超出了这个问题的范围。所以请不要发布使用tcolorbox
或类似软件包的答案(实际上可以,但前提是您知道如何嵌套 breakabletcolorbox
es,这是我试图重现的效果)。
答案1
警告: 该代码尚未经过非常广泛的测试,并且在更复杂的情况下可能无法工作。
复杂的部分是将一些代码放在段落的开头。这\everypar
就是设计要做,但是它会被许多其他东西覆盖和破坏,因此尝试使用它注定会失败。
幸运的是,LaTeX3 团队有一个可以解决这个问题的软件包,这是我从https://tex.stackexchange.com/a/326947/86,那就是l3galley
。这个答案是 2016 年的,但我刚刚尝试过,它在你的用例上效果很好。
\documentclass{article}
\usepackage{tikz}
\usetikzlibrary{tikzmark}
\usepackage{tikzpagenodes}
\usepackage{lipsum}
\usepackage{xparse}
\usepackage{l3galley}
\NewDocumentCommand\OutlineParagraph { m }
{
\tikz[overlay,remember picture] \draw
([yshift=\baselineskip]{{pic cs:begin-#1}-|current page text area.west}) --
([yshift=\baselineskip]{{pic cs:begin-#1}-|current page text area.east});
\tikz[overlay,remember picture] \draw
({{pic cs:end-#1}-|current page text area.west}) --
({{pic cs:end-#1}-|current page text area.east});
}
\ExplSyntaxOn
\tl_new:N \g_splicer_saved_tl
\NewDocumentEnvironment{myenv}{ m }
{
\OutlineParagraph{#1}
\group_begin:
\tl_set:Nn \l_splicer_name_tl {#1}
\tl_gset:NV \g_splicer_saved_tl \g_galley_par_begin_hook_tl
\tl_gput_right:Nn \g_galley_par_begin_hook_tl
{
\pgfmark{begin-#1}
}
}
{
\pgfmark{end-\tl_use:N \l_splicer_name_tl}
\tl_gset:NV \g_galley_par_begin_hook_tl \g_splicer_saved_tl
\group_end:
}
\ExplSyntaxOff
\begin{document}
\tikz[overlay,remember picture] \filldraw[red]
(pic cs:begin-nolist) circle(2pt) -- (pic cs:end-nolist) circle(2pt);
\begin{myenv}{nolist}
\lipsum[1]
\end{myenv}
\begin{myenv}{list}
\begin{itemize}
\item \lipsum[1]
\end{itemize}
\end{myenv}
\tikz[overlay,remember picture] \filldraw[blue]
(pic cs:begin-list) circle(2pt) -- (pic cs:end-list) circle(2pt);
\end{document}
得出的结果为:
现在,这已经有几个问题了。你会注意到,我把\tikz
绘制红色圆圈的命令移到了前环境。这是因为,我猜,\g_galley_par_begin_hook_tl
执行了几次,但实际上只有一个被排版。这导致出现这种情况,即有多个 tikzmark定义具有相同的名称,但只有一个写入辅助文件。在文档中定义 tikzmark 之前,辅助文件中的那个获胜(通常是您想要的那个),但在定义之后,最后声明的那个获胜。我有一个解决这个问题的解决方法\tikzmarknode
,但我需要调查一下,看看处理这个问题的正确方法是什么\tikzmark
。对于在段落开头和结尾绘制的线条,这不是问题,因为它们是在文档中定义 tikzmark 之前绘制的,所以它们使用辅助文件中的定义。
更新 2021-03-03
这是更新。它使用l3galley
代码在环境中标记段落,然后使用 tikzmark 别名功能选择这些标记中的第一个和最后一个,并使用给定的名称保存它们。(注意:我稍微更新了别名代码,以便从一开始就可以使用别名,就像真正的 tikzmark 一样;不过,我不认为这在本版本的代码中使用。)
我在实际标记代码以及将其放在何处方面遇到了问题,因为即使有选项overlay
,TikZ 图片也会创建一个框,如果它出现在错误的位置,则会导致问题,因此我添加了将其放在页脚中的代码。为了确保执行正确的代码,我使用 tikzmark 包中的测试来查看给定的 tikzmark 是否在给定的页面上。
\documentclass{article}
%\url{https://tex.stackexchange.com/q/585759/86}
\usepackage{tikz}
\usetikzlibrary{tikzmark}
\usepackage{tikzpagenodes}
\usepackage{lipsum}
\usepackage{xparse}
\usepackage{l3galley}
% These are the commands that will mark the start and end of the paragraphs
\NewDocumentCommand\MarkParagraphStart { O{red} m m }
{%
\tikz[overlay,remember picture] {
\draw[#1]
(
[yshift=#2]
{{pic cs:#3 start}-|current page text area.west}) --
(
[yshift=#2]
{{pic cs:#3 start}-|current page text area.east})
}%
}
\NewDocumentCommand\MarkParagraphEnd { O{blue,dashed,ultra thick} m m }
{%
\tikz[overlay,remember picture] {
\draw[#1]
(
[yshift=#2]
{{pic cs:#3 end}-|current page text area.west}) --
(
[yshift=#2]
{{pic cs:#3 end}-|current page text area.east})
}%
}
\ExplSyntaxOn
% Boolean to control whether we are marking paragraphs
\bool_new:N \g_tikzmark_paragraphs_bool
% Count to keep track of paragraphs
\int_new:N \g_tikzmark_paragraph_count_int
% Installs the hooks to possibly mark the start of paragraphs
\tl_gput_right:Nn \g_galley_par_begin_hook_tl
{
% Are we marking paragraphs?
\bool_if:NT \g_tikzmark_paragraphs_bool
{
% Increment the counter
\int_gincr:N \g_tikzmark_paragraph_count_int
% Mark this place
\pgfmark{paragraph~start~\int_use:N \g_tikzmark_paragraph_count_int}
}
}
% Installs the hooks to possibly mark the end of paragraphs
\tl_gput_right:Nn \g_galley_par_end_hook_tl
{
% Are we marking paragraphs?
\bool_if:NT \g_tikzmark_paragraphs_bool
{
% Mark this place
\pgfmark{paragraph~end~\int_use:N \g_tikzmark_paragraph_count_int}
}
}
% We do the actual drawing in the footer, this prop will contain a list of
% commands to execute. To decide which commands, we associate each one
% with a tikzmark. If the tikzmark is on the current page, the command
% is executed and deleted.
\prop_new:N \g_tikzmark_actions_prop
% This is the command that possibly executes the code
\cs_new:Npn \tikzmark_process_prop:
{
\prop_map_inline:Nn \g_tikzmark_actions_prop
{
\iftikzmarkoncurrentpage{##1}%
##2
\prop_gremove:Nn \g_tikzmark_actions_prop {##1}
\fi
}
}
% Install the processing commands in the footer
\tl_gput_right:cn {@oddfoot}
{
\tikzmark_process_prop:
}
\tl_gput_right:cn {@evenfoot}
{
\tikzmark_process_prop:
}
% The value of \baselineskip needs to be fixed at the invocation time rather
% than execution time. Fortunately, LaTeX3 is adept at handling expansion
% so we wrap the code to install the marks in LaTeX3 functions.
\cs_new:Npn \tikzmark_paragraph_start:nnn #1#2#3
{
\prop_gput:Nnn \g_tikzmark_actions_prop {#1} {
\MarkParagraphStart {#2}{#3}
}
}
\cs_new:Npn \tikzmark_paragraph_end:nnn #1#2#3
{
\prop_gput:Nnn \g_tikzmark_actions_prop {#1} {
\MarkParagraphEnd {#2}{#3}
}
}
\cs_generate_variant:Nn \tikzmark_paragraph_start:nnn {nVn}
% We don't use this command, but it demonstrates how to make a user
% command to draw things using tikzmarks
\NewDocumentCommand \TikzmarkExecute { m m }
{
\prop_gput:Nnn \g_tikzmark_actions_prop {#1} {#2}
}
% Here's the environment to mark paragraphs
\NewDocumentEnvironment{MarkSection}{ m }
{
% Set the boolean for marking the paragraphs
\bool_gset_true:N \g_tikzmark_paragraphs_bool
% Alias the mark at the start of the first paragraph
\tikzmarkalias{#1~start}{paragraph~start~\int_eval:n { \g_tikzmark_paragraph_count_int +1 }}
% Install the code to draw the marks at the start and end
\tikzmark_paragraph_start:nVn {#1~start} \baselineskip {#1}
\tikzmark_paragraph_end:nnn {#1~end} {-2pt} {#1}
}
{
% If we're in horizontal mode when the environment ends then
% we want to exit it to ensure that the end of the last paragraph
% is marked
\ifhmode
\par
\fi
% Stop marking paragraphs
\bool_gset_false:N \g_tikzmark_paragraphs_bool
% Alias the end of the last paragraph
\tikzmarkalias{#1~end}{paragraph~end~\int_use:N \g_tikzmark_paragraph_count_int}
}
\ExplSyntaxOff
\begin{document}
\begin{MarkSection}{no list}
\lipsum[1-4]
\end{MarkSection}
\begin{MarkSection}{list}
\begin{itemize}
\item \lipsum[1]
\item \lipsum[2]
\item \lipsum[3]
\item \lipsum[4]
\end{itemize}
\end{MarkSection}
\end{document}
结果是这样的: