# Comment (automatically) a LaTeX source file to make it more readable

I am looking for a simple way to do the following :

Input :

\begin{document}
...
\section{}
...
\subsection{}
...
\subsubsection{}
...
\section{}
...
\end{document}


Output :

\begin{document}
...
%1
\section{}
...
%1.1
\subsection{}
...
%1.1.1
\subsubsection{}
...
%2
\section{}
...
\end{document}


The purpose of this is to make long documents more readable. I want to always know where I am in the whole plan of the document.

• I'll copy a part of another one of my comments for better visibility: Adding these comments introduces information that can easily become outdated when you reorder parts of the document, so you would have to be careful to keep the comments updated. Instead, you might want to look for an editor that supports displaying an outline of the document you're working on. (I don't know any off the top of my head, though.)
– n.st
Mar 26 '14 at 18:17
• @bela83 This has nothing to do with TeX, it doesn't belong on tex.SE. It's a text processing question. It's like asking a question about how to crop a picture of a car and wanting to move it to mechanics.SE Apr 8 '14 at 14:22
• @MichaelMrozek I thought so at first ! But in fact, the end of the question says "to make it more readable", so that I guess that I could get good advice from the people on tex.SE to achieve this, maybe in another way. Still you are right that the Q is about text processing... Apr 8 '14 at 20:00
• @bela83 Well, I checked with them in chat and they don't seem to think it's on-topic; maybe if you come up with something more TeX-specific Apr 8 '14 at 20:40
• @MichaelMrozek Thanks. I understand. Maybe it's one of those issues where I didn't emphasize enough the goal I am trying to achieve, and rather I talked of the solution I had come up with... Apr 12 '14 at 11:06

I have a quick solution which is useful a lot of times. Notice that is for personal use only(1); it could be refined, added in-place editing, error control, whatever. But I think it's useful as is too. The idea is leveraging LaTeX numbering itself.

So, first of all, you need to add labels to your document (which is good nonetheless):

\documentclass[12pt]{article}
\begin{document}
\section{a}
\label{sec:a}

\section{b}
\label{sec:b}

\subsection{b a}
\label{sec:ba}

\newpage

\subsection{b b}
\label{sec:bb}

\section{c}
\label{sec:c}
\end{document}


Next run latex as ever, suppose it is called walla.tex. Now you run this little python script:

#!/usr/bin/env python
#
#
import sys
import re

labels=[]
# build a list of label
for l in open(sys.argv[1] + ".aux"):
if l.find("newlabel{") != -1:
m = re.search(r'\\newlabel{(.*?)}{{(.*?)}{(.*?)}}', l)
if m:
labels.append("label: %s will be number: %s at page: %s" % (
m.group(1), m.group(2), m.group(3)))
else:
labels.append(l)

# scan input file
for l in  open(sys.argv[1] + ".tex"):
if l.find("\\label") != -1:
# we have a label, try to match it
m = re.search(r'\\label{(.*?)}', l)
# if not m: continue ERROR not managed here
key = m.group(1)
for lab in labels:
if lab.find(key) != -1:
# modify this to pretty print
sys.stdout.write("%%%%%% %s\n" % lab.strip())
break
# output the tex file avoiding old ones
if not l.startswith(r'%%% label'):
sys.stdout.write(l)


Call it find_tex_labels, make it executable, and run it as find_tex_labels walla > walla_annotated.tex (notice, no extensions in the arguments).

You'll have your annotated LaTeX file in output:

\documentclass[12pt]{article}
\begin{document}
\section{a}
%%% label: sec:a will be number: 1 at page: 1
\label{sec:a}

\section{b}
%%% label: sec:b will be number: 2 at page: 1
\label{sec:b}

\subsection{b a}
%%% label: sec:ba will be number: 2.1 at page: 1
\label{sec:ba}

\newpage

\subsection{b b}
%%% label: sec:bb will be number: 2.2 at page: 2
\label{sec:bb}

\section{c}
%%% label: sec:c will be number: 3 at page: 2
\label{sec:c}
\end{document}


...this will work for all the labels. I find it quite useful for cross-referencing equation etc when I am editing on a device that has no LaTeX in it. You can now substitute you original walla.tex for the new one.

It's your responsibility to keep the things in sync... and not using "%%% label" comments anywhere.

Footnotes:

(1) I promise the refine it a lot of times. Then given that I'm the only one using it, I correct errors if and when they come out... and never find time to clean it up.

The relatively difficult part is that you have to buffer a commented line to see if it needs updating in case the next line is a section indicator. It would be more simple if that data was on the same or next line.

The following should help you out. It can be invoked as python script.py input output or you can leave out the output and it writes to stdout. Don't do python script.py xx.tex xx.tex', but write to a temporary file and copy that one back to the original.

This updates existing lines of the form %x.y.z rest of comment leaving rest of comment untouched. If there is no such comment yet, it insert it. The special comments should start at the beginning of the line, as should the sectioning commands.

import sys

class ProcessLaTeX:
def __init__(self, ifp, ofp):
self.ofp = ofp
self.prev_comment = None
self.level = []
for line in ifp:
self.process(line)
# emit last line if comment
if self.prev_comment:
self.ofp.write(self.prev_comment)

def output(self, line):
pass

def process(self, line):
if line[0] == '%':
# store comment line, emitting any previously stored line
if self.prev_comment:
self.ofp.write(self.prev_comment)
self.prev_comment = line
return
lvl = self.check_level(line)
if lvl > -1:
self.output_level_comment(lvl)
if self.prev_comment:
self.ofp.write(self.prev_comment)
self.prev_comment = None
self.ofp.write(line)

def output_level_comment(self, lvl):
if self.prev_comment: # check if we overwrite an old one
# do not use the starting '%' and final newline
words = self.prev_comment[1:-1].split(' ', 1)
for c in words[0]:
if c not in '01234567890.':
self.ofp.write(self.prev_comment)
self.prev_comment = None
break
self.level.append(0) # in case this is a deeper level
self.level[lvl] += 1
self.level = self.level[:lvl+1] # cut of excess levels
lvls = '%' + '.'.join([str(l) for l in self.level])
if self.prev_comment: # overwrite the previous words[1]
words[0] = lvls
outs = ' '.join(words)
if not outs[-1] == '\n':
outs += '\n'
self.prev_comment = None
else:
outs = lvls + '\n'
self.ofp.write(outs)

def check_level(self, line):
if line and not line[0] == '\\':
return -1
cmd = line[1:].split('{', 1)[0]
try:
res = ['section', 'subsection', 'subsubsection',
'paragraph', 'subparagraph'].index(cmd)
except ValueError:
return -1
return res

out = sys.stdout if len(sys.argv) < 3 else open(sys.argv[2], 'w')
pl = ProcessLaTeX(open(sys.argv[1]), out)


What you are looking for, as I think, is nl's section delimiter option. From info nl:

• nl decomposes its input into (logical) pages; by default, the line number is reset to 1 at the top of each logical page. nl treats all of the input files as a single document; it does not reset line numbers or logical pages between files.

• A logical page consists of three sections: header, body, and footer. Any of the sections can be empty. Each can be numbered in a different style from the others.

• The beginnings of the sections of logical pages are indicated in the input file by a line containing exactly one of these delimiter strings:

• \:\:\: - start of header;
• \:\: - start of body;
• \: - start of footer.

You can set nl's logical page -delimiter on the command line like:

nl -dCC <infile


...where CC represent any two characters to replace the \: as indicated in the documentation. Given your input, I don't think that's necessary - we can just insert the defaults where applicable with a little bit of input filtering. Here's nl and sed paired up in a shell function I wrote designed to filter itself recursively:

sd() { n='
';     nl -bp"^\\\\$1section" -w1 -s"$n\:\:\:$n" | sed '/./!d;/^[0-9]/!s/^[[:blank:]]*//;/^%[0-9.]*$/h;t
s/./%&/;x;/%/G;s//./2;/\n.*/h;s///;x;s/\n//;N
s/$$\(.*$$$$\n$$\)$$\(.*$$$$..$$\)/\4\3\1\5/'
}


I fed it something like your example data and piped its output back into it a few times:

sd <<\IN |sd sub | sd subsub | sd subsubsub
\begin{document}
\section{}
some ordinary lines
\subsection{}
whatever
\subsubsection{}
\subsection{}
\subsubsection{}
\subsubsubsection{}
\section{}
\subsection{}
\end{document}
IN


Run just as above it prints:

\begin{document}
%1
\section{}
some ordinary lines
%1.1
\subsection{}
whatever
%1.1.1
\subsubsection{}
%1.2
\subsection{}
%1.2.1
\subsubsection{}

\:\:\:
%1.2.1.1
\:\:
\subsubsubsection{}
%2
\section{}
%2.1
\subsection{}
\end{document}


As you can see, the filter job is not completely finished, but it appears to do the job. nl numbers the -body of its input based on the -bp'attern' it is fed - and it starts its count over for each logical page - as delimited by a line comprised only of its logical page header delimiter \:\:\:.

So... sed filters its output - which already includes the delimiter as set in nl's -separator arg and basically sed just rearranges it a little so nl will find its section delimiter where it should on the next pass. sed does also keep a copy of the last ^%[0-9.]*$ line in its hold space though - and if the hold space is not empty when it encounters a line beginning with a number it appends that line to the contents of its hold space following a .. And that's the meat and potatoes, really. Still - as I say, it's not done. The last pass left the section delimiters and blank lines in the output. So, to clean that up: sd <<\IN |sd sub | sd subsub | sd subsubsub | grep -v '^\\:\|^$'
\begin{document}
\section{}
some ordinary lines
\subsection{}
whatever
\subsubsection{}
\subsection{}
\subsubsection{}
\subsubsubsection{}
\subsubsection{}
\subsubsubsection{}
\section{}
\subsection{}
\end{document}
IN


### OUTPUT:

\begin{document}
%1
\section{}
some ordinary lines
%1.1
\subsection{}
whatever
%1.1.1
\subsubsection{}
%1.2
\subsection{}
%1.2.1
\subsubsection{}
%1.2.1.1
\subsubsubsection{}
%1.2.2
\subsubsection{}
%1.2.2.1
\subsubsubsection{}
%2
\section{}
%2.1
\subsection{}
\end{document}
`