Von POD nach Quark (Linux-Magazin, April 2001)

Wie versprochen kommt heute der POD-Parser, der die gute alte Perl-Dokumentation in das vom Linux-Magazin für Artikel verwendete Quark-Express-Format verwandelt.

Um Artikel für's Linux-Magazin im von Perlprogrammierern bevorzugten Dokumentationsformat POD (Plain Old Documentation) zu schreiben, fügten wir letztes Mal zwei neue Befehle ins POD ein, nannten das Ergebnis flugs PND (Plain New Documentation) und schrieben einen Filter, der PND wieder nach POD wandelte.

Heute fügen wir nun der pod2xxx-Familie ein neues Mitglied hinzu: pod2quark wandelt das erweiterte POD in ein Format, das die Redakteure des Linux-Magazins problemlos in ihr Layoutsystem Quark-Express einspeisen können. Das Format zeichnet den Text mit speziellen Marken aus, die Überschriften, Listings, Abbildungen etc. kennzeichnen. Listing 1 zeigt einen in PND geschriebenen Testartikel, Listing 2 dessen Darstellung nach der Behandlung mit den Filtern pnd2pod und und dem heute vorgestellten pod2quark. Wie immer dienen die Zeilennummern nur der Illustration, die echten Dateien führen natürlich keine in sich.

Listing 1: beispiel.pnd

    01 =head1 Artikelüberschrift
    02 
    03 Ein wunderbarer Aufmacher.
    04 
    05 Der erste Textabschnitt befasst sich mit 
    06 dem wichtigen Perl-Code
    07 
    08     # Wie alles begann:
    09 
    10     perl -le 'print "Hello World!"'
    11 
    12 den jeder Perl-Programmierer im Schlaf 
    13 herbeten muss ([1] ist ein gutes Buch zum Thema). 
    14 Abbildung 1 illustriert die Einzelheiten.
    15 
    16 =figure fig/pod.gif Michael I<schläft>.
    17 
    18 =head2 Das Sonderzeichen C<E<lt>>
    19 
    20 Listing 1 zeigt, wie das Kleiner-Zeichen C<E<lt>>
    21 und der Backslash C<\> elegant maskiert werden.
    22 
    23 =include maskiere.pl listing
    24 
    25 Das war's für heute, bis zum nächsten Mal!
    26 
    27 =head2 Referenzen
    28 
    29 =over 4
    30 
    31 =item [1]
    32 
    33 "Hello World ohne Ballast", Bernd Brabbel, 
    34 Salbader-Verlag, 2001
    35 
    36 =back
    37 
    38 =include autorenkasten.pod

Listing 2: beispiel.qex

    01 @T:Artikelüberschrift
    02 
    03 @V:Ein wunderbarer Aufmacher.
    04 
    05 @L:Der erste Textabschnitt befasst sich mit 
    06 dem wichtigen Perl-Code
    07 
    08 @LI:
    09 # Wie alles begann:
    10 
    11 perl -le 'print "Hello World!"'
    12 
    13 @L:den jeder Perl-Programmierer im Schlaf 
    14 herbeten muss ([1] ist ein gutes Buch zum Thema). 
    15 Abbildung 1 illustriert die Einzelheiten.
    16 
    17 @Bi:fig/pod.gif
    18 @B:Abb. 1: Michael <I>schläft<I>.
    19 
    20 @ZT:Das Sonderzeichen <I><\<><I>
    21 
    22 @L:Listing 1 zeigt, wie das Kleiner-Zeichen <I><\<><I>
    23 und der Backslash <I><\\><I> elegant maskiert werden.
    24 
    25 @KT: Listing 1: maskiere.pl
    26 @LI: [Hier bitte Listing maskiere.pl einfügen]
    27 
    28 @L:Das war's für heute, bis zum nächsten Mal!
    29 
    30 @ZT:Referenzen
    31 
    32 [1] "Hello World ohne Ballast", Bernd Brabbel, 
    33 Salbader-Verlag, 2001
    34 
    35 @L:Michael schreibt über Perl und fährt auf
    36 der Autobahn schon mal schneller als erlaubt.

