Ein Pretty Printer für BSP-Views

Inhalt

Views mit gemischtem Inhalt

Im BSP-Praxisbuch habe ich auf die Mängel hingewiesen, die der von SAP ausgelieferte Pretty Printer für BSP-Views mit gemischtem Inhalt hat. Gemischter Inhalt bedeutet hierbei, dass der View sowohl HTML- als auch BSP-Elemente enthält. Der Pretty Printer müsste für derartige Views einen Baum aufbauen, der sowohl die HTML- als auch die BSP-Elemente berücksichtigt.

Der von SAP ausgelieferte Pretty Printer kann dies nicht leisten. Er ist für Views mit reinem BSP-Elementinhalt konzipiert. SAP macht dadurch deutlich, dass sie an Views mit gemischtem Inhalt kein Interesse hat. Die Views sollten aus SAP-Sicht besser vollständig mit BSP-Elementen aufgebaut werden, was impliziert, dass aller HTML-Code in den Implementierungen der Elementbehandlerklassen verborgen ist. So will man eine zusätzliche Indirektionsebene schaffen, um Webanwendungsentwickler nicht mit so "schmutzigen" Dingen wie HTML-Code zu belästigen. Ähnlich wie die XML-Metadaten des WebDynpros könnte man mit Views, die auf diese Weise aufgebaut sind, zumindest theoretisch ermöglichen, dass die Ausgabe in einer ganz anderen Präsentierungssprache als HTML erfolgt - vielleicht im PDF- oder Flash-Format.

Aber abgesehen davon, dass dies eine eher theoretische Möglichkeit ist – denn wo gibt es BSP-Tag-Libraries mit verschiedenen Zielformaten? – ist eine Abstraktion an dieser Stelle auch unerwünscht. In den Views befinden wir uns bereits auf der Ebene der Präsentation, eine zusätzliche Indirektionsebene ist hier nicht nötig. Ein Web-Anwendungsentwickler möchte sich in den Views mit seinen HTML-, CSS- und JavaScript-Kenntnissen verwirklichen! BSP-Elemente, die ihm Arbeit mit immer gleichen Code-Teilen abnehmen, sind ihm als Hilfsmittel stets willkommen, aber eben nur als Hilfe. Der Einsatz muss optional sein, und sie dürfen sich nicht als sein neuer Meister aufspielen, deren Regeln er etwa sklavisch zu befolgen hätte.

Ein Beispiel

Dazu kommt, dass ein View meist nur ein Fragment des gesamten HTML-Dokumentbaums darstellt. Die Formatierung des Quelltexts sollte daher auch für unvollständige HTML-Dokumente funktionieren. Ein formatierter HTML-View mit gemischtem Inhalt könnte etwa folgendermassen aussehen:
<%@page language="abap" %>
<%@extension name="Z" prefix="z" %>
<table>
  <tr>
    <td class="formatlabel"><%=otr(zsrs_appl/kunde)%></td>
    <td>
      <z:input binding = "//order/kunnr"
               shlp    = "zsrs_kunnr"/>
    </td>
  </tr>
  <tr>
    <td class="formatlabel"><%=otr(zsrs_appl/verkaeufer)%></td>
    <td>
      <z:input binding = "//order/verknr"
               shlp    = "zsrs_verknr"/>
    </td>
  </tr>
</table>
In diesem Beispiel stehen die blauen Passagen für Direktiven und Scripting-Anweisungen. Die HTML-Elemente sind in roter Farbe markiert, die BSP-Elemente sind braun dargestellt. Das Beispiel zeigt, dass der Elementbaum von den HTML- und den BSP-Elementen gemeinsam aufgespannt wird.

Abgesehen von der Einrückung und strukturierten Darstellung des Views, die es ermöglicht, Schachtelungsfehler schnell zu erkennen, soll ein BSP-Pretty-Printer noch einiges andere können. Hier folgt eine kurze Zusammenstellung der wichtigsten Features.

HTML-Elemente einheitlich klein schreiben

Im Gegensatz zu BSP-Elementen sollen die HTML-Elementen einheitlich dargestellt werden. Aus Gründen der XHTML-Kompatibilität empfiehlt sich die einheitliche Kleinschreibung auch für HTML-Elemente.
<HEAD>
wird zu
<head>

Im Gegensatz zu HTML-Elementen ist es für BSP-Elemente nicht egal, ob sie gross oder klein geschrieben werden. Für BSP-Elemente sollte daher die Schreibung des unformatierten Dokuments identisch übernommen werden:

<z:tableBody>
bleibt
<z:tableBody>

Attribute

Analog gilt: Attribute von HTML-Elementen sollen einheitlich kleingeschrieben werden, während die Schreibung für Attribute von BSP-Elementen beibehalten werden soll.

Attributwerte sollen darüberhinaus stets in Anführungszeichen erscheinen. Wo sie im Original fehlen, soll der Pretty Printer daher die Anführungszeichen automatisch einsetzen.

<BODY ONLOAD=doOnLoad();>
wird zu
<body onload="doOnLoad();">

Eine Umbruchregel erhöht die Lesbarkeit: Wenn ein Element sehr viele Attribute enthält und daher sehr lang wird, ist es besser, die Attributangaben gemeinsam zu formatieren.

<INPUT ONCLICK=doOnClick() maxlength=17 size=10 onchange="return chk();" onMouseover="showToolTip();" onMouseout="hideToolTip();">
wird zu
<input onclick     = "doOnClick();"        onchange    = "return chk();"        onmouseover = "showToolTip();"        onmouseout  = "hideToolTip();"        maxlength   = "17"        size        = "10" >

BSP-Elemente ohne Inhalt

Es ist zwar theoretisch möglich, dass in einem Elementaufruf wie dem folgenden
<z:column name="c1"> 

      </z:column>
der Leerraum zwischen dem öffnenden und schliessenden Tag bei dessen Verarbeitung irgendeine geheimnisvolle Rolle spielt. Da dies eine viel zu fragile und intransparente Angelegenheit ist, kann man ohne Einschränkung vom Formatierer erwarten, dass er Code wie den obigen zu einem sich selbst schliessenden Tag zusammenfasst:
<z:column name="c1"> 

      </z:column>

wird zu
<z:column name="c1"/>

Einrückung und Umbruch

Die Einrückung der Elemente erfolgt, wie oben beschrieben unter Einbezug sowohl der HTML- als auch der BSP-Elemente. Die Tiefe der Einrückung sollte parametrisierbar sein, ein praktisch ratsamer Wert ist zwei Zeichen pro Stufe.

Es gibt eine Reihe von HTML-Tags wie <meta>, <link>, <p>, ..., die nicht geschlossen werden müssen. Der Formatierer soll für diese Tags die Einrücktiefe nicht verändern. Die Menge der Tags, die nicht geschlossen werden müssen, ist zwar spezifiziert, soll aber für den Pretty Printer konfigurierbar sein.

