[Tex/LaTex] Showing nice indentation of Python code

fancyvrbpygmentspythontex

I use pythontex to typeset my Python listings. I'd like to have some kind of L-shaped lines, like the ones I added in the image below to clearly exhibit the indentation level of every line of code.

Any ideas?

enter image description here

\documentclass[varwidth,margin = 1mm]{standalone}
\usepackage{pythontex}
\usepackage[T1]{fontenc}
\usepackage{lmodern}
\begin{document}
\begin{pygments}[frame=single]{python}
def f(n):
  if n == 1:
    return 1
  else:
    return f(n-1)

print(f(4))
\end{pygments}
\end{document}

Best Answer

Thank you for very interesting problem.

You can use following macros:

\documentclass[varwidth,margin = 1mm]{standalone}
\usepackage{pythontex}
\usepackage[T1]{fontenc}
\usepackage{lmodern}

\def\makeL{\vbox\bgroup \global\linenum=0 
   \let\FancyVerbFormatLine=\Lformat \let\next=}
\def\sxdef#1{\expandafter\xdef\csname#1\endcsname}

\newcount\spacenum  \newcount\linenum  \newcount\linenumA  \newcount\tmpnum
\def\Lformat#1{\global\spacenum=0
   \ifx\end#1\end\else \LformatA#1\end\fi
   \global\advance\linenum by1
   \sxdef{L:\the\spacenum}{\the\linenum}%
   \tmpnum=\spacenum \def\Llist{}\let\Ldraw=\relax
   \loop
      \advance\tmpnum by-1
      \ifnum\tmpnum>-1
         \expandafter\ifx\csname L:\the\tmpnum\endcsname \relax \else
            \advance\spacenum by-\tmpnum
            \linenumA=\linenum  
            \advance\linenumA by-\csname L:\the\tmpnum\endcsname
            \edef\Llist{\Ldraw{\the\spacenum}{\the\linenumA}\Llist}%
            \spacenum=\tmpnum
         \fi
   \repeat
   \let\Ldraw=\LdrawX
   \csname FV@ObeyTabs\endcsname{\rlap{\Llist}#1}}
\def\LformatA#1{%
   \ifx#1\fspace \global\advance\spacenum by1 \let\next=\LformatA
   \else \let\next=\formatlineB \fi
   \ifx#1\end \let\next=\relax \fi
   \next
}
\def\formatlineB#1\end{}
\expandafter\def\expandafter\fspace\expandafter{\csname FV@Space\endcsname}