Die vom Linux-Magazin verwendeten Layouttags stehen am Zeilenanfang -- eingeleitet von einem @-Zeichen und von einem Doppelpunkt abgeschlossen. Hier sind die wichtigsten:

    @T:  Artikelüberschrift
    @V:  Der Vorspann
    @L:  Ein Absatz im Text
    @ZT: Eine Zwischenüberschrift
    @KT: Ein Kasten (z.B. Listings)
    @LI: Code eines Listings
    @Bi: Eine Abbildung
    @B:  Abbildungsunterschrift

Einen Eintrag in der dem Artikel angehängten Literaturliste leitet die in eckige Klammern eingebettete Referenznummer ein:

   [1] "Hello World ohne Ballast", 
   Bernd Brabbel, Salbader-Verlag, 
   2001

Im fließenden Text zeichnet die Tagfolge <I>...<I> kursiv zu druckenen Text aus. Da @, \ und < im Quark-Format Sonderbedeutung haben, müssen sie, wenn sie wörtlich gemeint sind, maskiert werden:

    @ wird zu <\@>
    < wird zu <\<>
    \ wird zu <\\>

Umlaute sind übrigens keine Sonderzeichen und dürfen in Latin-1 vorliegen, wenn die ursprüngliche POD-Datei in Latin-1 vorliegt. In der Ausgangssprache POD sind bekanntlich < und > Sonderzeichen und werden dort, wenn sie wörtlich gemeint sind, als E<lt> und E<gt> geschrieben.

Jedem POD-Tag lässt sich ein entsprechendes Quark-Pendant zuordnen: Die mit =head1 gekennzeichnete Hauptüberschrift wird zu @T:, der erste Absatz des Artikels zum Vorspann (@V:), alle anderen Absätze werden mit @L: ausgezeichnet. Mit =head2 ausgezeichnete Zwischenüberschriften werden zu @ZT:. Und der letztes Mal vorgestellte Filter pnd2pod erzeugte schon die Mechanik, um Listings in Kästen zu befördern (@KT: und @LI:) und Abbildungen und deren Beschreibungstexte mit @Bi: und @B: auszuzeichnen.

Dieses simple Regelwerk weist jedem POD-Tag ein entsprechendes Pendant im Quark-Format zu -- fragt sich nur, wie man schnellsten den POD-Salat einliest und die Konvertierung vornimmt?

Hierzu gibt's zum Glück schon das standardmäßig mitgelieferte Modul Pod::Parser, das eine POD-Datei ansaugt und bei jedem erkannten POD-Kommando einen Handler anspringt, der dann Transformationen und Ausgaben vornehmen darf. Pod::Parser selbst definiert dabei nur allgemeine Dummy-Handler. Benutzerdefinierte Transformationswerkzeuge erben dann zwar die die Schnittstelle von Pod::Parser, überschreiben die Handler aber mit eigenen, die die notwendigen Umwandlungen vornehmen und das Ergebnis ausgeben.

Listing 3 zeigt die Implementierung des Transferskripts pod2quark, das lediglich das neue Modul Quark hereinzieht, ein neues Quark-Objekt erzeugt und dessen Methode parse_from_file() aufruft. In Wahrheit steckt hinter dem weiter unten vorgestellten Quark-Modul natürlich Pod::Parser, dessen Methode parse_from_file() die Quark-Klasse einfach erbt. Der Aufruf

    pod2quark datei.pod

gibt den ins Quark-Format umgewandelten POD-Artikel einfach auf der Standardausgabe aus. Da Quark.pm nicht im üblichen Perl-Baum installiert wurde, sondern unter /u/mschilli/perl-modules, lässt Zeile 4 in pod2quark den Interpreter perl wissen, dass er auch dort nach eingebundenen Modulen suchen soll.