Für die meisten Tags ist es sinnvoll, vor dem Start-Tag einen Zeilenumbruch einzuführen. Aber auch hier gibt es Ausnahmen. Für Inline-Tags wie tt, i oder b wird man i.a. keinen Umbruch erwarten. Die Menge dieser Inline-Tags soll ebenfalls frei definierbar sein.

Ob Umbrüche am Elementende stehen sollen, entscheidet am besten der Entwickler selbst: Wenn die unformatierte Source das Element mit einem oder mehreren Umbrüchen enden lässt, so ist dies offenbar beabsichtigt, und die Zahl dieser Umbrüche soll in den formatierten Code übernommen werden. Wenn der Entwickler keinen Umbruch am Elementende wünscht, schreibt er auch in der unformatierten Source das Elementande ohne Umbruch direkt ans Ende des Elementinhalts.

Realisierung in der Workbench

Leider kommt diese Lösung nicht ganz ohne Modifikation aus, denn wir möchten ja den Button "Pretty Printer" des SAP-Standards mit einer anderen, sinnvolleren Funktion belegen. Da in anderen Contexten möglicherweise der Pretty Printer des Standards vorzuziehen ist, habe ich den Weg eines Benutzerparameters gewählt, mit dem der Benutzer seine eigene Formatierungsklasse angeben kann, die statt des Standards verwendet werden soll.

Der Benutzerparameter Z_BSP_FORMATTER

Um eigene Formatierungsklassen verwenden zu können, habe ich den neuen Benutzerparameter Z_BSP_FORMATTER definiert. In der Pflege der eigenen Benutzerparameter (übers Menü, oder direkt mit der Transaktion SU3) kann man dort eine Klasse hinterlegen, die die Formatierung übernehmen soll:

Die Klasse ZCL_BSP_FORMATTER

Im Prinzip kann man mit diesem Benutzerparameter eine beliebige Klasse definieren, die den Pretty Print leistet. Am besten kopieren Sie sich die zu ersetzende Methode pretty_print der Klasse CL_O2_CO2_PROCESSOR in die neue Klasse, hier also in eine neue Klasse ZCL_BSP_FORMATTER, und löschen danach die Implementierung. Dann haben Sie sichergestellt, dass Sie die gleiche Schnittstelle verwenden, nämlich die folgende:

Leider bietet die ABAP Runtime keinen HTML-Parser an. Daher muss unser Pretty Printer letztlich als Betriebssystemkommando ausgeführt werden. Ein Weg, der sich übrigens schon in so manchen Fällen als nützliche Ergänzung zum Sprachumfang von ABAP Objects erwiesen hat. Wenn das Betriebssystem einen Perl-Interpreter besitzt, wie es bei Unix-Systemen zur Standardausrüstung gehört, steht Ihnen die ganze Welt des CPAN offen. In diesem Fall verwenden wir die Komponente HTML::Parser, die meist bereits zur Perl-Auslieferung dazugehört und nicht extra aus dem CPAN installiert werden muss.

Unsere PrettyPrinter-Implementierung muss also lediglich die unformatierte Source ans Betriebssystem übergeben, dort ein Programm zur Formatierung aufrufen und schliesslich die formatierte Source wieder abholen. Das machen wir über das Filesystem wie folgt:

method pretty_print.

  data: lv_infile      type string,
        lv_outfile     type string,
        lv_formatted   type string,
        lv_unformatted type string,
        lv_name        type string,
        lv_param       type btcxpgpar,
        lt_prot        type standard table of btcxpm,
        lv_pageline    type o2pageline.

  field-symbols: <lv_line> type o2pageline.

* Default - Formatierung hat funktioniert
  test_ok = 'X'.

* Applikation und Seite zusammenfassen
  split page at '.' into lv_name sy-msgli.
  concatenate appl '_' lv_name into lv_name.
  translate lv_name to lower case.

* Unformatierte BSP-Seite im Filesystem ablegen
  concatenate '/tmp/' lv_name into lv_param.
  concatenate lv_param '.unf' into lv_infile.
  open dataset lv_infile for output in text mode
       encoding default.
  loop at bsp_unformatted assigning <lv_line>.
    transfer <lv_line> to lv_infile.
  endloop.
  close dataset lv_infile.

* Pretty Printer im OS ausführen
  call function 'SXPG_COMMAND_EXECUTE'
    exporting
      commandname                   = 'Z_BSP_PRETTY_PRINT'
      additional_parameters         = lv_param
    tables
      exec_protocol                 = lt_prot
    exceptions
      no_permission                 = 1
      command_not_found             = 2
      parameters_too_long           = 3
      security_risk                 = 4
      wrong_check_call_interface    = 5
      program_start_error           = 6
      program_termination_error     = 7
      x_error                       = 8
      parameter_expected            = 9
      too_many_parameters           = 10
      illegal_command               = 11
      wrong_asynchronous_parameters = 12
      cant_enq_tbtco_entry          = 13
      jobcount_generation_error     = 14
      others                        = 15.
  if sy-subrc <> 0.
    message id sy-msgid type 'I' number sy-msgno
            with sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
    bsp_formatted = bsp_unformatted.

  else.

* Inputfile kann nun gelöscht werden
    delete dataset lv_infile.

* Ergebnis abholen
    clear bsp_formatted.
    concatenate lv_param '.for' into lv_outfile.
    open dataset lv_outfile for input in text mode
         encoding default.
    if sy-subrc eq 0.
      do.
        read dataset lv_outfile into lv_pageline.
        if sy-subrc ne 0.
          exit.
        endif.
        insert lv_pageline into table bsp_formatted.
      enddo.
      close dataset lv_outfile.

* Outputfile kann nun auch gelöscht werden
      delete dataset lv_outfile.

    endif.

  endif.

endmethod.

Das Betriebssystemkommando Z_BSP_PRETTY_PRINT

Die obige Methode pretty_print( ) schreibt den aktuellen Inhalt des BSP-Views in eine temporäre Datei, ruft das Betriebssystemkommando Z_BSP_PRETTY_PRINT auf und nimmt dessen Ausgabe - den formatierten Viewcode - als Datei entgegen, um ihn als interne Tabelle an den Aufrufer, die BSP-Entwicklungsumgebung zurückzugeben. Hinter diesem Betriebssystemkommando verbirgt sich der Aufruf eines Perl-Programms bsp_pp.pl. Das Kommando, macht man in Transaktion SM69 wie folgt bekannt:

Die Modifikation in CL_O2_PAGE

Schliesslich wird der neue Code in die BSP-Entwicklungsumgebung eingebunden. Ein guter Punkt hierfür ist die Methode page_pretty_printer_ext() der Klasse cl_o2_page. Hier mischen wir uns in die Instanzerzeugung des Pretty Printers mit einer Modifikation ein. Es wird dort ein Pretty-Printer-Objekt pp erzeugt und zur Formatierung die Methode pretty_print() aufgerufen. Da die Methode dynamisch aufgerufen wird, kann statt der Klasse cl_o2_co2_processor ebensogut eine eigene Klasse verwendet werden, wenn sie nur eine Methode pretty_print mit derselben Schnittstelle aufweist.

