16

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}
  • You want this with pythontex or any code package is fine? – Alenanno Jun 11 '15 at 14:32
  • Well pythontex would be really nice as it is part of my workflow. In addition, a solution with pythontex could take advantage of Python to parse and place, say, tikz code. But any code package would be fine. – cjorssen Jun 11 '15 at 14:40
  • Ok. Yes it would be better with your package, but I was asking in case it wasn't possible with that one (I'm not saying it's not). :D – Alenanno Jun 11 '15 at 14:47
  • 1
    pythontex uses Pygments to do the highlighting. In principle, it would be possible to post-process the Pygments output based on indentation level, and add in the lines. However, that might get complicated, and might fail for expressions that break across multiple lines unless you were really careful. The "proper" way to do this would probably be in Pygments, but even there, I'm not sure if the (default) lexer has enough information to do this in the general case. – G. Poore Jun 12 '15 at 12:53
  • 2
    While this might be possible another way is just to follow PEP 8 and use 4 spaces per indentation level. Then no extra lines are necessary and it looks much closer to what the code looks like in an external editor. – Alexander Sep 4 '15 at 11:08
20
+500

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

  • +1 Thank you so much for taking the time to answer this. This is really an interesting approach since it is "generic". However, I was hoping for a solution that would survive page breaks (that's where my students sometimes forget how many indentations they should read). Would you be that skillful macro programmer? ;-) – cjorssen Sep 6 '15 at 18:52
  • @cjorssen OK, I'll try it. If I understand the rules here, I have about 5 days for this... – wipet Sep 6 '15 at 19:44
  • Perfect! Thank you so much. This could clearly be part of fancyvrb itself @Herbert. – cjorssen Sep 10 '15 at 10:28
  • When you have a minute, could you please explain the magic you use to actually draw the lines? I have some issues when margin get changed (say in an enumerate environment) that I'd like to be able to adjust. Thanks – cjorssen Nov 25 '15 at 12:10
  • @cjorssen Do you mean what \pdfliteral does? I don't fully understand your question. Please, sent me an email with more specification. It is better to me to use email than this forum for communication. – wipet Nov 25 '15 at 17:52
14

I know what I did here is cheating. Also I know that this is far from a final solution, as it currently still requires the code to be entered twice into the file. But this could for example be simplified by loading the code from an external file all along. Think of this as a concept/suggestion of approaching the issue differently.

What this code basically does is it replaces each leading double space with a "|_".The "_" part of the previous line gets erased (covered) when the next line prints it's "|_". Finally, the pygments

It might not be the most elegant or sophisticated way, but it saves you a lot of bother. Of course this is not the ultimate implementation The result looks like this

enter image description here

Code:

\documentclass{article}
\usepackage{luacode,luatexbase}
\usepackage{pythontex}
\usepackage{tikz}
%% Lua-side code
%this goes through the lines of the code and reads how many "doubles spaces are leading each line
\begin{luacode}
function readindent ( line ) 
    if not string.find(line, "drawindent") then
      indent = math.floor(string.len(string.match(line,"^%s*") or "") / 2)
      line = "~\\\\"..string.rep("\\pycodehook",indent)..""
    end
    return ( line )
end 
\end{luacode}
%% TeX-side code
\newcommand\ignore[1]{(#1)}
\newenvironment{drawindent}{%
  \directlua{luatexbase.add_to_callback("process_input_buffer",readindent, "readindent")}}{%
  \directlua{luatexbase.remove_from_callback("process_input_buffer","readindent")}}
\newcommand*\pycodehook{\rlap{\smash{~\tikz{                        %this draws a hook in each and every code line and erases hooks from the lines above
  \fill[fill=gray]  (-0.24pt,0pt) rectangle (1ex,1\baselineskip);
  \fill[fill=white]  (0.24pt,.5pt) rectangle (1ex,1.5\baselineskip);
  }}}\phantom{\ttfamily{~~}}}
\begin{document}\noindent
\begin{minipage}[t][0pt][t]{0pt}%
\begin{drawindent}
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{drawindent}
\end{minipage}%
\begin{minipage}[t]{\columnwidth}%
\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{minipage}
\end{document}

For Completeness: some of the code is taken from here: Treat environment content as String, replace substrings and interpret afterwards

  • 1
    +1 Thank you so much for taking the time to answer this. This is really an interesting approach. Unfortunately, luatex is not part of my workflow for that kind of job. I'm afraid I think I'll adopt wipet's way of doing it. But, again, thank you! – cjorssen Sep 6 '15 at 18:55
  • I think that is a good choice – sheß Sep 6 '15 at 19:03

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.