Listing 3: pod2quark

    1 #!/usr/bin/perl -w
    2 
    3 use strict;
    4 use lib '/home/mschilli/perl-modules';
    5 use Quark;
    6 
    7 my $parser = Quark->new();
    8 $parser->parse_from_file ($ARGV[0]);

Listing 4 zeigt mit Quark.pm die eigentliche Implementierung des heute vorgestellten Werkzeugs. Zeile 8 stellt Pod::Parser in den mit our global deklarierten @ISA-Array und weist Quark damit als von Pod::Parser abgeleitete Klasse aus.

Quark.pm überschreibt vier Methoden der Basisklasse Pod::Parser, deren Funktion in den folgenden Abschnitten besprochen wird.

Listing 4: Quark.pm

    001 ##################################################
    002 package Quark;
    003 ##################################################
    004 
    005 use Pod::Parser;
    006 use constant MAX_VERBATIM_LEN => 35;
    007 
    008 our @ISA = qw(Pod::Parser);
    009 
    010 ##################################################
    011 sub initialize {
    012 ##################################################
    013     my ($parser) = @_;
    014 
    015     $parser->{Intro} = 1;
    016 }
    017 
    018 ##################################################
    019 sub command {
    020 ##################################################
    021     my ($parser, $command, $para, $line) = @_;
    022 
    023     my $tag = "";
    024     my $quark = 0;
    025     
    026     $parser->{InVerbatim} = 0;
    027 
    028     return if $command eq 'pod';
    029     return if $command eq 'over';
    030 
    031     if ($command eq 'head1') {
    032         $tag = '@T:';
    033     } elsif ($command eq 'head2') {
    034         $tag = '@ZT:';
    035     } elsif ($command eq 'item') {
    036         $para =~ s/\s+$//s;
    037         $parser->{Item} = $para;
    038         return;
    039     } elsif ($command eq 'for') {
    040         return unless $para =~ /^quark/;
    041         $quark = 1;
    042         $para =~ s/^quark\s*\n//;
    043     }
    044 
    045     my $text = $parser->interpolate($para, $line);
    046     #$text = quark_esc($text, '@\\') unless $quark;
    047     $parser->output("$tag$text");
    048 }
    049 
    050 ##################################################
    051 sub verbatim {
    052 ##################################################
    053     my($parser, $text) = @_;
    054 
    055     return if $text =~ /^\s*$/s;
    056 
    057         # Zeilenlänge prüfen
    058     while($text =~ /(.*)/g) {
    059         if(length($1) > MAX_VERBATIM_LEN) {
    060             warn "Verbatim line too long: $1";
    061         }
    062     }
    063 
    064     #$text = quark_esc($text, '@\\');
    065     $text =~ s/^ {4}//mg;
    066     
    067         # Absatz im Verbatim-Stück?
    068     if($parser->{InVerbatim}) {
    069         $parser->output("$text");
    070         return 1;
    071     }
    072     
    073     $parser->{InVerbatim} = 1;
    074 
    075     $parser->output("\@LI:\n$text");
    076 }
    077 
    078 ##################################################
    079 sub interior_sequence {
    080 ##################################################
    081     my ($parser, $cmd, $arg) = @_;
    082 
    083         # B<.>, I<.> wird <I>.<I>
    084     if($cmd =~ /B|I/) {
    085         return "<I>$arg<I>";
    086     }
    087 
    088         # C<.> wird <C>.<C>
    089     if($cmd =~ /C/) {
    090         return "<C>$arg<C>";
    091     }
    092 
    093         # E<.> Notierung für Sonderzeichen in POD
    094     if($cmd eq "E") {
    095         $arg eq "gt" && return ">";
    096         $arg eq "lt" && return "<";
    097     }
    098 }
    099 
    100 ##################################################
    101 sub textblock {
    102 ##################################################
    103     my ($parser, $para, $line) = @_;
    104 
    105     my $tag = "";
    106 
    107     if(exists $parser->{Item}) {
    108         $para = "$parser->{Item} $para";
    109         delete $parser->{Item};
    110     } else {
    111         $tag = '@L:';
    112     }
    113 
    114     my $text = $parser->interpolate($para, $line);
    115     #$text = quark_esc($text, '@\\');
    116 
    117 
    118     if($parser->{Intro}) {
    119         $tag = "\@V:";
    120         $parser->{Intro}  = 0;
    121     }
    122 
    123     $parser->{InVerbatim} = 0;
    124     $parser->output("$tag$text");
    125 }
    126 
    127 ##################################################
    128 sub output { 
    129 ##################################################
    130     my($parser, $text) = @_;
    131 
    132     my $out_fh = $parser->output_handle;
    133     print $out_fh $text;
    134 }
    135 
    136 ##################################################
    137 sub quark_esc {
    138 ##################################################
    139     my ($string, $chars) = @_;
    140 
    141         # Backslash drin oder nicht?
    142     my $backslash = ($chars =~ s-\\--);
    143 
    144         # Alles ausser Backslashes ersetzen
    145     $string =~ s-([$chars])-<\\$1>-g;
    146 
    147     if($backslash) {
    148             # Sonderbehandlung
    149         $string =~ s-(?<!<)(\\)-<\\$1>-g;
    150     }
    151     
    152     return $string; 
    153 }