Daher stellen wir der Anweisung create object pp type ('CL_O2_CO2_PROCESSOR'), die hier im Standard steht, eine Auswertung des neuen Benutzerparameters Z_BSP_FORMATTER voran. Wenn dieser Parameter einen Klassennamen enthält, so verwenden wir diesen Klassennamen für die Instanzbildung. Wenn nicht, geht es wie im Standard mit der Klasse CL_O2_CO2_PROCESSOR weiter:

...
* call pretty printer
*{   REPLACE        D12K936392
*\  create object pp type ('CL_O2_CO2_PROCESSOR').

  data: lv_pp_class type classname.
  get parameter id 'Z_BSP_FORMATTER' field lv_pp_class.
  if lv_pp_class is initial.
    lv_pp_class = 'CL_O2_CO2_PROCESSOR'.
  endif.
  create object pp type (lv_pp_class).

*}   REPLACE 
  appl_str = me->pagekey-applname.
  page_str = me->pagekey-pagekey.
  try.
      CALL METHOD pp->('PRETTY_PRINT')
        EXPORTING
          indent              = 2
          ATTRIBUTE_NORMALIZE = 'X'
          ATTRIBUTE_THRESHOLD = 2
          BSP_UNFORMATTED     = source
          appl                = appl_str
          page                = page_str
        IMPORTING
          test_ok             = test_ok
          BSP_FORMATTED       = pp_source.

...

Testrahmen und rufendes Programm

Bislang habe ich nur beschrieben, wie die ABAP-Workbench in die Lage versetzt werden kann, den Pretty Printer an ein externes Programm zu delegieren. Nun komme ich zur eigentlichen Logik dieses Perl-Programms.

Was das Programm leisten soll, habe ich in einer Testsuite PrettyPrinter.t festgelegt. Die Testsuite besteht zur Zeit aus 16 Tests. Jeder Test sichert eine bestimmte Eigenschaft des Programms ab, indem er den Pretty Printer für einen kleinen Teststring aufruft und jeweils mit dem erwarteten Ergebnis vergleicht. Die Testsuite baut auf dem Perl-Paket Test auf und fängt folgendermassen an:

use strict;
use Test;

# Anzahl der Tests muss am Anfang angegeben werden
BEGIN { plan tests => 16 }

# Modul laden...
use BSP::PrettyPrinter qw(pretty_print);

# 1 - Identische Transformation
  ok(pretty_print("<head></head>")             , "<head></head>");
# 2 - HTML-Elementnamen immer lowercase  
  ok(pretty_print("<HEAD></heAD>")             , "<head></head>");
# 3 - Scripting bleibt erhalten  
  ok(pretty_print("<%--Test--%><head> </head>") , "<%--Test--%>\n<head></head>");
# 4 - HTML-Kommentare bleiben erhalten  
  ok(pretty_print("<!--Test--><head></head>") , "<!--Test-->\n<head></head>");
  ...
Das Output der Testsuite ist spartanisch: Für jeden Test wird ok ausgegeben, wenn er erfolgreich durchgelaufen ist, oder not ok, wenn er fehlschlug. Die Ausgabe ist standardisiert und folgt dem sogenannten Test Anything Protocol (TAP). Damit ist das Output des Tests auch maschinell weiterverwertbar. Hier das Beispiel einer Ausgabe, wobei ich in den Test Nr. 4 bewusst einen Fehler eingebaut habe:
1..16
# Running under perl version 5.008007 for MSWin32
# Win32::BuildNumber 815
# Current time local: Fri Nov 23 12:35:06 2007
# Current time GMT:   Fri Nov 23 11:35:06 2007
# Using Test.pm version 1.25
ok 1
ok 2
ok 3
not ok 4
# Test 4 got: "<!--Test--!>\n<head></head>" (F:\HTMLPA~1\PRETTY~1.T at line 17)
#   Expected: "<!--Test--!>1\n<head></head>"
#  F:\HTMLPA~1\PRETTY~1.T line 17 is:   ok(pretty_print("<!--Test--!><head></head>") , "<!--Test--!>1\n<head></head>");
ok 5
...
Die Testsuite zeigt, dass es im Package BSP::PrettyPrinter eine Funktion pretty_print( ) gibt, die den unformatierten HTML-Code als String entgegennimmt und den formatierten Code als Returnwert zurückliefert. Da wir in der Regel keinen String, sondern eine Datei als Eingabe haben, benötigen wir auch hier noch eine aufrufende Hülle, die einen Dateinamen entgegennimmt, die Datei in einen String einliest, den Pretty Printer aufruft und das Ergebnis in die Standardausgabe oder in eine andere Datei stellt. Ich habe dieses rufende Programm bsp_pp.pl genannt:
# ----------------------------------------------------------------------
# BSP Pretty Printer
# ----------------------------------------------------------------------
# Doku siehe http://bsp.mits.ch/supplements/pretty.htm
# (C) Rüdiger Plantiko, Migros IT-Services (MITS), 7/2007
# ----------------------------------------------------------------------

package BSP::PrettyPrinter;

use HTML::Parser 3; # HTML::Parser mindestens in Version 3
use strict;
use Exporter;

our $VERSION = 0.9;
our @ISA     = qw(Exporter);
our @EXPORT  = qw(&pretty_print);

# ----------------------------------------------------------------------
# Pretty Print - einzige öffentliche Methode des Pakets
# Erwartet unformatierten String
# Gibt formatierten String zurück
# ----------------------------------------------------------------------
sub pretty_print($) {
  my @bspParts;
# BSP-Scripting vorläufig durch HTML-Kommentare ersetzen
  my $buf = substituteBsp(shift,\@bspParts);
# Transformation mithilfe des HTML::Parsers durchführen
  return transform( $buf, \@bspParts );
  }


sub substituteBsp( ) {

  my ($buf,$bspParts) = @_;

# ----------------------------------------------------------------------
# Erster Durchgang
# Ersetze alle BSP-Direktiven durch HTML-Kommentare <!--BSP1--> etc.
# Baue gleichzeitig einen Array mit den Originalausdrücken auf
# ----------------------------------------------------------------------
  my $bspCounter = 0;
  my $buf1 = "";
  my $posi=0;
  my $pre;
  my $spc;
  my $bspTag;

# Regulärer Ausdruck trifft alle Formen von Scripting
# <% ... %>, <%= ... %> und <%-- --%>
  while ( $buf =~ /(.*?)(\s*)(<%(.*?)%>)/gs) {
    ($pre,$spc,$bspTag) = ($1,$2,$3);
# Kontrolliert einen Zeilenumbruch einfügen
# Einrückung des Benutzers stehen lassen
    if ($spc =~ /\n/ ) {
      (my $s1 = $spc) =~ s/^.*(\n[^\n]*)$/$1/s;
      $bspTag = $s1 . $bspTag;
      }
# Ersetzungsausdruck aufbauen, $bspCounter wird der Index
# im Array @$bspParts
    $buf1 .= $pre . "<!--_bsp$bspCounter-->" ;
    push( @$bspParts, $bspTag);
    $bspCounter++;
    $posi = pos($buf);
    }

# Den Schwanz (alles ab letztem gefundenen Scriptingausdruck)
# noch hintanstellen
  $buf1 .= substr( $buf, $posi);

  return $buf1;

  }

