Die Substitions-Engine in der Büro-Am-Draht Library

Grundlagen

Die Substitutions-Engine besteht aus Die Syntaktische Grundlage der Substitutions-Engine sind neue "HTML"-Tags, die nur paarweise vorkommen dürfen. Der HTML-Text zwischen öffnendem und schliessendem Tag wird Block genannt. Der Block kann beliebige andere Substitutions-Tags enthalten, und natürlich beliebig viele "normale" HTML Tags. Fast alle Substitutionen operieren auf dem Block.

Die Substitutions-Tags haben Parameter, die teilweise defaults haben (und damit optional sind). Diese Parameter können mit doppelten oder einfachen Anführungszeichen gequoted werden (in HTML sind nur doppelte erlaubt).

Achtung! Innerhalb von Parameter-Werten sollte der Buchstabe "<" vermieden werden! ">" darf vorkommen; man muss als Expressions dementsprechend umschreiben.

Vordefinierte Tags

Die Substitutions-Engine definiert zur Zeit folgenden Tags:
ins expr=expr
ins var=name
ins param=name

Evaluiert die expr und fügt den entstandenen String in den laufenden Text ein (INSert). Der eingefügte String ersetzt den Block.

Als Expressions können beliebige Perl-Expressions auftreten. Variablen, die in proto oder use oder loop definiert worden sind, müssen in den Expressions mit "%" prefixed werden.

Wenn nur der Wert einer Variablen "foo" eingesetzt werden soll, kann anstelle von expr="%foo" das tag var=foo benutzt werden.

Wenn ein "param"-Parameter gesetzt ist, wird ein über PARAM definierter Block eingesetzt.

Im Falle eines Fehlers (weder expr- noch var-tag angegeben, oder Fehler beim Evaluieren der Tags) wird der ursprüngliche Block ausgegeben.

param name=name

Definiert einen "Param-Block". Hiermit können ganze HTML-Sektionen als Parameter übergeben werden. Param-Blöcke können von INS eingesetzt werden, sie werden von USE an die Instantiierung von Prototypen übergeben.

loop var=name begin=expr end=expr [inc=expr]

Der Code zwischen <LOOP> und </LOOP> wird mehrmals ausgeführt. Eine neue Variable var wird eingeführt und zählt von begin bis end in schritten von inc. Innerhalb der Schleife können Expressions auf diese Variable zugreifen; ausserhalb der Schleife ist die Variable nicht mehr definiert. inc hat einen default-wert von +1 bzw. -1.

foreach [var=name] [count=name] list=expr

Die Expression muss eine Referenz auf eine Liste liefern (z.B. [1..10]). Der Code zwischen <FOREACH> und </FOREACH> wird für jedes Element der Liste einmal ausgeführt. Eine neue Variable var wird eingeführt und hat jeweils den Wert des aktuellen Elements. Die Variable count hält den Listen-Index des aktuellen Elements. Beide Variablen, var und count, sind optional.

for vars=names init=expr cond=expr step=expr [max=expr]

Zuerst werden mehrere neue Variablen vars (eine komma-separierte Liste, die auch leer sein kann) eingeführt. Dann wird die init-expression ausgeführt; diese sollte (als "Nebeneffekt") die variablen initialisieren (der Rückgabewert der init-expression wird ignoriert). Der Schleifenblock wird wiederholt ausgeführt, solange die cond-expr "true" liefert. Nach jeder Ausführung des Blocks wird die step-expr evaluiert; als nebeneffekt sollte sie die variablen modifizieren.

Bei der for-schleife ist die Gefahr gegeben, eine Endlosschleife zu schreiben; um dies zu verhindern, bricht die Schleife nach max vielen Iterationen ab. Normalerweise ist max auf 100 gesetzt, man kann den Wert aber über den max-parameter explizit setzen.

proto name=name

Der Block zwischen <PROTO> und </PROTO> wird als "Prototyp" unter dem Namen definiert. Darauf folgende USE können darauf zugreifen.

use proto=name param1=value1 ... paramn=valuen

Instanziiert einen vorher definierten PROTO-Block. Beliebig viele Parameter definieren Variablen, auf die in dem instanziierten Block zugegriffen werden können. Diese Variablen sind lokal zur Instanziierung.

Innerhalb des Bodies können beliebig viele PARAM-Blöcke auftauchen, die innerhalb der Instanziierung sichtbar sind.

kill

Der Block zwischen <KILL> und </KILL> wird nicht ausgedruckt. KILL kann dazu benutzt werden, grössere Bereiche auszukommentieren. Wie alle anderen Tags kann auch KILL rekursiv aufgerufen werden.

if cond=expr

Falls die expr wahr ist, wird der Block zwischen <IF> und </IF> dargestellt; andernfalls wird er übergangen. Fallsexpr falsch ist, wird ein eventuell vorhandener ELSE-Parameter-Block ausgeführt.

include file=name