initialize() ab Zeile 11 springt der Parser zu Beginn des Parse-Vorgangs an. Quark.pm setzt hier nur die frech hinzudefinierte neue Instanzvariable Intro, die später helfen wird, festzustellen, ob der aktuelle Absatz der Aufmacher oder einer der folgenden Absätze ist.

command() ab Zeile 19 ruft der Parser jedes Mal auf, wenn er auf ein mit = eingeleitetes POD-Kommando (wie z.B. =head1) stößt. Vier Parameter liegen dann vor: $parser als eine Referenz auf das Parser-Objekt, $command als der Name des Kommandos (z.B. head1), $para als der dem Kommando beigefügte Textabsatz (z.B. der Text der Überschrift) und die Zeilennummer $line in der ursprünglichen POD-Datei.

Die Zeilen 28 und 29 ignorieren die Kommandos =pod und =over, die pod2quark nicht bearbeitet. Das if-Konstrukt ab Zeile 31 nimmt für die Kommandos =head1, =head2, =item und =for die richtigen Aktionen vor. Für =item-Kommandos, die zur Auflistung von Literaturverweisen dienen, speichert Zeile 37 den übergebenen String (im allgemeinen eine Referenzennummer im Format [1]) in der Instanzvariablen Item des Parser-Objekts. Vorher löscht es mit s/\s+$//s abschließende Leerzeilen aus dem String, da der Parser die Angewohnheit hat, nicht nur den =item übergebenen String zu melden, sondern auch noch zwei Zeilenumbrüche. Wegen des Modifizierers /s (für single line) des regulären Ausdrucks passen auf /\s+$/ eine beliebige Anzahl von Leerzeichen und Zeilenumbrüchen am Ende eines mehrzeiligen Strings.

Im Fall von =for quark-Anweisungen (alle anderen =fors ignoriert Quark.pm geflissentlich) löscht es den quark-String, übernimmt aber den Rest des Absatzes -- wo unser letztes Mal gezeigtes pnd2pod vorher bereits fertige Quark-Kommandos für Listings und Abbildungen abgelegt hat.

Zeile 45 ruft die Methode interpolate() des Parsers auf, um die im Text der erfassten Überschrift (oder auch Bildunterschrift) vorkommenden POD-Macros wie C<...> oder I<...> mittels der weiter unten definierten Methode interior_sequence() zu bearbeiten, in entsprechende Quark-Sequenzen umzusetzen und das interpolierte Ergebnis zurückzugeben.