# ----------------------------------------------------------------------
# Zweiter Durchgang: HTML-Parser / Pretty Printer
# ----------------------------------------------------------------------

{

# Private Daten des HTML-Parsing-Blocks:

# Referenz auf den Stack der BSP-Ausdrücke
my $bspParts;

# Parser-Stack
my @stack;
my $result; # Ergebnis (neu)

# Tags, die nicht unbedingt geschlossen werden müssen
my %nonClosingTags;
# Tags, die keinen Zeilenumbruch erfordern
my %inlineTags;
# Tags, die stets Text enthalten (kein HTML)
my %cdataTags;
# Flag, dass wir gerade in einem solchen Tag sind
my $cdataContent;
# Kritische Länge, ab der die Attribute umgebrochen werden
my $attBreakLimit;
# Einrücktiefe
my $indentDepth;

sub transform {

  (my $buf1,$bspParts,my $config) = @_;

  my $bspOpen = 0;

# Initialisierungen
  do_init($config);
  init_result();

# Parser instanziieren, Handler definiern, parse() aufrufen
  my $parser = HTML::Parser->new(
    default_h => [ \&others,          'event,text' ],
    start_h   => [ \&start_handler,   'self,tagname,attr,text' ],
    end_h     => [ \&end_handler,     'self,tagname,text' ],
    text_h    => [ \&text_handler,    'self,text' ],
    comment_h => [ \&comment_handler, 'self,text' ]
    ) || die $!;
# Case Sensitive ist nötig wegen der BSP-Elemente
  $parser->case_sensitive( 1 );
# XML-Mode ist nötig wegen der BSP-Elemente
# Hilft, dass selbstschliessende Tags korrekt erkannt werden
  $parser->xml_mode( 1 );
  $parser->boolean_attribute_value( "_BOOL_" );
# Nun das Parsing durchführen...
  $parser->parse($buf1) || die $!;
# ... und beenden
  $parser->eof;

# Ausgabestring zurückgeben
  my $buf2 = getResult();
  $buf2 =~ s/^\n// unless $buf1 =~ /^\n/;
  return $buf2;

  }

# ----------------------------------------------------------------------
# Initialisierungen für das Parsing - Konfigurationseinstellungen
# ----------------------------------------------------------------------
sub do_init {
  my $config = shift;
  my $return;

# Selbstausführendes Konfigurationsfile ...
  if ($config) {
    unless ($return = do $config) {
      die "couldn't parse $config: $@"         if $@;
      die "couldn't do $config: $!"            unless defined $return;
      die "couldn't run $config"               unless $return;
      }
    }
  else {
# ... oder Konfigurationen aus diesem Script (siehe unten)
    $return = standard_config();
    }

# Tag-Mengen als Hashs aufbereiten
  %inlineTags     = map { ($_,1) } @{$return->{inlineTags}};
  %nonClosingTags = map { ($_,1) } @{$return->{nonClosingTags}};
  %cdataTags      = map { ($_,1) } @{$return->{cdataTags}};

# Zeilenzahl, ab der es einen Umbruch beim Attribut-Rendern gibt
  $attBreakLimit  = $return->{attBreakLimit};

# Einrücktiefe
  $indentDepth    = $return->{indentDepth};

  }

# ----------------------------------------------------------------------
# Ereignisbehandler für HTML::Parser
# ----------------------------------------------------------------------
# Öffnende Tags
sub start_handler {
  
  my ($self,$tagname,$attr,$text) = @_;
  my (%myAttributes,$key,$value,$prefix);
  my $atts="";
  
  my $deepPrint = (length( $text ) > $attBreakLimit or $text =~ /\n/);

# Tags innerhalb von CDATA als Text behandeln
  if ($cdataContent) {
    purePrint( $text );
    push @stack, [ "text", $text ];
    return;
    }

# HTML-Tags ( = die ohne Namespace) kleinschreiben
  $tagname = lc $tagname unless $tagname =~ /:/;

# Merken, dass ein CDATA-Tag geöffnet wurde
  if (exists $cdataTags{$tagname}) {
    $cdataContent = 1;
    }

# Zeilenumbruch für Nicht-Inline-Tags
  newLine() unless $inlineTags{lc $tagname};

# MIME-Type für JavaScript setzen
  if ($tagname =~ /script/i ) {
# Die einzige Scriptsprache, die wir haben
    $attr->{"type"} = "text/javascript";
    }

# HTML-Attributnamen kleinschreiben
  foreach $key (sort keys %$attr) {
    $value = $attr->{$key};
    $value =~ s/<!--_bsp(\d+)-->/{@$bspParts[$1]}/ges;
    $key = lc $key unless $tagname =~ /:/;
# <script> wird mit type spezifiziert, nicht mit language
    next if $tagname eq "script" and $key eq "language";
    if ($deepPrint) {
# Lange Elemente: Werte zunächst in Hash aufnehmen
      $myAttributes{$key} = $value;
      }
    else {
# Werte anfügen
      if ($value eq "_BOOL_") {
        $atts .= qq( $key);
        }
      else {
        $atts .= qq( $key="$value");
        }
      }
    }

# Elementdaten merken
  push @stack, [ "start", $tagname, $text, $atts, \%myAttributes, outLength() ];

# Inline-Tag? Dann schliessenden Whitespace des letzten Text-Elements ausgeben
  purePrint(getTerminatingWhiteSpace(-1)) if $inlineTags{$tagname};

# Formatierte Ausgabe, falls kritische Länge überschritten oder der User einen Umbruch eingefügt hat
  if ($deepPrint) {
    deepPrintWithAttributes( $tagname, \%myAttributes );
    }
  else {
    deepPrint("<$tagname$atts>", $inlineTags{$tagname});
    }

# Attribute, die nicht geschlossen werden müssen, ändern nicht die Einrücktiefe
  if (not exists $nonClosingTags{$tagname} ) {
    incDepth();
    }
  }

# Behandelt Text und BSP-Scripting
sub text_handler {
  my ($self,$text) = @_;

# Text und aktuellen Offset merken
  push @stack, [ "text", $text, outLength() ];

# Text, der nur aus Leerraum besteht, ignorieren
  return if $text =~ /^\s*$/ && not $cdataContent;

# Schliessenden Leerraum entfernen
  $text =~ s/(\S)\s*$/$1/s unless $cdataContent;

# Ausgabe
  purePrint($text);

  }

# Endebehandlung
sub end_handler {

  my ($self,$tagname,$text) = @_;
  my $emptyContent = 0;
  my @start;

  $tagname = lc $tagname unless $tagname =~ /:/;

# Flag zurücksetzen, wenn ein CDATA-Element beendet wird
  $cdataContent = 0 if $cdataTags{$tagname};

# Innerhalb von CDATA-Inhalt: Text ausgeben, merken, fertig
  if ($cdataContent) {
    push @stack, ["text",$text, outLength()];
    purePrint($text);
    return;
    }

# Ermitteln, wieviele Newlines am Schluss des Inhalts stehen
  my $terminatingNewLines = getTerminatingNewLines();

# Ein BSP-Element schliesst sich selbst
  if ($tagname =~/:/ and emptyContent()) {
    if ( (@start = @{lastStart()}) &&
         $tagname eq $start[1] ) {
      decDepth();
# Ausgabe zurückspulen
      setOutLength($start[-1]);
      if ($start[3]) {
        deepPrint("<$start[1]$start[3]/>");
        }
      else {
        deepPrintWithAttributes( $start[1], $start[4], 1 );
        }
      return;
      }
    }

# Information auf Stack legen
  push @stack, ["end",$tagname, outLength()];

# Einrücktiefe
  decDepth() unless $nonClosingTags{$tagname};

# Schliessende Zeilenwechsel gemäss Elementinhalt
  newLine($terminatingNewLines) unless $inlineTags{$tagname};

  if ($terminatingNewLines) {
    deepPrint("</$tagname>");
    }
  else {
    purePrint("</$tagname>");
    }
  }

# Kommentarbehandler
sub comment_handler {
  my $buf = $_[1];
  if ($buf =~ s/<!--_bsp(\d+)-->/{@$bspParts[$1]}/ges ) {
    push @stack, ["scripting",$buf,outLength()];
    }
  else {
    push @stack, ["comment",$buf, outLength() ];
    }

# Schliessende Zeilenumbrüche des vorangehenden Textes reproduzieren
  $_=getTerminatingNewLines(-1);
  newLine(getTerminatingNewLines(-1));

  purePrint($buf);
  }

sub others {
# Alle nicht speziell behandelten Events: Merken, ausgeben
  my ($event, $text) = @_;
  push @stack, [$event,$text,outLength()];
  purePrint($text);
  }


# ----------------------------------------------------------------------
# Routinen für den Zugriff auf den Elementstack
# ----------------------------------------------------------------------

sub getTerminatingNewLines {
  my $off = shift || 0;
  return 0 if $off < -$#stack;
  my $newlines = getTerminatingWhiteSpace($off);
  $newlines =~ s/[^\n]//gs;
  return length($newlines);
  }

sub getTerminatingWhiteSpace {
  my $off = shift || 0;
  return 0 if $off < -$#stack;
# Stack auswerten: Wieviele Zeilenumbrüche
# enthielt das Element am Ende?
  my @tos = @{$stack[-1+$off]};
  return "" unless $tos[0] eq "text";
  $tos[1] =~ m/\S?(\s*)$/s ;
  return $1;
  }

sub emptyContent {
# Stack auswerten: Ist dieses Element leer?
  my @tos;
  for (reverse @stack) {
    @tos = @$_;
# Kommentare ignorieren
    next if $tos[0] eq "comment";
# Scripting im Elementinhalt: Also nicht inhaltleer
    return 0 if $tos[0] eq "scripting";
# Funktion wird vor dem Output von end in den Stack gerufen
# Trifft er auf ein Ende-Tag, ist klar, dass das Element einen
# Inhalt haben muss
    return 0 if $tos[0] eq "end";
    if ($tos[0] eq "text") {
# Nichtleere Texte sind ebenfalls ein Inhalt
      return 0 unless $tos[1] =~ /^\s*$/g;
      }
    elsif ($tos[0] eq "start") {
# Wenn wir hier vorbeikommen, enthielt das Element nur \s*
      return 1;
      }
    }
# Kein ausgewogener Element-Stack
  return 0;
  }

sub lastStart {
# Letztes Start-Element auf dem Stack ermitteln
  my @tos;
  for (reverse @stack) {
    @tos = @$_;
    return \@tos if $tos[0] eq "start";
    }
  return undef;
  }

sub lastElementType {
# Letzten Element-Typ zurückgeben
  my $off = shift || 0;
  return 0 if $off < -$#stack;
  my @tos = @{$stack[-1+$off]};
  return $tos[0];
  }

# ----------------------------------------------------------------------
# Eingerückter Druck
# ----------------------------------------------------------------------
{
# statische "private" Variable, Zugriff nur durch folgenden Subs
# Workarea (Kopfzeile) des Stacks
  my $wa = "";
# $depth bedeutet die aktuelle Einrücktiefe
  my $depth = 0;
  sub init_result {
    $wa = "";
    $depth = 0;
    }
# Zeile mit Einrückung ausgeben
  sub deepPrint {
    my ($text,$pure) = @_;
    if ($pure) {
      purePrint( $text );
      }
    else {
      purePrint( " " x $depth . $text );
      }
    }
  sub purePrint {
    $wa .= shift;
    }
  sub newLine {
    my $times = @_ ? shift : 1;
    $wa .= "\n" x $times;
    }
  sub getResult {
    my $result = $wa;
    return $result;
    }
  sub outLength() {
    return length($wa);
    }
  sub setOutLength() {
    $wa = substr( $wa, 0, shift);
    }
# Öffnendes Element mit Attribut-Alignment ausgeben
  sub deepPrintWithAttributes {
    my ($tagname, $atts, $closing) = @_;
    my $pre = " " x $depth;
    my $firstTime = 1;
    my $line = "$pre<$tagname";
    my $key;
    my $maxKeyLength = 0;
    my $value;
    purePrint( $line );
    $pre = "\n" . ( " " x (length( $line ) + 1) );
    foreach $key (keys %$atts) {
      if (length( $key ) > $maxKeyLength) {
        $maxKeyLength = length( $key );
        }
      }
    foreach $key (sort keys %$atts) {
      if ($firstTime) {
        $firstTime = 0;
        purePrint(" ");
        }
      else {
        purePrint( $pre );
        }
      $value = $atts->{$key};
      if ($value eq "_BOOL_") {
        purePrint( $key );
        }
      else {
        purePrint( $key .
              (" " x ($maxKeyLength - length($key))) .
              qq( = "$value") );
        }
      }
    purePrint($closing ?  "/>" : ">" );
    }

# Einrücktiefe erhöhen
  sub incDepth {
    $depth += $indentDepth;
    }
# Einrücktiefe erniedrigen, wenn möglich
  sub decDepth {
    $depth -= $indentDepth;
    if ($depth < 0) {
      $depth = 0;
      }
    }
  }  # Ende Print-Block

}  # Ende Parser-Block