\def\LdrawX#1#2{%
   \dimen0=.5em \dimen0=#1\dimen0 \advance\dimen0 by-.4em  
   \dimen1=\normalbaselineskip \dimen1=#2\dimen1 \advance\dimen1 by-1ex
   \message{(#1,#2)=(\the\dimen0,\the\dimen1,\the\normalbaselineskip)}%
   \pdfliteral{q 1 g .5 G .9963 0 0 .9963 2 2 cm 
      0 0 \ptdim0 \ptdim1 re f \ptdim0 0 m 0 0 l 0 \ptdim1 l S Q}%
   \dimen0=.5em \kern#1\dimen0
}
{\lccode`\?=`\p \lccode`\!=`\t  \lowercase{\gdef\ignorept#1?!{#1}}}
\def\ptdim#1{\expandafter\ignorept\the\dimen#1 }

\begin{document}
\makeL{
\begin{pygments}[frame=single]{python}
class enum:
  def __init__(self,level=1):
    self.content=list()   
    self.level=level
  def __repr__(self):
    ret = "[["
    for x in self.parse():
      ret += repr(x)
    return ret+"]]"
  def parse(self):
    itemcounter = 0   
    subbullets=enum(self.level+1)
    parsed=list()
    for item in self.content:
      itemcounter += 1
      try:
        if item[0][0] in [PENITEM, ENITEM]:
      except IndexError:
        self.fail("foobar")
#ende
\end{pygments}
}
\end{document}

This means that you must write:

\makeL{
\begin{pygments}[frame=single]{python}
... your code
\end{pygments}
}

And you get:

L example

Of course, pygments environment needs to run pythontex example after TeXing in order to syntax highlighting.

Explanation The \makeL macro redefines \FancyVerbFormatLine used for verbatim output by pygments environment as \Lformat. This macro counts the line and counts the number of spaces in the front of the line and saves it as macro \L:num-spaces with the contents line-number. Then the list of L symbols is stored for current line into \Llist macro. Each L symbol is represented in the form \Ldraw{right}{up} where right is the width of L (the number of the columns) and up is the height of the L (the number of the lines). Finally, the L symbols (i. e. \Ldraw) are processed using \pdfiteral.

Limitation: this macro expects that no page breaks are in the middle of the code. But a skillful macro programmer can add such feature as an exercise.

Dependences: you need not any additional package. You can process the macros by pdflatex or lualatex or xelatex. When xelatex is used then you must define:

\def\pdfliteral#1{\special{pdf:literal #1}}

Edit The second version of my macros below (at the end of this post) simplifies the input and allows page breaks in the middle of the code.

The input can be prepared as normally in Verbatim (from fancyvrb) or pygments (from pythontex) environments. If the new key-value pair indent=L is present then the L indentations is calculated and printed else these environments work normally. Example:

\begin{pygments}[frame=single, indent=L]{python}
... your code
\end{pygments}

or

\begim{Verbatim}[frame=single, indent=L]
... your code
\end{pygments}

In order to allow page breaking we must decompose the large L (over more lines) to one or more Is ended by small L:

XXXXXXXX
I XXXXXX
I I XXXX
I L XXXX
L XXXXXX

Then we can print Is and Ls in each line separately and the code can be broken to more lines. There is only one little problem: we must know the indentation of the following line when printing current line in order to decide if L or I must be printed. Fortunately, the fancyverb.sty keeps current line in the box and it print this box after reading next line. So, we can calculate the indentation of the next line and print the box from previous line overlapped by Ls or Is. This work is done by \Lbox macro (instead original \box in the fancyvrb.sty macros). And \Lbox uses \Llist calculated from previous line where is a list of \Ldraw macros desribed above. And \Ldraw macro draws L or I simply by:

\def\Ldraw{\ifnum\tmpnum<\spacenum \expandafter\LdrawI \else \expandafter\LdrawL\fi}

where \tmpnum includes the current column number and \spacenum the number of spaces of the next line.

The example follows. My macros must be included after loading pythontex.sty or fancyvrb.sty because some internal macros from fancyvrb.sty are re-defined.

\documentclass{article}
\usepackage{pythontex}
\usepackage[T1]{fontenc}
\usepackage{lmodern}  

\def\sxdef#1{\expandafter\xdef\csname#1\endcsname}
\def\sdef#1{\expandafter\def\csname#1\endcsname}
{\lccode`\?=`\p \lccode`\!=`\t  \lowercase{\gdef\ignorept#1?!{#1}}}
\def\ptdim#1{\expandafter\ignorept\the\dimen#1 }

\newcount\spacenum  \newcount\linenum  \newcount\linenumA  \newcount\tmpnum

\def\Lcountspaces#1{\spacenum=0 \ifx\end#1\end\else \LcountspacesA#1\end\fi}
\def\LcountspacesA#1{%
   \ifx#1\fspace \advance\spacenum by1 \let\next=\LcountspacesA
   \else \let\next=\LcountspacesB \fi
   \ifx#1\end \let\next=\relax \fi
   \next
}
\def\LcountspacesB#1\end{}
\expandafter\def\expandafter\fspace\expandafter{\csname FV@Space\endcsname}

\def\Lbegin{\global\linenum=0 \gdef\Llist{}}

\def\Lformat#1{%
   \global\advance\linenum by1
   \sxdef{L:\the\spacenum}{\the\linenum}%
   \tmpnum=\spacenum \gdef\Llist{}\let\Ldraw=\relax
   \loop
      \advance\tmpnum by-1
      \ifnum\tmpnum>-1
         \expandafter\ifx\csname L:\the\tmpnum\endcsname \relax \else
            \advance\spacenum by-\tmpnum
            \linenumA=\linenum  
            \advance\linenumA by-\csname L:\the\tmpnum\endcsname
            \xdef\Llist{\Ldraw{\the\spacenum}{\the\linenumA}\Llist}%
            \spacenum=\tmpnum
         \fi
   \repeat  
   \csname FV@ObeyTabs\endcsname{#1}}

\def\Lbox#1{\hbox to\hsize{\rlap{\box#1}%
  \kern\csname @totalleftmargin\endcsname \kern\csname FV@XLeftMargin\endcsname
  \setbox0=\hbox{\csname FV@LeftListFrame\endcsname}\kern\wd0
  \tmpnum=0 \Llist\hss}}

\def\Ldraw{\ifnum\tmpnum<\spacenum \expandafter\LdrawI \else \expandafter\LdrawL\fi}
\def\LdrawL#1#2{%
   \dimen0=.5em \dimen0=#1\dimen0 \advance\dimen0 by-.4em
   \dimen1=1.4ex
   \dimen2=.5ex 
   \pdfliteral{q \Lshape\space .9963 0 0 .9963 \ptdim2 \ptdim2 cm
      \ptdim0 0 m 0 0 l 0 \ptdim1 l S Q}%
   \LdrawE{#1}}

\def\LdrawI#1#2{%
   \dimen0=-.85ex
   \dimen1=\normalbaselineskip
   \dimen2=.5ex
   \pdfliteral{q \Lshape\space .9963 0 0 .9963 \ptdim2 \ptdim0 cm
      0 0 m 0 \ptdim1 l S Q}%
   \LdrawE{#1}}

\def\LdrawE#1{\dimen0=.5em \kern#1\dimen0 \advance\tmpnum by#1 }

\csname define@key\endcsname{FV}{indent}{\csname FV@indent@#1\endcsname}
\sdef{FV@indent@L}{\let\FancyVerbFormatLine=\Lformat}

\def\Lshape{1 g .5 G 1 w 1 j 1 J} % .5 gray 1bp width rounded ends, rounded corner

%%%%%%% re-definition of internal macros from fancyvrb.sty
{\makeatletter
\gdef\FV@ListProcessLine@i#1{\Lcountspaces{#1}%  <--- added by P.O.
  \hbox{%
    \ifvoid\@labels\else
      \hbox to \z@{\kern\@totalleftmargin\box\@labels\hss}%
    \fi
    \FV@ListProcessLine{#1}}%
  \let\FV@ProcessLine\FV@ListProcessLine@ii}
\gdef\FV@ListProcessLine@ii#1{\Lcountspaces{#1}%  <--- added by P.O. 
  \setbox\@tempboxa=\FV@ListProcessLine{#1}%
  \let\FV@ProcessLine\FV@ListProcessLine@iii}
\gdef\FV@ListProcessLine@iii#1{\Lcountspaces{#1}% <--- added by P.O.
  {\advance\interlinepenalty\clubpenalty\penalty\interlinepenalty}%  
  \Lbox\@tempboxa                              % <--- Lbox by P.O.
  \setbox\@tempboxa=\FV@ListProcessLine{#1}% 
  \let\FV@ProcessLine\FV@ListProcessLine@iv}
\gdef\FV@ListProcessLine@iv#1{\Lcountspaces{#1}%  <--- added by P.O. 
  \penalty\interlinepenalty
  \Lbox\@tempboxa                              % <--- Lbox by P.O.
  \setbox\@tempboxa=\FV@ListProcessLine{#1}}%
\gdef\FV@ListProcessLastLine{\spacenum=0 %     % <--- added by P.O.  
  \ifx\FV@ProcessLine\FV@ListProcessLine@iv
    {\advance\interlinepenalty\widowpenalty\penalty\interlinepenalty}%
    \Lbox\@tempboxa                            % <--- Lbox by P.O.
  \else
    \ifx\FV@ProcessLine\FV@ListProcessLine@iii
      {\advance\interlinepenalty\widowpenalty
        \advance\interlinepenalty\clubpenalty
        \penalty\interlinepenalty}%
      \Lbox\@tempboxa                         % <--- Lbox by P.O.
    \else
      \ifx\FV@ProcessLine\FV@ListProcessLine@i
        \FV@Error{Empty verbatim environment}{}%
         \FV@ProcessLine{}%
      \fi
    \fi
  \fi}
\gdef\FVB@Verbatim{\Lbegin\FV@VerbatimBegin\FV@Scan} % <--- Lbegin added by P.O.
\gdef\@begin@pygments@hook{\Lbegin}
}
%%%%%%%%%%% end of re-definition

\begin{document}

Testing:

\begin{pygments}[frame=single, indent=L]{python}
def f(n):
  if n == 1:
    return 1
  else:
    return f(n-1)

print(f(4)) 
\end{pygments}

Next code:\vskip10cm

\begin{pygments}[frame=single,indent=L]{python}
class enum:
  def __init__(self,level=1):
    self.content=list()
    self.level=level
  def __repr__(self):
    ret = "[["
    for x in self.parse():
      ret += repr(x)
    return ret+"]]"  
  def parse(self):
    itemcounter = 0
    subbullets=enum(self.level+1)
    parsed=list()    
    for item in self.content:
      itemcounter += 1
      try:
        if item[0][0] in [PENITEM, ENITEM]:
      except IndexError:
        self.fail("foobar")
\end{pygments}

\end{document}

Because of the \Lshape macro (gray, roundend corners), the detail of the result looks like:

lcode2

Related Question