Das File mit dem angegebenen Namen wird textuell includiert und ersetzt den Text im Block. Der Name ist ein lokaler filename relativ zum Ausführungsdirectories des Skripts. Falls das File nicht gefunden wird (oder kein Name angegeben war), wird der Block ausgegeben.

Parameter substituieren

Nehmen wir einmal an, wir wollen einen Parameter in einem Tag berechnen. Ein typisches Beispiel ist die default-parameter-Setzung von Forms. Das könnte so aussehen:
<subs expr='defined(param("lala")) ? "<INPUT TYPE=text VALUE=". param("lala") . ">" : "<INPUT TYPE=text VALUE="foo bar">"></subs>
Das ist hässlich und aufwendig. Um derartiges zu vermeiden, gibt es eine parameter-substitutions-syntax:
<INPUT TYPE=text NAME="lala" ?VALUE="param('lala') || 'foo bar!' " >
Jeder Parameter, dessen name mit einem Fragezeichen beginnt, benennt eine Expression, die evaluiert wird, um den Wert des Parameters zu bestimmen. Wenn der Wert undefiniert ist, wird der Parameter fallengelassen.

Einen Haken hat die Sache: normalerweise werden "gewöhnliche" HTML-Tags von der Substitutions-Engine überhaupt nicht angesehen. Damit die Parameter des INPUT-Tag angesehen werden (und nur dann wirkt dieser Mechanismus), muss das Tag bei der Engine "angemeldet" werden. Dies geschieht im Skript mit der Zeile:

add_html_tag_no_body('input'); # _no_body, weil es ein tag ohne end-tag ist
Um Tags anzumelden, die ein End-Tag (und damit einen Body von HTML-Statements) haben, benutzt man
add_html_tag('table');

Tags selber definieren

Der eigentliche Sinn der Substitutions-Engine liegt darin, dass man mit relativ geringem Aufwand eigene Tags definieren kann. Ein Blick in DBAD.pm zeigt, wie dies geht.

Tutorial 1: Einfaches REPEAT

Nehmen wir an, wir wollen ein Tag <REPEAT> definieren, dass seinen Body N-mal wiederholt ausgiebt. Ein parameter "N" soll diesen Wert angeben und einen default von "3" haben.

Es muss genau eine Funktion geschrieben werden, die das Repeat ausführt. Diese Funktion könnte so aussehen:

01 sub action_repeat {
02   my ($action, $env) = @_;
03   my ($tag, $body, $args) = @$action;
04   my $n = eval_arg($args->{'n'}, $env, 3);
05   if ($n < 0) { # illegal number of repeat (negative)
06      $n = -$n;     # make it legal
07   }
08   for(my $i = 0; $i < $n; $i ++){
09      exec_html_tree ($body, $env);
10   }
11 }
Die beiden ersten Zeilen (2,3) sind stereotypsch bei allen HTML-Actions identisch. Die Action bekommt das aktuelle variablen-environment ($env) und die parameter uebergeben ($action). Die Parameter sind eine 3-elementige Liste, bestehend aus dem tag (wird nur benötigt, wenn ein action-handler für mehrere tags definiert wird), dem body, und den argumenten). Der Body ist ein html-parse-tree, dessen interne struktur uns nicht zu interessieren braucht; die argumente sind ein hash (assoziatives array), das zu jedem parameter den wert (als string) enthält.

Zeile 4 zeigt uns die convenience-Funktion eval_arg(string, env, default), die ein argument in einen wert umwandelt. Sollte der parameter nicht definiert sein, wird der default-Wert zurückgegeben.

Zeilen 5-7 sorgen dafür, dass der Schleifenzähler nicht negativ ist (dann würde die Schleife nicht terminieren).

Die Zeilen 8-10 schliesslich führen den html-parse-tree n-mal aus; hierdurch wird er ausgedruckt.

Nachdem der handler so definiert ist, muß er noch angemeldet werden:

add_html_tag('repeat', \&action_repeat);
führt das neue Tag "repeat" ein und definiert action_repeat als dessen handler-funktion.

Tutorial 2: REPEAT mit Environment

Das einfache Repeat definiert keine variable, so dass innerhalb der Schleife nicht auf den Schleifenzähler zugegriffen werden kann. Jetzt wird gezeigt, wie dies geht.
01 sub action_repeat {
02   my ($action, $env) = @_;
03   my ($tag, $body, $args) = @$action;
04   my $n = eval_arg($args->{'n'}, $env, 3);
05   my $var = $args->{'var'} || 'i';
06   if ($n < 0) { # illegal number of repeat (negative)
07      $n = -$n;     # make it legal
07   }
08   for(my $i = 0; $i < $n; $i ++){
10      my $local_env = env_new_scope($env);
11      env_set($local_env, $var, $i);
12      exec_html_tree ($body, $local_env);
13   }
14 }
Zeile 5 definiert jetzt einen neuen Parameter "var", dessen default-wert "i" ist. Var benennt die Variable, die den Schleifenzähler enthält.

In Zeile 10 wird das aktuelle variablen-environment um einen scope erweitert. In diesen scope wird die neue variable mit env_set hineingeschrieben.