# ----------------------------------------------------------------------
# Standard-Konfigurationen des BSP Pretty Printers
# ----------------------------------------------------------------------
sub standard_config {

my %conf;

# Tags, die nicht notwendig geschlossen werden müssen
  $conf{nonClosingTags}  =  ["input", "br", "p",
                             "textarea","area","hr",
                             "li","meta", "col",
                             "option","link","img"];
# Tags, die keinen Zeilenumbruch erfordern
  $conf{inlineTags}      =  ["b", "i", "tt","span"];
# Tags, deren Inhalt nicht geparsed, sondern als Text behandelt werden soll
  $conf{cdataTags}       =  ["script", "style"];
# Ab dieser Länge: Attribute aufspalten
  $conf{attBreakLimit}   = 80;
# Einrücktiefe für Schachtelungen
  $conf{indentDepth}     = 2;

\%conf;
}

1;

Der Pretty Printer

Nachdem nun auch die Ein- und Ausgabethemen sowie das Testen behandelt sind, kann ich mich endlich dem Herzstück, der PrettyPrint-Funktion selbst ausführlicher zuwenden. Es zeigt sich dass gerade die Programmiersprache Perl dank ihrer Flexibilität, ihrer Robustheit und vor allem ihres starken Befehlssatzes zur Zeichenkettenverarbeitung für Aufgaben dieser Art ideal geeignet ist. Auch steht uns dort - anders als in ABAP - ein HTML-Parser zur Verfügung.

