Blog der Heimetli Software AG

HTML-Files zusammensetzen

Problem: in einem Web-Projekt mit mehreren Seiten gibt es gleiche Elemente wie Header und Footer auf jeder Seite. Wie kann man diese gemeinsamen Elemente einfach in die HTML-Seiten einfügen?

php

Fast alle Webserver kommen mit PHP. Es liegt also nahe, es mit PHP zu versuchen.

Das muss nicht einmal dynamisch gemacht werden weil PHP auch von der Kommandozeile aufgerufen werden kann.

Dieser Ansatz stellte sich als einfach zu realisieren und gut verständlich heraus.

Die Files wurden in Sourcen (.src) und Includes (.inc) unterteilt und sehen so aus:

header.inc

  <header>
   <nav>
    <ul>
     <li><a href="index.html">Home</a></li>
     <li><a href="about.html">About</a></li>
    </ul>
   </nav>
  </header>

footer.inc

  <footer>
   <p>Text im Footer</p>
  </footer>

index.src

<!DOCTYPE html>
<html lang="de">
 <head>
  <meta charset="utf-8">
  <title>Die Startseite</title>
 </head>
 <body>
<?php require("header.inc"); ?>
  <h1>index.html</h1>
  <p>Ein bisschen Text als Inhalt</p>
<?php require("footer.inc"); ?>
 </body>
</html>

Durch die Processing Instructions bleibt das HTML übersichtlich und sauber getrennt von den Anweisungen.

Aus diesen Files kann index.html wie folgt erzeugt werden:

php index.src

Wenn das Ergebnis per Redirection in ein File geschrieben wird, dann resultiert das gewünschte HTML-File:

php index.src > index.html

Unter Linux ist es auch kein Problem, mehrere Files mit einer Kommandozeile zu erzeugen:

for f in *.src; do php $f > ${f%.*}.html; done

m4

m4 ist ein selten gebrauchtes Unix-Tool. Es ist ein ausgewachsener Makroprozessor von dem in dieser Applikation bloss die Include-Funktion gebraucht wird.

Die .inc-Files bleiben genau gleich, nur das index.src ändert:

<!DOCTYPE html>
<html lang="de">
 <head>
  <meta charset="utf-8">
  <title>Die Startseite</title>
 </head>
 <body>
include(`header.inc')dnl
  <h1>index.html</h1>
  <p>Ein bisschen Text als Inhalt</p>
include(`footer.inc')dnl
 </body>
</html>

include macht das was der Name sagt, dnl bedeutet delete to Newline.

Und so wird das File erstellt:

m4 index.src > index.html

XInclude und XSLT

Eigentlich sollte dies die richtige Technik sein um die Files zu kombinieren.

Allerdings müssen die Files gültiges XML enthalten damit der Parser sie verarbeiten kann. Das Markup für diese Variante sieht so aus:

header.xml

<?xml version="1.0"?>
<header>
 <nav>
  <ul>
   <li><a href="index.html">Home</a></li>
   <li><a href="about.html">About</a></li>
  </ul>
 </nav>
</header>

footer.xml

<?xml version="1.0"?>
<footer>
 <p>Text im Footer</p>
</footer>

index.xml

<?xml version="1.0"?>
<html lang="de">
 <head>
  <meta charset="utf-8"/>
  <title>Die Startseite</title>
 </head>
 <body>
  <include xmlns="http://www.w3.org/2001/XInclude" href="header.xml"/>
  <h1>index.html</h1>
  <p>Ein bisschen Text als Inhalt</p>
  <include xmlns="http://www.w3.org/2001/XInclude" href="footer.xml"/>
 </body>
</html>

Dazu braucht es ein File das das XML transformiert:

html.xsl (Version für Windows)

<?xml version="1.0" encoding="iso-8859-1"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">

 <xsl:output method="html" encoding="utf-8" include-content-type="no" indent="yes"/>
 
 <xsl:strip-space elements="*"/>

 <xsl:template match="html">
  <xsl:text disable-output-escaping="yes">&lt;!DOCTYPE html&gt;&#x0A;</xsl:text>
  <xsl:copy>
   <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
 </xsl:template>
 
 <xsl:template match="@*|node()">
  <xsl:copy>
   <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="@xml:base"/>

</xsl:stylesheet>

Die Befehlszeile lautet:

java -jar \tools\saxon\saxon9he.jar -xi -o:index.html index.xml html.xsl

Eigentlich sollte das mit XSLT eine einfache Sache sein. Es ist aber gar nicht so einfach, den DOCTYPE richtig zu setzen. Den Trick dafür kannte ich schon, so dass das kein Hindernis war. Es stellte sich aber heraus, dass ich zwei Details übersehen hatte:

  • XInclude setzt ein xml:base-Attribut ins XML ein
  • Im erzeugten HTML gibt es viele Leerzeilen

Das erste Problem wird durch ein Pattern gelöst das das Attribut ignoriert.

Beim zweiten Problem kann man argumentieren dass die Leerzeilen den HTML-Parser gar nicht stören. Oder dass man die Input-Files halt ohne überflüssigen Whitespace schreiben soll. Und es ist denkbar, den Whitespace mit einem HTML-Minifier wegzuputzen.

Weil die anderen Versuche so gut geklappt hatten, wollte ich hier eine ähnliche Lösung haben. Mit xsl:strip-space habe ich die auch gefunden. Allerdings kann es passieren, dass durch das sture ersetzen von Whitespace zwischen Elementen auch Whitespace betroffen ist den es eigentlich im HTML braucht. Eine wirklich saubere Lösung ist dieser Ansatz also nicht.

Wie angedeutet, wurde das File auf Windows mit dem Saxon-Prozessor erzeugt. Der müsste zwar auch unter Linux laufen, aber dort habe ich es mit xsltproc probiert. Der motzte erst mal über XSLT 2.0, erzeugte ein HTML ohne Indentierung und setzte dafür eine altmodische HTML-Deklaration ein...

Fazit: die anderen Lösungsansätze sind besser.

cpp

Als alter C-Hacker kam ich natürlich auch auf die Idee, den C Preprozessor zu benutzen. Es stellte sich aber bald heraus, dass das keine gute Idee ist.

Als erstes kamen mir die Zeilennummern in die Quere die der cpp einsetzt. Das konnte ich aber durch eine Option abschalten.

Dann setzte er eine Menge vordefinierter Header und Symbole ins File ein. Dafür gab es eine weitere Option.

Aber dann stolperte er über die Quotes im HTML und meldete Syntax-Fehler. Da wurde es mir zu blöd und ich gab auf.

sed

Mit sed habe ich keinen guten Ansatz gefunden um die Files zusammenzusetzen. Einfügen von File header.inc nach Zeile n wäre zwar möglich, aber das war mir nicht flexibel genug.

Beim herumprobieren mit sed kam mir eine bessere Idee: awk

awk

Mit awk geht es einfach. Ich habe aus Faulheit die php-Files genutzt, was nicht wirklich vernünftig ist, aber es hat auf Anhieb geklappt:

awk -F'"' '{ if( $0 ~ /<?php/ ) { while( (getline line < $2) > 0 ) print line; close($2) } else { print } }' index.src