Zeile 46 maskiert mittels der ab Zeile 131 definierten Funktion quark_esc() die Zeichen @ und \ durch <\@> und <\\>. Die Funktion quark_esc() nimmt einen Textstring entgegen und einen zweiten String, der die im Textstring zu ersetzenden Sonderzeichen enthält. Der Backslash muss in Perl bekanntlich auch in einfachen Anführungszeichen maskiert werden. Das Sonderzeichen < wird in command() nicht ersetzt, da diese Aufgabe schon das darunterliegende interior_sequence() erledigt hat. Mehr dazu später.

Die in Zeile 122 definierte output()-Methode ist tatsächlich eine Methode der Klasse Quark und nicht etwa eine ursprünglich von Pod::Parser stammende. Sie ist ab Zeile 122 definiert. Ihre Aufgabe besteht lediglich darin, den ihr übergebenen Text mit print() auf das Ausgabe-File-Handle des Parsers schreiben, das die Methode output_handle() liefert.

Für die in POD eingerückten Abschnitte (meist bereits fertig formatierte Kurzlistings im Fließtext), ruft der Parser jeweils die verbatim()-Methode mit dem Parser-Objekt $parser und dem Listingstext auf, der in $text vorliegt. Besteht letzterer nur aus Leerzeilen, kehrt Zeile 55 rasch wieder zurück. Da der Spaltensatz des Linux-Magazins die Zeilenlänge von Kurzlistings auf ca. 35 Zeichen beschränkt, gibt Zeile 60 für jede längere Zeile eine kurze Warnung aus, so dass der Autor diese gemäß den Empfehlungen in [1] im Unix-Format umbrechen kann. In den Text eingebettete Listings sollten sich bekanntlich nie über mehr als ein paar Zeilen hinziehen, für längere und benamte Listings steht das =include-Tag bereit. Letzteres veranlasst pod2quark, im fertigen Manuskript lediglich den Namen des Listings mit den notwendigen Formatierungsangaben zu erwähnen und das ganze in einen Kasten zu stecken. Der Autor formatiert dann die Listings und packt sie extern bei, worauf der Setzer sie kunstvoll um die Textspalten gruppiert.

Zeile 64 maskiert daraufhin die Sonderzeichen @, \ und < im Listing gemäß den Quark-Richtlinien und Zeile 75 gibt den Verbatim-Absatz mit dem vorangestellten Tag @LI: aus. Zeile 65 entfernt die zum Einrücken verwendeten Leerzeichen.

Leerzeilen in eingerückten Verbatim-Absätzen veranlassen den POD-Parser leider, jedesmal von neuem die verbatim-Methode aufzurufen. Damit dies nicht jedesmal ein @LI:-Tag zur Folge hat, merkt sich der Parser die Tatsache, dass er sich in einem Verbatim-Absatz befindet in der Instanzvariablen InVerbatim. Schlägt die Variable an, hängt Zeile 69 den gegenwärten Verbatim-Absatz einfach an die Ausgabe an, ohne ein weiteres @LI:-Tag auszuschreiben. Im nächsten Text- oder Verbatimblock, den die weiter unten beschriebenen textblock()/verbatim()-Methoden erfassen, wird InVerbatim wieder auf 0 zurückgesetzt.

Die ab Zeile 79 definierte Methode interior_sequence() ruft der Parser jedesmal auf, wenn er auf eine von PODs Inline-Text-Auszeichnungen wie E<gt> oder I<...> stößt. In $cmd liegt dann der Kommandoname (z.B. E oder I) und in $arg der eingeschlossene Text vor. Zeile 84 sucht nach den Kommandos B, I oder C, die pod2quark allesamt mit <I>...<I> in kursiven Text transformiert.

Ab Zeile 89 wird's haarig: An einem mit E<gt> in PND kodierten Sonderzeichen > nimmt Quark keinen Anstoß, also kann aus E<gt> gleich > werden, aber E<lt> ist ein Sonderzeichen, das in Quark als <\<> notiert, was Zeile 90 erledigt, indem es quark_esc() auffordert, im String '<' nur das Zeichen < quark-sicher zu maskieren.