Das Modul BSP::PrettyPrinter setzt die Komponente HTML::Parser mindestens im Release 3.0.0 voraus, was keine grossartige Einschränkung ist. Das Modul HTML::Parser macht keine Annahmen über die Struktur des zugrundeliegenden Dokuments. Das ist für BSP-Views, die ja meist nur HTML-Fragmente, keine vollständigen HTML-Dokumente darstellen, genau das Richtige. HTML::Parser löst einfach für jedes öffnende oder schliessende Tag, für jeden Kommentar und für jeden Textknoten, den es entdeckt, ein Ereignis aus, für das man sich mit einer Behandlerroutine registrieren kann.

Das Modul BSP::PrettyPrinter geht folgendermassen vor:

Die Details können dem unten folgenden Programmcode sowie dem obigen Testprogramm entnommen werden. Ich habe versucht, die am Anfang definierten Regeln zu implementieren. Der aktuelle Quelltext ist in meinem github Account zu finden

BSP Pretty Printer bei github

Achten Sie darauf, dass der Modul von Perl auch gefunden werden kann. Fügen Sie dafür den Ordner BSP und das enthaltene Modul PrettyPrinter.pm in einem Bibliothekspfad Ihrer Perl-Laufzeit ein. Wenn Sie einen eigenen Pfad bevorzugen, können Sie diesen zu Beginn des Programms bsp_pp.pl mit dem Pragma use lib <path> bekanntmachen.

# ----------------------------------------------------------------------
# BSP Pretty Printer
# ----------------------------------------------------------------------
# Doku siehe http://bsp.mits.ch/supplements/pretty.htm
# (C) Rüdiger Plantiko, Migros IT-Services (MITS), 7/2007
# ----------------------------------------------------------------------

package BSP::PrettyPrinter;

use HTML::Parser 3; # HTML::Parser mindestens in Version 3
use strict;
use Exporter;

our $VERSION = 0.9;
our @ISA     = qw(Exporter);
our @EXPORT  = qw(&pretty_print);

# ----------------------------------------------------------------------
# Pretty Print - einzige öffentliche Methode des Pakets
# Erwartet unformatierten String
# Gibt formatierten String zurück
# ----------------------------------------------------------------------
sub pretty_print($) {
  my @bspParts;
# BSP-Scripting vorläufig durch HTML-Kommentare ersetzen
  my $buf = substituteBsp(shift,\@bspParts);
# Transformation mithilfe des HTML::Parsers durchführen
  return transform( $buf, \@bspParts );
  }


sub substituteBsp( ) {

  my ($buf,$bspParts) = @_;

# ----------------------------------------------------------------------
# Erster Durchgang
# Ersetze alle BSP-Direktiven durch HTML-Kommentare <!--BSP1--> etc.
# Baue gleichzeitig einen Array mit den Originalausdrücken auf
# ----------------------------------------------------------------------
  my $bspCounter = 0;
  my $buf1 = "";
  my $posi=0;
  my $pre;
  my $spc;
  my $bspTag;

# Regulärer Ausdruck trifft alle Formen von Scripting
# <% ... %>, <%= ... %> und <%-- --%>
  while ( $buf =~ /(.*?)(\s*)(<%(.*?)%>)/gs) {
    ($pre,$spc,$bspTag) = ($1,$2,$3);
# Kontrolliert einen Zeilenumbruch einfügen
    if ($spc =~ /\n/ ) {
      $bspTag = "\n" . $bspTag;
      }
# Ersetzungsausdruck aufbauen, $bspCounter wird der Index
# im Array @$bspParts
    $buf1 .= $pre . "<!--_bsp$bspCounter-->" ;
    push( @$bspParts, $bspTag);
    $bspCounter++;
    $posi = pos($buf);
    }

# Den Schwanz (alles ab letztem gefundenen Scriptingausdruck)
# noch hintanstellen
  $buf1 .= substr( $buf, $posi);

  return $buf1;

  }

# ----------------------------------------------------------------------
# Zweiter Durchgang: HTML-Parser / Pretty Printer
# ----------------------------------------------------------------------

