环境开始和结束时的 Tikzmark

环境开始和结束时的 Tikzmark

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或类似软件包的答案(实际上可以,但前提是您知道如何嵌套 breakable tcolorboxes,这是我试图重现的效果)。

答案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}

得出的结果为:

使用 tikzmark 标记段落开头的演示

现在,这已经有几个问题了。你会注意到,我把\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}

结果是这样的:

第 1 页,带段落标记 第 2 页,带段落标记

相关内容