Der imperia View-Prozessor

Die Hauptmotivation für die Entwicklung des imperia View-Prozessors war Frustration über die damals existierenden Template-Engines, die es für Perl gab. Keine davon bot alle Features, die ich für das V im MVC-Framework von imperia unabdingbar fand. Einige der wichtigeren Designentscheidungen hinter dem View-Prozessor sind im Folgenden erläutert, um einen Einblick in die Anatomie eines modernene Template-Prozessors vermitteln.

Leider ist die Syntaxhervorhebung der folgenden Beispiele alles andere als perfekt, weil der auf dieser Site verwendete Highlighter Rouge imperia View-Templates nicht unterstützt.

Allgemeine Syntax

Wie viele andere Template-Engines kopiert der imperia View-Prozessor einfach die Eingabe in die Ausgabe, bis auf die Abschnitte, die durch eine Kombination aus geschweiften Klammern und Prozentzeichen markiert sind.

<div>
verbatim
{% VIEWCODE %}
verbatim
</div>

Die öffnenden und schließenden Trennsymbole sind eigentlich konfigurierbar. Jedoch ist dies ein eher esoterisches und selten genutztes Feature.

Verschlucken von Leerzeichen

Bei der Verwendung von Template-Engines werden häufig zusätzliche Leerzeichen in der Ausgabe generiert, was bei HTML zu Layoutproblemen führen kann. Der imperia View-Prozessor erlaubt genaue Kontrolle über die Verarbeitung von Leerzeichen um View-Instruktionen.

<div><a href="{% link %}">
    {%= "Beschriftung" -%}
  </a>
</div>

Mit einem zusätzlichen Bindestrich (-) versehene Trennsymbole sind gierig, und verschlucken in ihrer Richtung alle Leerzeichen, die sich im Weg befinden. Trennsymbole mit Gleichheitszeichen (=) sind sehr gierig, und verschlucken auch Zeilenumbrüche. Der obige Code produziert folgende Ausgabe:

<div><a href="http://inhalt.der.variablen.link">Beschriftung
  </a>
</div>

Die Grundidee hierfür ist von Template Toolkit übernommen, wurde aber erweitert.

Kommentare

View-Kommentare sind auch gültige HTML-Kommentare, beginnen allerdings mit <!--%, also inklusive der Prozentzeichen. Das hat einen großen Vorteil:

<!--% FIXME! Nicht getestet, weil hier keiner einen IE hat ... -->
  Beste Ergebnisse nur mit Internet Explorer.
Der Kommentar verschwindet in der Ausgabe. Das Ganze kann auch mit dem Verschlucken von Leerzeichen kombiniert werden:
<strong>
    <!--%= FIXME! Nicht getestet, weil hier keiner einen IE hat ... =%-->
    Beste Ergebnisse nur mit Internet Explorer.<!--%=
  %--></strong>
Ausgabe:
<strong>Beste Ergebnisse nur mit Internet Explorer.</strong>

View-Direktiven

Es gibt zwei Arten von View-Direktiven, body-lose Direktiven und Direktiven mit Body:

<ul>
{%- #foreach item (@{items}) =%}
  <li>{% item %}</li>
{%= #end -%}
</ul>

Dies ist ein Beispiel einer einfachen Direktive mit Body. Sie wird eingeleitet von #KEYWORD, in diesem Falle foreach. Abgeschlossen werden diese Direktiven mit dem jeweiligen #end.

Body-lose Direktiven sind an der doppelten Raute (##) zu erkennen:

<footer>
{% ##include('layout/footer.html') %}
</footer>

Wenn man so will, eine Analogie zu <br/>!

Ausdrücke

Alles andere innerhalb der Curly-Prozent-Kombinationen wird als Code in der Minisprache X evaluiert.

{% 
    faculty = 1;
    if (number != 0) {
        for (i = number; i > 0; --i) {
            faculty *= i;
        }
    }
%}
<span>Die Fakultät von {% number %} ist {% faculty %}.</span>

X ist eine Mischung aus JavaScript und Perl. Der Name wurde so gewählt, weil B, C und D schon vergeben waren.

Terme

Die Syntax von Termen ist sehr ähnlich zu JavaScript bzw. EcmaScript:

{% i %} <!--% Variable. -->
{% (a + b) * (a - b) %} <!--% Arithmetik. -->
{% document.head.title %} <!--% Hash-Lookups (oder Methodenaufrufe). -->
{% document[head][title] %} <!--% Exakt das Gleiche. -->
{% items[5] %} <!--% Sechstes Element des Arrays "items". -->
{% items.5 %} <!--% Noch einmal das Gleiche. -->
{% items["5"] %} <!--% Noch immer das Gleiche. -->
{% meta.getHeader('Content-Type') %} <!--% Methodenaufruf. -->
{% meta.getAuthor() %} <!--% Methodenaufruf ohne Argumente. -->
{% meta.getAuthor %} <!--% Das Gleiche wie oben. -->

Dem Renderer kompilierter View-Templates wird eine beliebig komplizierte Datenstruktur, payload (also Nutzdaten) genannt, übergeben. Terme werden gegen diese Datenstruktur ausgewertet. Die Ausgabe des Terms in Zeile 3 wird vom View-Prozessor - genauer gesagt vom X-Interpreter - so berechnet:

return $payload->{document}->{head}->{title}.

Funktionen

Die Syntax von Funktionsaufrufen ist wenig originell:

{% push(array, item1, item2, item3) %}
<span>{% time() %} Sekunden seit 1. Januar 1970, 00:00 GMT sind vergangen.</span>

Das sollte an Basisinformation genügen, um die folgenden Code-Schnipsel zu verstehen.

Interessante Features

Bevor ich begann, den imperia View-Prozessor zu entwickeln, hatte ich mit einer Vielzahl anderer Template-Engines gearbeitet. Sein Feature-Umfang ist daher eine Kombination fast aller Dinge, die ich an anderen Engines vermisst hatte.

Body-First-Head-Last-Rendering

Bei HTML-Templates hat man oft damit zu kämpfen, dass der Head vor dem Body gerendert wird, was zu umständlichen und unsauberen Lösungen verleitet.

Dies soll das folgende typische Beispiel eines HTML-Templates illustrieren:

<html>
  <head>
  {% ##include('layout/head.html') %}
  </head>
  <body>
  {% ##include(imperia.body.template) %}
  </body>
</html>

Der HTML-Head wird in Zeile 3 inkludiert, der Body in Zeile 6. Das Argument der Include-Anweisung für den Body ist hier eine Variable statt einer hartkodierten Zeichenkette, damit der Controller die Auswahl des Ausgabetemplates steuern kann.

Das zugehörige Template des helloWorld-Controllers könnte so aussehen:

<!--% Zuweisungen. -->
 {%= 
     imperia.stylesheets.push('/assets/css/hello.css');
     imperia.title = 'Hallo, Welt!' 
 -%}
<h1>{% imperia.title %}</h1>
<div>
Der eigentliche Seiteninhalt ...
</div>

Das für diesen View spezifische Stylesheet /assets/css/hello.css wird der Liste einzubindender Stylesheets in Zeile 3 zugefügt. Der Seitentitel wird in Zeile 4 gesetzt und in Zeile 6 ausgegeben. Für den Body funktioniert das ohne Probleme.

Nehmen wir an, die Datei layout/head.html sieht folgendermaßen aus:

<title>{% imperia.title %}</title>
{% #foreach stylesheet (@{imperia.stylesheets}) %}
 <link rel="stylesheet" href="{% stylesheet %}" />
{% #end %}

Sie wird vor dem Body-Template eingebunden. Daher ist die Variable imperia.title noch nicht gesetzt, und auch das view-spezifische Stylesheet fehlt noch in der Liste imperia.stylesheets.

Am Ende des Tages muss man Titel und Stylesheet im Controller setzen, was jedoch dem MVC-Paradigma zuwiderläuft. Der imperia View-Prozessor hält allerdings eine ebenso einfache wie elegante Lösung für dieses Problem bereit:

<html>
  {% #assign(body) %}
  {% ##include(imperia.body.template) %}
  {% #end %}
  <head>
  {% ##include('layout/head.html') %}
  </head>
  <body>
  {% raw(body) %}
  </body>
</html>

Der Body wird nicht sofort ausgegeben, sondern in die Variable body gerendert, und zwar bevor der Head gerendert wird. Die Views für den Body können daher beliebige Variablen für den Head setzen oder verändern.

Der Body wird in Zeile 13 ausgegen. Weil er typischerweise Markup enthält, wird er durch die Funktion raw() geleitet, die das Escaping von HTML-Sonderzeichen unterbindet.

Es handelt sich hierbei um ein häufig anzutreffendes Muster in imperia View-Templates, lächerlich einfach, jedoch sehr mächtig.

Skin-Fähigkeiten

Einer der Gründe, weshalb das Look & Feel von Applikationen in Templates hinterlegt wird, ist die Skinfähigkeit. Anwenderinnen können Templates ersetzen, um die Applikation an das Corporate Design des Unternehmens anzupassen. Das funktioniert aber nur bis zum nächsten Update der Software gut. Danach müssen die maßgeschneiderten Templates erneut angepasst werden, in der Regel von Hand. Die Alternative ist, dass der Hersteller der Softare auf ein Upgrade der Templates verzichtet.

Der imperia View-Prozessor wird daher immer mit einer Liste von Include-Verzeichnissen aufgerufen. Die Argumente jeder ##include-Direktive werden alle der Reihe nach in diesen Verzeichnissen gesucht. Das Standardverzeichnis von imperia ist stets das Letzte in dieser Liste.

Das erlaubt imperia als Hersteller der Software, die mitgelieferten View-Templates ohne Rücksicht auf Verluste zu überschreiben, weil Kunden ihre maßgeschneiderten Tempates in projektspezifischen Verzeichnissen speichern, die bei der Suche von inkludierten Templates Vorrang haben.

<body>
<img src="{% ##include('layout/logo.html') %}" width="200" height="100" />
...
{% ##include('scripts.html') %}
{% ##xinclude('custom-scripts.html') %}
</body>

Das Template layout/logo.html wird somit zuerst in allen projektspezifischen Verzeichnissen gesucht. Auf diese Weise lassen sich auch sehr leicht serverseitige Browserweichen realisieren, um die Ausgabe für verschiedene Zielplattformen wie mobile Endgeräte zu optimieren. Auch barrierefreie Versionen der Oberfläche können so mit serverseitiger Unterstützung generiert werden. Dafür müssen lediglich weitere Verzeichnisse dem Include-Pfad zugefügt werden.

##xinclude-Direktiven sind ein weiteres Beispiel für eine simple Möglichkeit zur Anpassung an individuelle Bedürfnisse. Sie funktionieren exakt wie ##include-Direktiven, übergehen allerdings Fehler ohne jede Warnung. Damit kann imperia zwanglos Eingriffsmöglichkeiten für individuelle Anpassungen bereitstellen. Die Datei custom-scripts.html (siehe Zeile 5 im obigen Beispiel) wird beispielsweise nur dann geladen, wenn sie gefunden wird. Sollte sie nicht existieren, passiert einfach nichts.

Rendering-Kontexte

Eine der Grundsatzentscheidungen beim Entwurf eines Template-Prozeossors ist die Frage ob Ausgabe standardmäßig escapet werden soll oder nicht. Aus Sicherheitsgründen (XSS-Attacken) ist standardmäßiges Escaping der bessere Ansatz.

Der imperia View-Prozessor wird in einem sogenannten Rendering-Kontext ausgeführt. Vorgabe für diesen Kontext ist HTML. Das führt dazu, dass das Ergebnis von View-Code vor der Ausgabe normalerweise HTML-escapet wird. Die Definition von Funktionen kann allerdings spezifieren, in welchen Kontexten die Ausgabe der jeweiligen Funktion als sicher betrachtet wird.

Die Funktion raw() zum Beispiel ist in allen Kontexten sicher. Sie kopiert daher einfach nur die Eingabe in die Ausgabe. Die Funktion escape_html ist nur im Kontext HTML sicher, während die Funktion escape_javascript sowohl im Kontext JavaScript als auch im Kontext HTML sicher ist.

Zur Zeit werden nur die Kontexte HTML und JavaScript unterstützt. Andere Kontexte können jedoch sehr einfach zugefügt werden.

Plug-In-Fähigkeit

Der imperia View-Prozessor ist mehr ein Framework, denn eine konkrete Template-Engine. Der genaue Funktionsumfang wird über zwei Plug-In-APIs festgelegt.

Zunächst einmal gibt es Plug-Ins für Direktiven, also diejenigen Anweisungen, die mit einfachen oder doppelten Rauten beginnen, wie zum Beispiel ##include, #assign or #macro (für Funktionsdefinitionen innerhalb von View-Templates). Es mag überraschend sein, dass auch #for, #foreach oder #if als Plug-Ins definiert sind. Mit diesem Plug-In-API lässt sich die Semantik der Sprache also nicht nur erweitern - beispielsweise könnte ein #switch-Anweisung programmerit werden - sondern die Definition der Sprache ließe sich auch ändern, indem ein Plug-In überschrieben wird.

Die zweite Plug-In-Schnittstelle dient der Bereitstellung von Bibliotheken. Funktionen wie raw(), escape_html() und so weiter werden in Bibliotheken (intern Import Realms genannt) organisiert. Die zu importierenden Bibliotheken werden beim Aufruf des View-Prozessors angegeben. Es ist so sehr einfach, die Funktionalität des View-Prozessors mit eigenen Plug-Ins zu erweitern.

Eine während der Entwicklungsphase häufig genutzter Import ist die Util-Bibliothek. In ihr werden die Funktionen dump(structure) (zur Darstellung komplizierter Datenstrukturen in menschlich lesbarer Form), warn(structure) (wie dump(), allerdings mit Ausgabe auf die Standardfehlerausgabe, also in der Regel das Error-Log des Web-Servers) und log(msg) zur Ausgabe beliebiger Meldungen mit der genauen Angaben der Herkunft im Quelltext) definiert.

Payload-Variablen-Scoping

Jedes View-Include bekommt seine eigene Kopie der Payload-Datenstruktur. Es handelt sich dabei allerdings nur um eine flache Kopie! Daraus resultiert, dass der Gültigkeitsbereich (scope) von Variablen für erfahrende Entwickler anfangs merkwürdig anmutet. Man gewöhnt sich aber nach kurzer Zeit daran.

{%= a = 'foo' =%}
{%= data.a = 'bar' =%}

Wert von a ist {% a %}.
Wert von data.a ist {% data.a %}.

{%- ##include('child.html') -%}

Wert von a ist {% a %}.
Wert von data.a ist {% data.a %}.

In Zeile 1 wird eine Variable a auf der ersten Ebene definiert, in der zweiten Zeile wird data.a auf der zweiten Verschachtelungsebene definiert. Zweite Ebene bedeutet hier, dass data.a eine Eigenschaft der Variablen data (des Objekts data in JavaScript) ist.

Das inkludierte View child.html sieht so aus:

{%= a = 'überschrieben' =%}
{%= data.a = 'überschrieben' =%}

Wert von a in child.html ist {% a %}.
Wert von data.a in child.html ist {% data.a %}.

Hier wird also genau das überschrieben, was im Elternview definiert wurde. Die daraus resultierende Ausgabe ist eventuell etwas überraschend:

Wert von a ist foo.
Wert von data.a ist bar.

Wert von a in child.html is überschrieben.
Wert von data.a in child.html ist überschrieben.

Wert von a ist foo.
Wert von data.a ist überschrieben.

Das bedeutet: Ein Include kann ab der zweiten Ebene alle Variablen des Eltern-Views manipulieren, aber nicht solche der ersten Ebene. Dies ist der Tatsache geschuldet, dass die Payload-Struktur des Kind-Views nur eine flache Kopie der Struktur der Eltern ist.

Was eine flache im Gegensatz zu einer tiefen Kopie ist, kann vielleicht besser in JavaScript erfasst werden:

var payload = {
    a: 'foo',
    data: {
        a: 'bar'
    }
};
var shallowCopy = {};
for (var key in payload) {
    shallowCopy[key] = payload[key];
}

console.log(shallowCopy.data.a === payload.data.a);

Ganz wie Perl kopiert auch JavaScript Skalare (Zeichenkettenliterale, Zahlen, ...) by value, also als Wert. Objekte bzw. Hashs und Arrays werden dagegen by reference also als Referenz kopiert. Daher zeigen shallowCopy.data.a und payload.data.a auf exakt die selbe Sache. Lässt man das Beispiel von node.js oder von der JavaScript-Konsole im Browser ausführen, sieht man dass die in der letzten Zeile erzeugte Ausgabe true ist.

Dieses leicht merkwürdige Variablen-Scoping mag wie ein Paradebeispiel dafür aussehen, wie ein Bug zum Feature erklärt wird. Zugegebenermaßen ist es erheblich billiger eine flache als eine tiefe Kopie einer Datenstruktur zu erzeugen. Das war jedoch nicht der Grund, sondern es handelte sich um eine bewusste Design-Entscheidung.

Eines der Ziele des View-Prozessor war, im Zweifel Intuivität vor Konsistenz zu setzen, damit die Sprache auch für Nicht-Programmierer, insbesondere Designer leicht zu verstehen ist. Es kristallisierte sich bald heraus, dass die Anwender intuitiv ein Verhalten erwarteten, wie es jetzt implementiert ist. Es wurde zum Bespiel solcher Code geschrieben:

<div>Eingeloggt als {% imperia.user.name %}</div>
<div>
Mails:
<ul>
{% #for (i = 0; i < imperia.mails.length; ++i) %}
  <li>imperia.mails[i].subject</li>
{% #end %}
</ul>
</div>

Es wurde erwartet, dass man bedenkenlos einfache Variablen wie i oder tmp benutzen und überschreiben konnte, ganz gleich, ob diese Variablen bereits im Eltern-View verwendet wurde. Andererseits wurde erwartet, dass eigene Variablen in inkludierten Views sichtbar waren und dort auch den gleichen Wert hatten. Das Verlangen, die etwas kryptischer aussehenden Variablen mit Punkten (imperia.user) oder eckigen Klammern (mails[i]) zu überschreiben, wurde dagegen praktisch nie geäußert.

Am Ende habe ich mich entschieden, dass die flache Kopie für Nicht-Programmierer intuitiver ist als ein Konzept von lokalen und globalen Variablen.

Do The Right ThingTM

Das Scoping von Variablen ist ein Beispiel, wo die Konsistenz der besseren Benutzbarkeit geopfert wurde. Die Code-Schnipsel des vorherigen Abschnitts zeigen jedoch noch zwei weitere Beispiele für diese Linie.

{% a = 'foo' %}

Diese Zuweisung erzeugt keine Ausgabe. Konventionellerweise ist der Wert einer Zuweisung allerdings der Wert der Variablen links des Zuweisungsoperators nach dem Zuweisung, und das obige Statement erzeugte den String foo in der Ausgabe. Anfangs wurden Zuweisungen immer so geschrieben:

{% a = 'foo'; '' %}

Der Wert des ganzen Ausdrucks war jetzt der leere String, aber das war umständlich zu schreiben. Ich beschloss daher, dass Zuweisungen in der X-Minisprache immer einen leeren String als Wert hatten. Das traditionelle Verhalten lässt sich auf zwei Arten erreichen:

{% a = 'foo'; a %}
or
{% a := 'foo' %}

Ein weiteres Beispiel, wo Intuitivität der Konsistenz vorgezogen wurde.

{% data.a = 'foo' %}

In den meisten Programmiersprachen müsste man hier zunächst die Variable data erzeugen. Diese muss dann als leerer Hash initialisiert werden, bevor den Hashkeys etwas zugewiesen werden kann. In X passiert das alles auf einmal und automatisch. Existiert eine Variable nicht, wird sie erzeugt. Falls sie wie ein Hash benutzt wird, wird sie als Hash erzeugt. Falls sie wie ein Array benutzt wird, wird sie als Array erzeugt.

Diese Autovivifizierung - übrigens von Perl übernommen - kann man als unsauber betrachten, aber meistens tut sie exakt das, was erwartet wird, und kann schwerlich zu Fehlern führen.

Der imperia View-Prozessor enthält eine Reihe weiterer Beispiele, wo die Beschwerden von Anwendern der ersten Stunde über umständliches Coding zu Änderungen an der Sprache führten.

Fehlerausgabe

Access denied connecting to mysql server at /var/www/shop/db.php:25!
The supplied user name was "root", the password was "Ken Sent Me".

Solche unterhaltsamen Fehlermeldungen kennt jeder, der ab und zu im Internet surft. Für Seitenbetreiber sind sie allerdings wenig spaßig, denn sie sind natürlich nur für die Entwicklerinnern einer Site und nicht für die Besucher gedacht.

Der imperia View-Prozessor verfügt über drei mögliche Fehlerkanäle. Vorgabe ist die Standardfehlerausgabe des Servers STDERR, normalerweise also das Error-Log des Web-Servers.

Für Entwicklersysteme oder Stagingserver können zwei weitere Kanäle konfiguriert werden. HTML fügt Fehlermeldungen als HTML-Kommentare in die Ausgabe ein und JAVASCRIPT gibt kleine JavaScript-Code-Schnipsel aus, welche die JavaScript-Funktion alert() mit der Meldung als Argument aufrufen. Die Fehlermeldungen erscheinen damit also als Dialogboxen.

Erwähnt werden sollte auch, dass Fehlermeldungen die genaue Position im Quelltext enthalten, also den Dateinamen mit Zeilennumer und Spalte. Man wird also selten nach vergessenen Semikolons suchen müssen.

Internationalisierung

Die Bibliothek I18N, die standardmäßig importiert ist, stellt die vollständige Schnittstelle von Gettext im View-Prozessor zur Verfügung, und erlaubt damit die Verwendung ihrer mächtigen Übersetzungsfunktionen inklusive Pluralbehandlung und Übersetzungskontexte. Die Syntax ist denkbar einfach:

<h1>{% __('Fatal Error!') %}</h1>

<div>
{% __x('Error deleting "{file}": {error}!',
       file => filename, error => syserror) %}
</div>

Das reicht für die ganze überwiegende Zahl an Anwendungsfällen aus. Einfache Meldungen werden in Aufrufe der Funktion __() gepackt, und Meldungen mit interpolierten, benannten Variabeln in __x().

Das Symbol => ist übrigens das von Perl bekannte fat comma (fettes Komma). Es hat die gleiche Funktion wie ein normales Komma, behandelt das linksseitige Argument allerdings als String in einfachen Anführungszeichen.

Kompilierung bei Bedarf

Der View-Compiler wird entweder mit einer einfachen (skalaren) Zeichenkette oder mit einer Referenz auf eine Zeichenkette als Argument aufgerufen. Stringreferenzen werden als Referenz zu Viewcode interpretiert, normale Strings als Dateiname eines View-Templates, das dann relativ zur Liste der Include-Verzeichnisse gesucht wird.

Bei der Übergabe des Namens einer Templatedatei passiert allerdings überraschenderweise praktisch nichts. Der Perl-Code, der dieses Template kompiliert, sieht tatsächlich ungefähr so aus:

$code = \"{\% include('$filename') \%}";

Er generiert lediglich eine Include-Anweisung und kehrt dann sofort zurück. Nichts wird geparset, nichts wird übersetzt. Es wird noch nicht einmal geprüft, ob die Template-Datei überhaupt existiert. Alle Direktiven werden erst dann ausgeführt und ausgewertet, wenn dies für das Rendern der Ausgabe notwendig ist.

{% #if ('x' == 'x') %}
  {% ##include('root.html') %}
{% #else %}
  {% ##include('does/not/exist.html') %}
{% #endif %}

Die Include-Anwesung für does/not/exist.html wird hier also niemals ausgeführt, weil die If-Bedingung immer wahr ist.

Der Parser bzw. Compiler cachet die kompilierten Syntaxbäume aller Templates übrigens sehr aggressiv im Speicher, und zwar bis zu einer konfigurierbaren Maximalgröße. Für Entwicklungszwecke kann die Caching-Strategie dergestalt angepasst werden, dass zuerst der Zeitstempel der jeweiligen Templatedatei geprüft wird, bevor ein kompilierter Syntaxbaum aus dem Cache zurückgegeben wird.

 

Die kleine Tour durch die Features des imperia View-Prozessors endet hier. Keines der vorgestellten Features stellt Raketentechnik dar, viele von ihnen sind heute auch in anderen Template-Engines verfügbar. Als ich den View-Prozessor 2008 zu entwickeln begann, war das für viele Features allerdings noch nicht der Fall.

In seiner Gesamtheit macht der Umfang an Features den View-Prozessor zu einem mächtigen Werkzeug, mit dem sich intelligente Views mit komplexen Features abbilden lassen, und die ihn zu einem Vertreter seiner Art machen, der sich hinter keiner anderen Template-Engine verstecken muss.


blog comments powered by Disqus