{

# Private Daten des HTML-Parsing-Blocks:

# Referenz auf den Stack der BSP-Ausdrücke
my $bspParts;

# Parser-Stack
my @stack;
my $result; # Ergebnis (neu)

# Tags, die nicht unbedingt geschlossen werden müssen
my %nonClosingTags;
# Tags, die keinen Zeilenumbruch erfordern
my %inlineTags;
# Tags, die stets Text enthalten (kein HTML)
my %cdataTags;
# Flag, dass wir gerade in einem solchen Tag sind
my $cdataContent;
# Kritische Länge, ab der die Attribute umgebrochen werden
my $attBreakLimit;
# Einrücktiefe
my $indentDepth;

sub transform {

  (my $buf1,$bspParts,my $config) = @_;

  my $bspOpen = 0;

# Initialisierungen
  do_init($config);
  init_result();

# Parser instanziieren, Handler definiern, parse() aufrufen
  my $parser = HTML::Parser->new(
    default_h => [ \&others,          'event,text' ],
    start_h   => [ \&start_handler,   'self,tagname,attr,text' ],
    end_h     => [ \&end_handler,     'self,tagname,text' ],
    text_h    => [ \&text_handler,    'self,text' ],
    comment_h => [ \&comment_handler, 'self,text' ]
    ) || die $!;
# Case Sensitive ist nötig wegen der BSP-Elemente
  $parser->case_sensitive( 1 );
# XML-Mode ist nötig wegen der BSP-Elemente
# Hilft, dass selbstschliessende Tags korrekt erkannt werden
  $parser->xml_mode( 1 );
  $parser->boolean_attribute_value( "_BOOL_" );
# Nun das Parsing durchführen...
  $parser->parse($buf1) || die $!;
# ... und beenden
  $parser->eof;

# Ausgabestring zurückgeben
  my $buf2 = getResult();
  $buf2 =~ s/^\n// unless $buf1 =~ /^\n/;
  return $buf2;

  }

# ----------------------------------------------------------------------
# Initialisierungen für das Parsing - Konfigurationseinstellungen
# ----------------------------------------------------------------------
sub do_init {
  my $config = shift;
  my $return;

# Selbstausführendes Konfigurationsfile ...
  if ($config) {
    unless ($return = do $config) {
      die "couldn't parse $config: $@"         if $@;
      die "couldn't do $config: $!"            unless defined $return;
      die "couldn't run $config"               unless $return;
      }
    }
  else {
# ... oder Konfigurationen aus diesem Script (siehe unten)
    $return = standard_config();
    }

# Tag-Mengen als Hashs aufbereiten
  %inlineTags     = map { ($_,1) } @{$return->{inlineTags}};
  %nonClosingTags = map { ($_,1) } @{$return->{nonClosingTags}};
  %cdataTags      = map { ($_,1) } @{$return->{cdataTags}};

# Zeilenzahl, ab der es einen Umbruch beim Attribut-Rendern gibt
  $attBreakLimit  = $return->{attBreakLimit};

# Einrücktiefe
  $indentDepth    = $return->{indentDepth};

  }

# ----------------------------------------------------------------------
# Ereignisbehandler für HTML::Parser
# ----------------------------------------------------------------------
# Öffnende Tags
sub start_handler {
  my ($self,$tagname,$attr,$text) = @_;
  my (%myAttributes,$key,$value,$prefix);
  my $atts="";

# Tags innerhalb von CDATA als Text behandeln
  if ($cdataContent) {
    purePrint( $text );
    push @stack, [ "text", $text ];
    return;
    }

# HTML-Tags ( = die ohne Namespace) kleinschreiben
  $tagname = lc $tagname unless $tagname =~ /:/;

# Merken, dass ein CDATA-Tag geöffnet wurde
  if (exists $cdataTags{$tagname}) {
    $cdataContent = 1;
    }

# Zeilenumbruch für Nicht-Inline-Tags
  newLine() unless $inlineTags{lc $tagname};

# MIME-Type für JavaScript setzen
  if ($tagname =~ /script/i ) {
# Die einzige Scriptsprache, die wir haben
    $attr->{"type"} = "text/javascript";
    }

# HTML-Attributnamen kleinschreiben
  foreach $key (sort keys %$attr) {
    $value = $attr->{$key};
    $value =~ s/<!--_bsp(\d+)-->/{@$bspParts[$1]}/ges;
    $key = lc $key unless $tagname =~ /:/;
# <script> wird mit type spezifiziert, nicht mit language
    next if $tagname eq "script" and $key eq "language";
    if (length( $text ) > $attBreakLimit ) {
# Lange Elemente: Werte zunächst in Hash aufnehmen
      $myAttributes{$key} = $value;
      }
    else {
# Werte anfügen
      if ($value eq "_BOOL_") {
        $atts .= qq( $key);
        }
      else {            
        $atts .= qq( $key="$value");
        }
      }
    }

# Elementdaten merken
  push @stack, [ "start", $tagname, $text, $atts, \%myAttributes, outLength() ];

# Inline-Tag? Dann schliessenden Whitespace des letzten Text-Elements ausgeben
  purePrint(getTerminatingWhiteSpace(-1)) if $inlineTags{$tagname};

# Formatierte Ausgabe
  if (length( $text ) > $attBreakLimit) {
    deepPrintWithAttributes( $tagname, \%myAttributes );
    }
  else {
    deepPrint("<$tagname$atts>", $inlineTags{$tagname});
    }

# Attribute, die nicht geschlossen werden müssen, ändern nicht die Einrücktiefe
  if (not exists $nonClosingTags{$tagname} ) {
    incDepth();
    }
  }

# Behandelt Text und BSP-Scripting
sub text_handler {
  my ($self,$text) = @_;

# Text und aktuellen Offset merken
  push @stack, [ "text", $text, outLength() ];

# Text, der nur aus Leerraum besteht, ignorieren
  return if $text =~ /^\s*$/ && not $cdataContent;

# Schliessenden Leerraum entfernen
  $text =~ s/(\S)\s*$/$1/s;

# Ausgabe
  purePrint($text);

  }

# Endebehandlung
sub end_handler {

  my ($self,$tagname,$text) = @_;
  my $emptyContent = 0;
  my @start;

  $tagname = lc $tagname unless $tagname =~ /:/;

# Flag zurücksetzen, wenn ein CDATA-Element beendet wird
  $cdataContent = 0 if $cdataTags{$tagname};

# Innerhalb von CDATA-Inhalt: Text ausgeben, merken, fertig
  if ($cdataContent) {
    push @stack, ["text",$text, outLength()];
    purePrint($text);
    return;
    }

# Ermitteln, wieviele Newlines am Schluss des Inhalts stehen
  my $terminatingNewLines = getTerminatingNewLines();

# Ein BSP-Element schliesst sich selbst
  if ($tagname =~/:/ and emptyContent()) {
    if ( (@start = @{lastStart()}) &&
         $tagname eq $start[1] ) {
      decDepth();
# Ausgabe zurückspulen
      setOutLength($start[-1]);
      if ($start[3]) {
        deepPrint("<$start[1]$start[3]/>");
        }
      else {
        deepPrintWithAttributes( $start[1], $start[4], 1 );
        }
      return;
      }
    }

# Information auf Stack legen
  push @stack, ["end",$tagname, outLength()];

# Einrücktiefe
  decDepth() unless $nonClosingTags{$tagname};

# Schliessende Zeilenwechsel gemäss Elementinhalt
  newLine($terminatingNewLines) unless $inlineTags{$tagname};

  if ($terminatingNewLines) {
    deepPrint("</$tagname>");
    }
  else {
    purePrint("</$tagname>");
    }
  }

# Kommentarbehandler
sub comment_handler {
  my $buf = $_[1];
  if ($buf =~ s/<!--_bsp(\d+)-->/{@$bspParts[$1]}/ges ) {
    push @stack, ["scripting",$buf,outLength()];
    }
  else {
    push @stack, ["comment",$buf, outLength() ];
    }

# Schliessende Zeilenumbrüche des vorangehenden Textes reproduzieren
  $_=getTerminatingNewLines(-1);
  newLine(getTerminatingNewLines(-1));

  purePrint($buf);
  }

sub others {
# Alle nicht speziell behandelten Events: Merken, ausgeben
  my ($event, $text) = @_;
  push @stack, [$event,$text,outLength()];
  purePrint($text);
  }


# ----------------------------------------------------------------------
# Routinen für den Zugriff auf den Elementstack
# ----------------------------------------------------------------------

sub getTerminatingNewLines {
  my $off = shift || 0;
  return 0 if $off < -$#stack;
  my $newlines = getTerminatingWhiteSpace($off);
  $newlines =~ s/[^\n]//gs;
  return length($newlines);
  }

sub getTerminatingWhiteSpace {
  my $off = shift || 0;
  return "" if $off < -$#stack;
# Stack auswerten: Wieviele Zeilenumbrüche
# enthielt das Element am Ende?
  my @tos = @{$stack[-1+$off]};
  return "" unless $tos[0] eq "text";
  $tos[1] =~ m/\S?(\s*)$/s ;
  return $1;
  }

sub emptyContent {
# Stack auswerten: Ist dieses Element leer?
  my @tos;
  for (reverse @stack) {
    @tos = @$_;
# Kommentare ignorieren
    next if $tos[0] eq "comment";
# Scripting im Elementinhalt: Also nicht inhaltleer
    return 0 if $tos[0] eq "scripting";
# Funktion wird vor dem Output von end in den Stack gerufen
# Trifft er auf ein Ende-Tag, ist klar, dass das Element einen
# Inhalt haben muss
    return 0 if $tos[0] eq "end";
    if ($tos[0] eq "text") {
# Nichtleere Texte sind ebenfalls ein Inhalt
      return 0 unless $tos[1] =~ /^\s*$/g;
      }
    elsif ($tos[0] eq "start") {
# Wenn wir hier vorbeikommen, enthielt das Element nur \s*
      return 1;
      }
    }
# Kein ausgewogener Element-Stack
  return 0;
  }

sub lastStart {
# Letztes Start-Element auf dem Stack ermitteln
  my @tos;
  for (reverse @stack) {
    @tos = @$_;
    return \@tos if $tos[0] eq "start";
    }
  return undef;
  }

sub lastElementType {
# Letzten Element-Typ zurückgeben
  my $off = shift || 0;
  return 0 if $off < -$#stack;
  my @tos = @{$stack[-1+$off]};
  return $tos[0];
  }

# ----------------------------------------------------------------------
# Eingerückter Druck
# ----------------------------------------------------------------------
{
# statische "private" Variable, Zugriff nur durch folgenden Subs
# Workarea (Kopfzeile) des Stacks
  my $wa = "";
# $depth bedeutet die aktuelle Einrücktiefe
  my $depth = 0;
  sub init_result {
    $wa = "";
    $depth = 0;
    }
# Zeile mit Einrückung ausgeben
  sub deepPrint {
    my ($text,$pure) = @_;
    if ($pure) {
      purePrint( $text );
      }
    else {
      purePrint( " " x $depth . $text );
      }
    }
  sub purePrint {
    $wa .= shift;
    }
  sub newLine {
    my $times = @_ ? shift : 1;
    $wa .= "\n" x $times;
    }
  sub getResult {
    my $result = $wa;
    return $result;
    }
  sub outLength() {
    return length($wa);
    }
  sub setOutLength() {
    $wa = substr( $wa, 0, shift);
    }
# Öffnendes Element mit Attribut-Alignment ausgeben
  sub deepPrintWithAttributes {
    my ($tagname, $atts, $closing) = @_;
    my $pre = " " x $depth;
    my $firstTime = 1;
    my $line = "$pre<$tagname";
    my $key;
    my $maxKeyLength = 0;
    my $value;
    purePrint( $line );
    $pre = "\n" . ( " " x length( $line ) );
    foreach $key (keys %$atts) {
      if (length( $key ) > $maxKeyLength) {
        $maxKeyLength = length( $key );
        }
      }
    foreach $key (sort keys %$atts) {
      if ($firstTime) {
        $firstTime = 0;
        purePrint(" ");
        }
      else {
        purePrint( $pre );
        }
      $value = $atts->{$key};
      if ($value eq "_BOOL_") {
        purePrint( $key );
        }
      else {
        purePrint( $key .
              (" " x ($maxKeyLength - length($key))) .
              qq( = "$value") );
        }
      }
    purePrint($closing ?  "/>" : ">" );
    }

# Einrücktiefe erhöhen
  sub incDepth {
    $depth += $indentDepth;
    }
# Einrücktiefe erniedrigen, wenn möglich
  sub decDepth {
    $depth -= $indentDepth;
    if ($depth < 0) {
      $depth = 0;
      }
    }
  }  # Ende Print-Block

}  # Ende Parser-Block