Zeile 95 definiert die Methode textblock() die der Parser für jeden normalen Textabsatz ohne besondere Auszeichnung aufruft. Der Text liegt in $para. Wenn von einem vorausgehenden =item-Kommando her die Instanzvariable Item des Parsers gesetzt ist, verzweigt Zeile 101 in den if-Block, der den gespeicherten =item-Text vor den Text des Absatzes einfügt, damit die Literaturverweise ordnungsgemäß aufgelistet werden, das @L:-Tag entfällt in diesem Fall.

Die in Zeile 108 aufgerufene interpolate()-Methode ruft für die zu ersetzenden Inline-Tags wieder die interior_sequence()-Methode auf und nimmt die notwendigen Transformationen vor. Das anschließend aufgerufene quark_esc() ersetzt nur die Sonderzeichen @ und \, da etwa auftretende <-Zeichen schon von der interior_sequence()-Methode erschlagen wurden.

quark_esc() ersetzt, wie schon erwähnt, die ihm im zweiten String übergebenen gefährlichen Sonderzeichen durch Quark-sichere -- es bedient sich jedoch eines kleinen Kniffs: Es ersetzt Backslashes nur dann, falls diese nicht als <\ vorkommen und für bereits maskierte <-Zeichen stehen. Der Grund dafür: Zeile 109 muss aus einem bereits interpolierten Textabsatz unter anderem die wörtlich vorkommenden Backslashes maskieren. Da die Interpolationsfunktion dort aber schon <\<>-Konstrukte abgelegt hat, wäre es falsch, deren Backslashes nochmals zu maskieren. Deswegen entfernt Zeile 136 in quark_esc() eventuell vorkommende Backslashes aus dem Skalar $chars, der die zu ersetzenden Zeichen erhält. Die Variable $backslash fängt das Ergebnis ab, und erhält einen wahren Wert, falls sich in $chars vorher ein Backslash befand und einen falschen Wert, falls nicht. Statt des sonst üblichen /-Zeichens, um das Suchmuster im Substitutionsbefehl vom Ersetzungsmuster zu trennen, nutzen die regulären Ausdrücke in quark_esc() das Zeichen -, um Backslashitis-Schüben bei empfindlichen Perl-Programmierern vorzubeugen.

Zeile 139 ersetzt dann alle in $chars angegebenen Zeichen außer dem Backslash durch quark-sicherne Maskierungen. Zeile 143 ersetzt Backslashes anschließend nur, falls $chars dies vorher verlangte und dann auch nur dann, falls es kein Backslash in <\ ist, was das Suchmuster /(?<!<)(\\)/ einfordert. (\\) passt dabei auf einen einfachen Backslash, der negative Look-Behind (?<!) (neu mit Perl 5.6.0) davor stellt sicher, dass kein < davorsteht. Puh!

Installation

Das neue Quark.pm muss in einen Pfad, in dem perl es auch findet. Gerne darf pod2quark auch, wie vorgestellt, mit use lib auf einen nicht standardisierten Pfad verweisen. pod2quark sollte irgendwo stehen, wo die Shell es findet, also irgendwo im $PATH.

Aus einem mit PND geschriebenen Artikel wird also mit

    pnd2pod text.pnd
    pod2quark text.pod >text.qex

einer für's Linux-Magazin. Zu beachten ist, dass die mit =include und =figure eingebundenen Listings und Abbildungen nicht direkt im Text enthalten sind, sondern dem Artikel in einem Tar- oder Zip-Archiv beiliegen sollten. Schreibt ein, zwei, viele Artikel für's Linux-Magazin im PND-Format und konvertiert sie zur Freude aller Redakteure gleich ins richtige Layoutformat!

Literaturhinweise

[1]
Autorenhinweise des Linux-Magazins: http://www.linux-magazin.de/Info/autoren.html

[2]
Michael Schilli, ``New Tricks for an Old Dog'', Linux-Magazin 03/01, Seite XXX, http://www.linux-magazin.de/ausgabe/2001/03/XXX/xxx.html

[3]
POD-Manualseiten, perldoc perlpod

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.