2

TL;DR: how can I place a tikzmark at the actual beginning or end of a piece of text, ignoring spurious newlines added by other environments (see the pictures below)?

For the sake of simplicity, assume I want to define an environment whose purpose is to enclose one or more paragraphs between horizontal lines. To this aim, I want to create tikzmark references at the beginning and at the end of this environment, and then use the coordinates given by the tikzpagenodes library to draw the lines. A very simple implementation would look something like this.

\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
}

And in fact, if I try using the environment I have just defined, everything runs smoothly (red dots added for clarity).

\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}

first example

However, I quickly ran into problems as soon as I tried nesting some environments inside myenv. itemize, for instance, but actually any other environment that starts or ends with a newline.

\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}

second example

As you can see from the picture, there is some spurious vertical space, due to the fact that the tikzmark nodes are misplaced (or, to be more precise, they are not where I intended them to be). So the question is: how can I make sure that the tikzmarks are placed in the "correct" position for every possible (reasonable) use of the environment myenv?

A few remarks.

  • Yes, the implementation of myenv presented above is terrible. My actual implementation is way more involved, but for the sake of conciseness I stripped it down to a MWE.
  • I'm sure there are better, easier and more robust ways of achieving the simple task I have presented, but I really need tikzmark nodes for my (much) more ambitious environment, which is out of the scope of this question. So please do not post answer which use tcolorbox or similar packages (actually do, but only if you know how to nest breakable tcolorboxes, which is the effect I'm trying to recreate).
3
  • 1
    It's possible that this is because tikzmark is inside an hbox. What happens if you use \pgfmark instead? Commented Mar 4, 2021 at 0:03
  • @AndrewStacey That's an interesting idea. However, when I use \pgfmark, the spurious whitespace is still present, albeit different (a bit more above, a bit less below).
    – Delfad0r
    Commented Mar 4, 2021 at 0:15
  • Hmm, then I think the issue is that you want to insert some code at the moment text is actually put on the page (that it is a tikzmark or pgfmark is largely irrelevant to that) and I'm not sure how to achieve that. Commented Mar 4, 2021 at 11:33

1 Answer 1

2

Warning: This code is not very extensively tested and may very well not work in more complicated situations.

The complicated part is getting some code to be placed at the start of a paragraph. This is what \everypar is designed to do, but that gets overwritten and clobbered by so many other things that trying to use it is doomed to failure.

Fortunately, the LaTeX3 team have a package that works around this, which I learnt of from https://tex.stackexchange.com/a/326947/86, and that is l3galley. That answer is from 2016, but I just tried it and it worked fine on your use case.

\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}

This produces:

Demonstration of using tikzmark to tag the start of a paragraph

Now, there are a few snags already with this. You'll notice that I moved the \tikz command that draws the red circles to before the environment. This is because, I'm guessing, the \g_galley_par_begin_hook_tl is executed a few times but only one is actually typeset. This leads to a situation in which there are several tikzmarks that are defined with the same name but only one is written out to the aux file. Before the tikzmark is defined in the document then the one in the aux file wins (which is usually the one you want), but after it has been defined then the one that was last declared wins. I have a workaround for this with \tikzmarknode, but I need to investigate a bit to see what the right way to deal with this is for \tikzmark. For the lines that are drawn at the start and end of the paragraph then this isn't a problem as they are drawn before the tikzmark is defined in the document so they use the definition from the aux file.

Update 2021-03-03

Here's an update. It uses the l3galley code to mark paragraphs inside the environment, and then the tikzmark aliasing facility to pick the first and last of these marks and save them with the given name. (Note: I've updated the aliasing code a little to make aliases available from the outset, just as genuine tikzmarks are; I don't think this is used in this version of the code, though.)

I was running into problems with the actual marking code and where to put it because even with the overlay option then a TikZ picture creates a box and that can cause issues if it occurs in the wrong place, so I've added code that puts it in the footer. To ensure that the right code is executed, I use the tests from the tikzmark package to see if a given tikzmark is on a given page.

\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}

Here's what this produces:

Page 1 with paragraph marking Page 2 with paragraph marking

5
  • From what I understand from the package documentation, the problem with galley hooks is that they are performed at the beginning/end of each paragraph. If you try an itemize environment with two items, only the second one will be "outlined". This can probably be solved with a simple if statement, but there are other problems with spacing in this solution (for instance, try and typeset some text before the outline environment and see the difference between the list and the non-list case). This approach is certainly interesting, however, and I will tinker to see if I can get it to work.
    – Delfad0r
    Commented Mar 4, 2021 at 23:55
  • @Delfad0r Right, so this is more a proof-of-concept than a full solution. I've played around a bit with it and have explored some more possibilities, but some of it depends on exactly how you want it to work. For example, do you definitely want it to work via an environment? Commented Mar 5, 2021 at 13:55
  • @Delfad0r I've added an update. Commented Mar 5, 2021 at 22:47
  • This is brilliant, thank you! I will definitely mark your answer as accepted, since it perfectly addresses my question. As a sidenote, do you know if there would be any drawbacks to adding the tikz code to shipout/foreground instead of to the footer?
    – Delfad0r
    Commented Mar 7, 2021 at 9:46
  • @Delfad0r I don't think so. I did try a couple of different things before ending up with the footer, but there were a variety of issues with my first few versions so I don't know whether any of those were related to which code I was hooking in to to add the TikZ code. Commented Mar 7, 2021 at 15:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .