6

I would like some adjacent tcolorboxes to be connected, but not by using the option beforeafter skip=0pt, because I also want those boxes to have normal spacing from other text. That is, I expect the boxes as in this picture:

enter image description here

automatically changing to the looking in the picture below:

enter image description here

How can one achieve this?

Below is a MWE to play with.

\documentclass{article}

\usepackage[many]{tcolorbox}
\usepackage{blindtext}

\newtcolorbox{definition}{enhanced jigsaw,pad at break*=1mm,breakable,
left=4mm,right=4mm,top=1mm,bottom=1mm,
% beforeafter skip balanced=0pt,
colback=orange!10,boxrule=0pt,frame hidden,
borderline west={1.5mm}{-1mm}{green!50!black},arc=.7mm}

\begin{document}

\begin{definition}
    ...
\end{definition}

\begin{definition}
    \blindtext
\end{definition}

\end{document}

Old:

I have written the following (very ugly) code:

\documentclass{article}
\usepackage[many]{tcolorbox}

% \newenvironment{testbox}
% {\begin{tcolorbox}
% [enhanced jigsaw,pad at break*=1mm,breakable,
% colback=orange!10!white,boxrule=0pt,frame hidden,
% borderline west={1.5mm}{-1mm}{green}]}
% {\end{tcolorbox}}

\usepackage[user,savepos]{zref}
\usepackage{xifthen}

\newlength{\deftop}
\newlength{\defbot}
\newlength{\defsep}
\newcounter{def}

\newenvironment{testbox}
{\vspace{-1mm}\begin{tcolorbox}[blank,breakable]
\stepcounter{def}
\zsavepos{def\arabic{def}1}
\setlength{\global\deftop}{
  \dimexpr\paperheight - \zposy{def\arabic{def}1} sp}
\setlength{\global\defsep}{\deftop-\defbot}
\ifthenelse{\lengthtest{\defsep > -0.1mm}}
  {\ifthenelse{\lengthtest{\defsep < 3mm}}{\vspace{-4mm}}{}}
  {}\par
  % This should be a dynamical quantity,
  % adjusting according to the page's situation
\begin{tcolorbox}
[enhanced jigsaw,pad at break*=1mm,breakable,
colback=orange!10!white,boxrule=0pt,frame hidden,
borderline west={1.5mm}{-1mm}{green}]}
{\end{tcolorbox}
\zsavepos{def\arabic{def}2}
\setlength{\global\defbot}{
  \dimexpr\paperheight - \zposy{def\arabic{def}2} sp}
\end{tcolorbox}\vspace{-1mm}}

\begin{document}
    Some texts.
    \begin{testbox}box 1\end{testbox}
    Some texts.
    \begin{testbox}box 2\end{testbox}%\vspace{-4mm}
    \begin{testbox}box 3\end{testbox}
\end{document}

It uses zref to record the position of each box so as to check whether two boxes are adjacent. However this solution does not always work. This code doesn't check if two boxes are on the same page (I did write a version that tries to check the page number issue, but since page number checking in LaTeX doesn't work pretty well on the edge of the page, I disgarded it). Also, the 4mm is not a good idea because sometimes when a page is very loose, \vspace{4mm} is not enough to connect them.

I wish to know if you have a better and prettier way to achieve this effect.

2
  • Why do you need two independent colorboxes for box2 and box3? One tcolorbox with box2\\ box3 could work.
    – Ignasi
    Aug 24, 2019 at 9:36
  • @Ignasi Actually I'm writing a documentclass and this is part of my definition environment. When there're adjacent definitions I want them to be connected. If put in one box, the appearance of those definitions would be different.
    – Jinwen
    Aug 24, 2019 at 9:44

1 Answer 1

3
+50

Here's an approach using my new favourite tool in expl3's arsenal: \peek_analysis_map_inline:n. It allows you to scan the input stream token by token, letting you act on each one as required. The \ScanEnv command:

\ScanEnv [*] {<env>} {<true>} {<false>}

looks ahead in the token stream, ignoring spaces (and if * is given, ignoring \par), looking for \begin{<env>}. If that is found, it inserts the code <true> before the ignored spaces and \par. If the environment is not found, the <false> code is inserted instead.

With that, you can do what you want with

\ScanEnv* {definition} {\vspace{-\baselineskip}} {}

plus a \AfterEnvEnd to be able to use the end environment hook to insert the code. If you want to scan multiple environments, you need nested calls:

\ScanEnv* {definition} {\vspace{-\baselineskip}}
  {\ScanEnv* {definition*} {\vspace{-\baselineskip}} {}}

enter image description here

Here's the code:

\RequirePackage{xparse}
\ExplSyntaxOn
\makeatletter
\NewDocumentCommand \AfterEnvEnd { +m }
  { \jinwen_after_env_end:nw {#1} }
\cs_new_protected:Npn \jinwen_after_env_end:nw #1 #2
       \if@ignore\@ignorefalse\ignorespaces\fi
  { #2 \if@ignore\@ignorefalse\ignorespaces\fi #1 }
\makeatother
\NewDocumentCommand \ScanEnv { s m +m+m }
  {
    \IfBooleanTF {#1}
      { \jinwen_scan_env_ignore_par:nTF }
      { \jinwen_scan_env:nTF }
          {#2} {#3} {#4}
  }
\cs_new_protected:Npn \jinwen_scan_env:nTF
  { \__jinwen_scan_env:NnTF \c_false_bool }
\cs_new_protected:Npn \jinwen_scan_env_ignore_par:nTF
  { \__jinwen_scan_env:NnTF \c_true_bool }
\tl_new:N \l__jinwen_collected_tl
\cs_new_protected:Npn \__jinwen_scan_env:NnTF #1 #2 #3 #4
  {
    \tl_clear:N \l__jinwen_collected_tl
    \peek_analysis_map_inline:n
      {
        \tl_put_right:Nn \l__jinwen_collected_tl {##1}
        \int_compare:nNnTF { "##3 } = { 0 }
          {
            \exp_args:No \token_if_eq_meaning:NNTF {##1} \begin
              { \peek_analysis_map_break:n { \__jinwen_chk_env:nTFn {#2} {#3} {#4} } }
              {
                \bool_lazy_and:nnF {#1}
                    { \exp_args:No \token_if_eq_meaning_p:NN {##1} \par }
                  { \__jinwen_scan_env_end:n {#4} }
              }
          }
          { \int_compare:nNnF { "##3 } = { 10 } { \__jinwen_scan_env_end:n {#4} } }
      }
  }
\cs_new_protected:Npn \__jinwen_scan_env_end:n #1
  { \peek_analysis_map_break:n { \__jinwen_reinsert_tokens:nn {#1} { } } }
\cs_new_protected:Npn \__jinwen_reinsert_tokens:nn #1 #2
  {
    \use:x
      {
        \tl_clear:N \exp_not:N \l__jinwen_collected_tl
        \exp_not:n {#1} \l__jinwen_collected_tl #2
      }
  }
\cs_new_protected:Npn \__jinwen_chk_env:nTFn #1 #2 #3 #4
  {
    \exp_args:Nx \__jinwen_reinsert_tokens:nn
      { \str_if_eq:nnTF {#1} {#4} { \exp_not:n {#2} } { \exp_not:n {#3} } } { {#4} }
  }
\ExplSyntaxOff

\documentclass{article}
\usepackage[many]{tcolorbox}
\usepackage{blindtext}

\newtcolorbox{definition}{enhanced jigsaw,pad at break*=1mm,breakable,
left=4mm,right=4mm,top=1mm,bottom=1mm,
% beforeafter skip balanced=0pt,
colback=orange!10,boxrule=0pt,frame hidden,
borderline west={1.5mm}{-1mm}{green!50!black},arc=.7mm}

\newtcolorbox{definition*}{enhanced jigsaw,pad at break*=1mm,breakable,
left=4mm,right=4mm,top=1mm,bottom=1mm,
% beforeafter skip balanced=0pt,
colback=red!10,boxrule=0pt,frame hidden,
borderline west={1.5mm}{-1mm}{green!50!black},arc=.7mm}

\def\scandefinitionenv{%
  \AfterEnvEnd{%
    \ScanEnv*{definition}%
      {\vspace{-\baselineskip}}%
      {\ScanEnv*{definition*}%
        {\vspace{-\baselineskip}}%
        {}}}}

\AddToHook{env/definition/end}{\scandefinitionenv}%
\AddToHook{env/definition*/end}{\scandefinitionenv}%

\begin{document}

\begin{definition}
  This is followed by text
\end{definition}

something else

\begin{definition}
  This is followed by another definition
\end{definition}

\begin{definition*}
  See, no gap :)
\end{definition*}

\begin{quote}
  some other environment
\end{quote}

\end{document}
2
  • Thank you for this! I have one more question: if I have definition and definition*, is it possible to make them identical in the scan? I tried to add \ScanEnv*{definition*} after definition, \ScanEnv*{definition} and \ScanEnv*{definition*} after definition*, but then \begin{definition} or \begin{definition*} after end{definition*} doesn't get scan.
    – Jinwen
    Mar 13, 2021 at 0:10
  • @Jinwen Your approach (if I understood it right) was correct, but my implementation left some debris in the way and the scanning stopped too early. Should work now Mar 13, 2021 at 1:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.