highlighter
Literates Programmieren mit noweb
Syntax-Highlighting von Code-Chunks mit highlighter
Einsatz des listings-Pakets mit noweb
Das LaTeX-Paket listings unterstützt die Hervorhebung der Syntax vieler gängiger Programmiersprachen. highlighter bearbeitet den von noweb erzeugten LaTeX-Code so, dass an Stelle des normalen Code-Satzes das listings-Paket benutzt wird.
Über eine Pipeline wird highlighter mit dem noweave-Aufruf verbunden. Soll beispielweise ein noweb-Dokument mit eingebettetem Python-Code verarbeitet werden, führt man die Ausgabe von noweave auf die Standardeingabe des highlighter-Programms. Mit der Option -l gibt man die in den Code-Chunks eingesetzte Programmiersprache an:
noweave -latex pyfile.nw | highlighter -l Python > pyfile.tex
Welche Sprachen von listings unterstützt werden, kann man in der listings-Dokumentation nachlesen.
Um highlighter einsetzen zu können, müssen neben einer LaTeX-Installation die Pakete color, marvosym und natürlich listings verfügbar sein. Darüber hinaus ist eine Python-Laufzeitumgebung notwendig, da der highlighter in Python programiert ist. Python kann für nahezu alle Plattformen von http://www.python.org/ kostenlos bezogen werden.
Evtl. mit py2app und py2exe für Mac und Win ein Binary erstellen.
Implementierung
highlighters Arbeitsweise ist mit einem Filter vergleichbar. Das Programm liest die Daten zeilenweise von der Standard-Einabe und verarbeitet sie. Zunächst wird nach der Zeile \begin{document} gesucht und davor das listings-Paket sowie einige Formatierungsoptionen eingefügt. Anschließend wird jeder Code-Chunk in eine lstlisting-Umgebung eingefasst.
Die fertigen Zeilen speichert das Programm in einem Puffer und schreibt ihn nach Abschluss der Verarbeitung auf die Standardausgabe. Bei Bedarf können Statusausgaben auf die Standard-Fehlerausgabe geschrieben werden.
Die Klasse Highlighter
Die Klasse Highlighter implementiert die Suche, Verarbeitung der Optionen und Eingabe sowie die Pufferung der fertigen Zeilen.
<class Highlighter>=
class Highlighter(object):
"""Use listings package to highlight code chunks."""
Verarbeitung der Kommandozeilen-Optionen
Optionen können in Python recht komfortabel mit Hilfe des Moduls optparse verarbeitet werden. Die Methode parseOptions nutzt dieses Modul und definiert alle benötigten Optionen. Für eine einfache Online-Hilfe übergibt man zunächst eine Zeile mit einem Beispielaufruf sowie Programmname und -version an die Klasse OptionParser:
<parse options>=
def parseOptions(self):
usage = "usage: %prog [options]"
parser = optparse.OptionParser(usage=usage, version="%s %s" %
(__program_name__, __version__))
Anschließend werden sämtliche Optionen mit Hilfe der Methode add_option zum Parser hinzugefügt. Folgende Optionen kennt highlighter:
| /-l <lang>| –language=<lang>/ | Programmiersprache |
| /-e <encoding>| –encoding=<encoding>/ | Kodierung der Eingabe |
| /-v | –verbose/ | Statusinformationen ausgeben |
Kommandozeilen-Optionen.
<parse options>+=
parser.add_option("-l", "--language", metavar="LANG",
dest="language", default="Python", help="programming language")
parser.add_option("-e", "--encoding", metavar="ENCODING", dest="encoding",
default="latin_1", help="input encoding")
parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
default=False, help="print status messages")
Die Methode parse_args des Parser-Objekts verarbeitet die auf der Kommandzeile angegebenen Optionen und gibt sie zusammen mit übergebenen Argumenten als Paar (options, args) zurück. Die bei der Definition der Optionen angegebenen Speicher wie dest="language" sind auf dieselbe Art wie Methoden erreichbar. So lassen sich die privaten Variablen der Highlighter-Klasse definieren.
<parse options>+=
(options, args) = parser.parse_args()
self.__verbose = options.verbose
self.__language = options.language
self.__encoding = options.encoding
Der Konstruktor
Der Konstruktor bereitet den Code- und den Dokument-Puffer vor, erzeugt die Objekte für die Suche nach regulären Ausdrücken und setzt das Flag für den Dokumentbeginn auf False
.
<constructor>=
def __init__(self):
# default mode is documentation
self.__codeMode = False
self.__outputBuffer = StringIO()
self.__codeBuffer = StringIO()
self.__beginDocumentFound = False
Eine besondere Eigenheit ist, dass ein Backslash in einem regulären Ausdruck nicht nur einfach, sondern doppelt ausgezeichnet werden muss. D. h. einmal, um nicht als Sonderzeichen in einem String aufzutauchen, und dann, um als Backslash in einem regulären Ausdruck erkannt zu werden. Geschweifte Klammern müssen nur einfach ausgezeichnet werden. (Das hier mehr als vier Backslashs auftauchen, liegt an der Formatierung mit noweb und highlighter.)
__beginCode und __endCode werden benötigt, um Code-Chunks erkennen zu können; __codeName und __rBracket extrahieren gemeinsam den Chunk-Namen. Handelt es sich um die Fortsetzung eines Code-Chunks, wird dies von __plusSign erkannt.
<constructor>+=
# patterns
self.__beginDoc = re.compile("^\\\\begin\{document\}")
self.__beginCode = re.compile("\\\\nwbegincode\{\d+\}")
self.__endCode = re.compile("\\\\nwendcode\{\}")
self.__refLine = re.compile("\\\\LA\{\}")
self.__codeName = re.compile("\\\\moddef\{")
self.__rBracket = re.compile("\}")
self.__plusSign = re.compile("\\\\plusendmoddef")
Die run-Methode
In der Methode run steht die Hauptschleife, in der jede Zeile der Eingabe verarbeitet wird. Als Parameter wird der Filedescriptor der Standardeingabe übergeben.
Um etwaigen Kodierungsproblemen zu entgehen, muss die Zeile zuerst entsprechend der Angabe auf der Kommandozeile in das interne Stringformat von Python dekodiert werden. Kommt es dabei zu einem Fehler (möglicherweise wurde eine unbekannte Kodierung angegeben), wird eine Fehlermeldung ausgegeben und das Programm abgebrochen.
<run method>=
def run(self, fd):
"""Read lines from file descriptor"""
self.__message("Running highlighter...")
for raw_line in fd:
try:
line = raw_line.decode(self.__encoding)
except LookupError:
sys.stderr.write("%s: Unknown encoding\n" % self.__encoding)
sys.stderr.flush()
sys.exit(1)
Die dekodierte Zeile wird dann mit Hilfe der kompilierten regulären Ausdrücke wird durchsucht und abhängig vom gefundenen Zeilentyp eine bestimmte Methde aufgerufen.
<run method>+=
# check mode
if not self.__beginDocumentFound and self.__beginDoc.search(line):
self.__beginDocument(line)
elif self.__beginCode.search(line):
self.__setCodeMode(line)
elif self.__endCode.search(line):
self.__setDocMode(line)
elif self.__refLine.search(line):
self.__refChunkLine(line)
Wird keiner der regulären Ausdrücke gefunden, handelt es sich um eine für highlighter nicht relevante Zeile. Sie wird in einem Puffer abgelegt.
<run method>+=
else:
# collect all lines in output buffer
self.__collect(line)
Sind alle Zeilen der Standardeingabe verarbeitet, müssen sie wieder in die ursprüngliche Kodierung gebracht werden. Dazu wird der Inhalt des internen Ausgabepuffers ausgelesen, kodiert und das Ergebnis zurückgegeben.
<run method>+=
# remove escaped code chunks
buffer = self.__outputBuffer.getvalue().encode(self.__encoding)
return buffer
Die öffentliche Schnittstelle der Highlighter-Klasse umfasst nun :
<class Highlighter>+=
<constructor>
<parse options>
<run method>
Die privaten Methoden der Highlighter-Klasse
Wird in der Hauptschleife der Dokumentbeginn des LaTeX-Dokuments gefunden, fügt die Methode __beginDocument die für die Verwendung des listings-Pakets notwendigen Angaben ein.
<beginDocument method>=
def __beginDocument(self, line):
tmp = r"""\usepackage{marvosym}
\definecolor{mblue}{rgb}{0,0,0.5}
\definecolor{mgreen}{rgb}{0,0.5,0}
\definecolor{mgray}{gray}{0.25}
\usepackage{listings}
\lstloadlanguages{%s}
\lstset{language=%s,
extendedchars=true
breaklines=true,
breakatwhitespace=true,
prebreak=\scriptsize\Righttorque,
postbreak=\scriptsize\Lefttorque,
basicstyle=\small\ttfamily,
identifierstyle=\color{mgray},
commentstyle=\color[rgb]{0,0.5,0},
keywordstyle=\color[rgb]{0,0.2,0.5}\textbf,
stringstyle=\color[rgb]{0.5,0,0}\textbf,
frame=single,
tabsize=2,
columns=[l]flexible,
tab=\rightarrow,
xleftmargin=10pt,
showtabs=true,
morecomment=[s][\color{mgreen}]{\"\"\"}{\"\"\"},
morecomment=[s][\color{mblue}\textit]{<<}{\>\>}}""" % \
(self.__language, self.__language)
self.__outputBuffer.write(tmp)
self.__outputBuffer.write(line)
Um weitere Vorkommen von Dokumentanfängen auszuschließen, wird das Flag __beginDocumentFound gesetzt.
<beginDocument method>+=
self.__beginDocumentFound = True
Wird der Beginn eines Code-Chunks gefunden, ruft die Hauptschleife die Methode __setCodeMode auf. Diese sucht nach dem Chunk-Namen und schreibt ihn formatiert in den internen Ausgabepuffer. Handelt es sich um die Fortsetzung eines schon vorhandenen Code-Chunks, wird ein Plus-Zeichen hinzugefügt. Wird wider Erwarten kein Name gefunden, landet die Zeile unbearbeitet im internen Ausgabepuffer. Zuletzt wird das Flag __codeMode gesetzt.
<setCodeMode method>=
def __setCodeMode(self, line):
self.__message("> Set code mode")
# search for code chunk name
match = self.__codeName.search(line)
if match:
self.__outputBuffer.write("\n\\textlangle")
startPos = match.end()
line = line[startPos:]
# look for closing }
endPos = self.__rBracket.search(line).end()
# replace unallowed chars for latex before writing
self.__outputBuffer.write("\\textit{%s}" %
line[0:endPos-1].replace("_", "{\\_}"))
self.__outputBuffer.write("\\textrangle")
# + needed if \plusendmoddef
plusMatch = self.__plusSign.search(line)
if plusMatch:
self.__outputBuffer.write("+")
self.__outputBuffer.write("$\equiv$\n")
else:
self.__outputBuffer.write(line)
self.__codeMode = True
Wird der Beginn eines Dokumentations-Chunks gefunden, ruft die Hauptschleife die Methode __setDocMode auf. Der Dokumentations-Chunk schließt den vorherigen Code-Chunk ab und führt zum Aufruf der Methode __highlight. Diese bearbeitet den abgeschlossenen Code-Chunk für den Einsatz des listings-Pakets. Anschließend wird der Code-Puffer zurückgesetzt und das __codeMode-Flag deaktiviert.
<setDocMode method>=
def __setDocMode(self, line):
self.__message("> Set doc mode")
self.__highlight()
# clean code buffer
self.__codeBuffer = StringIO()
self.__codeMode = False
# end
Da in Code-Chunks Referenzen auf andere Code-Chunks auftauchen können, müssen die von noweb dort eingefügten LaTeX-Befehle durch normale Zeichen ersetzt werden. Dies übernimmt die Methode __refChunkLine.
<refChunkLine method>=
def __refChunkLine(self, line):
self.__message("> Got single doc line")
line = re.sub(r"\\LA{}", "<<", line)
line = re.sub(r"\\RA{}", ">>", line)
self.__codeBuffer.write(line)
# end
Normale Zeilen werden durch die Methode __collect eingesammelt und abhängig vom __codeMode auf die internen Puffer verteilt. Ist es aktiv, landen alle Zeilen im internen Code-Puffer, andernfalls im Ausgabepuffer.
noweave erzeugt in Code-Chunks für geschweifte Klammern die Zeichen \{ und \}, die jeweils durch die einfachen geschweiften Klammern ersetzt werden müssen.
<collect method>=
def __collect(self, line):
self.__message(" %s" % line)
if self.__codeMode:
line = line.replace("\{", "{").replace("\}", "}").replace('\\\\', '\\')
self.__codeBuffer.write(line)
else:
self.__outputBuffer.write(line)
# fi
# end
Die Methode __highlight integriert den internen Code-Puffer mit der listings-Umgebung und schreibt das Ergebnis in den Ausgabe-Puffer.
<highlight method>=
def __highlight(self):
self.__message("> Set lstlisting and code")
self.__outputBuffer.write("\\begin{lstlisting}\n")
self.__outputBuffer.write(self.__codeBuffer.getvalue())
self.__outputBuffer.write("\\end{lstlisting}\n")
# end
Die Methode __message liefert bei aktivierter verbose-Option Statusmeldungen auf die Standard-Fehlerausgabe. Die Nachricht wird entsprechend der auf der Kommandozeile angegeben Kodierung ausgegeben.
<message method>=
def __message(self, message):
if self.__verbose:
msg = message.encode(self.__encoding)
sys.stderr.write(msg.strip())
sys.stderr.write("\n")
sys.stderr.flush()
# fi
# end
Die privaten Methoden müssen nun noch in die Highlighter-Klasse eingefügt werden.
<class Highlighter>+=
<beginDocument method>
<setCodeMode method>
<setDocMode method>
<refChunkLine method>
<collect method>
<highlight method>
<message method>
Das gesamte Programm
Das gesamte Programm inkl. Modul-Import und Versionierung sieht nun folgendermaßen aus:
<highlighter>=
#!/usr/bin/env python
# -*- coding: iso8859-1 *-*
"""highlighter
"""
import optparse
import os
import re
import sys
import time
from StringIO import StringIO
__program_name__ = "highlighter"
__author__ = "$Author: fuller $"
__version__ = 0
# Source: $Id: highlighter.nw 26 2008-11-12 09:56:35Z fuller $
<class Highlighter>
def main(args):
highlighter = Highlighter()
highlighter.parseOptions()
sys.stdout.write(highlighter.run(sys.stdin))
if __name__ == "__main__":
main(sys.argv)
Download
Voraussetzung: Python version 2.x
highlighter rev 27 (6.2K)