# ----------------------------------------------------------------------
# Standard-Konfigurationen des BSP Pretty Printers
# ----------------------------------------------------------------------
sub standard_config {

my %conf;

# Tags, die nicht notwendig geschlossen werden müssen
  $conf{nonClosingTags}  =  ["input", "br", "p",
                             "textarea","area","hr",
                             "li","meta", "col",
                             "option","link","img"];
# Tags, die keinen Zeilenumbruch erfordern
  $conf{inlineTags}      =  ["b", "i", "tt","span"];
# Tags, deren Inhalt nicht geparsed, sondern als Text behandelt werden soll
  $conf{cdataTags}       =  ["script", "style"];
# Ab dieser Länge: Attribute aufspalten
  $conf{attBreakLimit}   = 80;
# Einrücktiefe für Schachtelungen
  $conf{indentDepth}     = 2;

\%conf;
}

1;

Customizing

Sie können natürlich jederzeit den Programmcode ändern, wenn Ihnen etwas nicht gefällt. Einiges können Sie aber auch selbst konfigurieren. Schreiben Sie einfach Perl-Code, der ein Datenobjekt zurückliefert, das genauso aufgebaut ist wie das oben von der Routine standard_config zurückgegebene Datenobjekt. Es folgt ein Beispiel, um die Einrücktiefe auf drei statt auf zwei Zeichen zu setzen. Speichern Sie diesen individuellen Konfigurationscode und geben Sie den Dateinamen als zweiten Aufrufparameter dem Programm bsp_pp.pl. Dann wird statt der obigen Routine standard_config Ihre individuelle Konfiguration ausgeführt.
{
my %conf;

# Tags, die nicht notwendig geschlossen werden müssen
  $conf{nonClosingTags}  =  ["input", "br", "p",
                             "textarea","area","hr",
                             "li","meta", "col",
                             "option","link","img"];
# Tags, die keinen Zeilenumbruch erfordern
  $conf{inlineTags}      =  ["b", "i", "tt","span"];
# Tags, deren Inhalt nicht geparsed, sondern als Text behandelt werden soll
  $conf{cdataTags}       =  ["script", "style"];
# Ab dieser Länge: Attribute aufspalten
  $conf{attBreakLimit}   = 80;
# Einrücktiefe für Schachtelungen
  $conf{indentDepth}     = 3;

# Hash mit Konfigurationen auf den Stapel legen
  \%conf;
}

Feedback

Für Anregungen, Verbesserungsvorschläge (am besten solche, die Sie selbst bereits erprobt haben) oder Bug Reports zu obigem Programm bin ich dankbar!

Zurück