Webtechnologien

(Auszug aus "Schematron - Effiziente Business Rules für XML-Dokumente", Kapitel 8)

Eines der wichtigsten Ziele bei der Entwicklung von HTML, war es, einen breiten Anwenderkreis auf eine möglichst einfache und fehlertolerante Weise zu befähigen, eigene HTML-Seiten zu schreiben. Das ist auch ein Grund dafür, dass Browser so ziemlich jeden beliebigen Code interpretieren und darstellen können und nicht bei inhaltlich oder syntaktisch fehlerhaften Inhalten die Anzeige verweigern. Mit der Einführung von XML wurde HTML zu XHTML, das seitdem den XML-Wohlgeformtheitsregeln entsprechen muss. Neben den vom Standard festgelegten validierbaren Regeln gibt es zusätzlich noch eine Reihe von Vorgaben, die beim Schrei­ben von XHTML-Seiten eingehalten werden sollten. Es handelt sich dabei um sogenannte Gestaltungshinweise, die auch vom W3C veröffentlicht wurden. Einige dieser Hinweise sollen dafür sorgen, dass Browser und Suchmaschinen die Seite optimal verarbeiten und indexieren können. Darüber hinaus gibt es noch Gestaltungsgrundsätze für das Layout, die eine möglichst gute Lesbarkeit bewerkstelligen sollen oder bestimmten Gruppen von Nutzern, wie z.B. Sehbeeinträchtigten den Zutritt zum WWW ermöglichen.

Viele dieser Gestaltungsempfehlungen können nicht mit den vom W3C herausgegebenen DTDs überprüft werden. Einige Regeln sind aus technischen Gründen nicht mit DTDs oder XML-Schemata überprüfbar, andere hätten zwar durch eine Erweiterung der XHTML-DTD eingefügt werden können, was aber die Erstellung von Internetseiten weiter erschwert hätte.

Im Folgenden werden nun einige dieser Regeln vorgestellt und exemplarisch mit Hilfe von Schematron überprüft.

Das lang-Attribut

In XHTML gibt es das Universalattribut lang, das angibt, in welcher Sprache der Inhalt einer Seite gehalten ist. Das Attribut enthält als Wert ein Sprachkürzel (nach RFC 17665). Zudem kann es in fast allen XHTML-Elementen verwendet werden, um beispielweise ein Zitat in einer anderen Sprache zu kennzeichnen. Wichtig wird die Verwendung von Sprachattributen im Zusammenhang mit Barrierefreiheit und dem Einsatz von Screenreadern. Ist die Sprache nicht korrekt angegeben, wird die Aussprache des Screenreaders so erfolgen, dass man Probleme haben wird, den Text zu verstehen.

Da XHTML ein XML-Dokument ist, kann man die Sprache mittels XML-Universalattribut xml:lang angeben. Das W3C empfiehlt für XHTML-Dokumente im Hinblick auf den Einsatz von Screenreadern, sowohl das lang-Attribut als auch das xml:lang-Attribut zu verwenden und beiden Attributen den gleichen Wert zuzuweisen. Bei der Erstellung von XHTML-Dokumenten kann es deshalb zu Fehlern bzw. Konflikten kommen, wie folgendes Beispiel zeigt:

 

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Zitate Niccolò Machiavelli</title>
  </head>
  <body>
    <h1>Zitate <span lang="it">Niccolò Machiavelli</span></h1>
    <p>Man kann einen Krieg beginnen, aber niemals beenden, wenn man will.</p>
    <p lang="it" xml:lang="de">Può la disciplina nella guerra più che il furore.</p>
  </body>
</html>

Das obige Beispiel stellt ein XHTML-konformes Dokument dar, enthält jedoch einige Schwächen bei der Verwendung der lang-Attribute. Zum einen fehlen die Attribute lang und xml:lang im root-Element. Darüber hinaus sind in einem Fall die Attributwerte nicht identisch, in einem anderen Fall fehlt das Attribut xml:lang.

Das folgende Schematron-Schema prüft XHTML-Dokumente auf diese Ungenauigkeit bei der Angabe der Sprachattribute:

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron">
  <ns uri="http://www.w3.org/1999/xhtml" prefix="html"/>
  <pattern>
    <rule context="html:html">
      <assert test="@lang">Das Attribut "lang" fehlt.</assert>
      <assert test="@xml:lang">Das Attribut "xml:lang" fehlt.</assert>
    </rule>
  </pattern>
  <pattern>
    <rule context="*[@xml:lang and @lang]">
      <assert test="@lang = @xml:lang">Die Werte der Attribute "lang" und "xml:lang" sollten identisch sein.</assert>
    </rule>
    <rule context="*[@lang]">
      <assert test="@xml:lang">Das Attribut "xml:lang" fehlt.</assert>
    </rule>
    <rule context="*[@xml:lang]">
      <assert test="@lang">Das Attribut "lang" fehlt.</assert>
    </rule>
  </pattern>
</schema>

Jedem Dokument sollte an zentraler Stelle (nämlich über das <html>-Element) eine Sprache zugewiesen werden. Davon abweichende Sprachangaben im Dokument können dann im entsprechenden Element festgelegt werden. In jedem Fall sollten zu diesem Zweck immer die beiden Attribute lang und xml:lang mit identischen Attributwerten verwendet werden. Die ISO-Standards 3166 oder 639 beschreiben registrierte Länderkürzel, die als Werte verwendet werden können.

Die Überschriften

In XHTML werden bekanntermaßen die Überschriften mit den Elementen <h1> bis <h6> ausgezeichnet. Dabei sollen die Überschriften nicht nur dazu dienen, eine bestimmte Darstellung zu erreichen, sie sollen auch logisch korrekt angeordnet sein. Das bedeutet, dass direkt auf eine Überschrift <h1> wieder eine Überschrift <h1> oder <h2> folgen sollte; nicht aber eine Überschrift <h3>.

Vielleicht fragen Sie sich, wozu das Ganze gut sein soll?
Überschriften dienen sehbeeinträchtigten Menschen zur Navigationshilfe in einem Dokument. Sie stellen also eine Gliederung dar, mit der der Benutzer eines Kapitels zum nächsten springen kann, was natürlich nur dann funktioniert, wenn Überschriften auch korrekt verwendet wurden und nicht beispielweise mit <h6> ein Bildtitel ausgezeichnet wurde. Ein weiterer Grund, sich neben der Validität an zusätzliche Regeln zu halten ist, dass Dokumentinhalte Jahre später unter Umständen in einem anderen Kontext und in einer anderen XML-Grammatik wiederverwendet werden. Da hierzu Überschriftenhierarchien ausgewertet werden müssen, kann es zu massiven Problemen bei einer möglichen Transformation kommen und eine manuelle Nachbearbeitung der Daten ist oft unabwendbar.

Hier ein Beispiel für eine falsch strukturierte XHTML-Datei.

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Böse Hierarchie</title>
  </head>
  <body>
    <h1>1. Einführung</h1>
    <h4>1.1 Vorwort</h4>
  </body>
</html>

Auch bei dieser Problematik kann uns Schematron behilflich sein. Die folgenden Rules zeigen exemplarisch, wie eine solche Prüfung funktionieren kann.

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
  <ns prefix="html" uri="http://www.w3.org/1999/xhtml"/>
  <pattern>
    <!--Die folgende Regel prüft, ob die erste Überschrift des Dokuments als h1 getaggt wurde.-->
    <rule context="html:body">
      <let name="firstDescendantHeading" value="descendant::node()[name()=('h1','h2','h3','h4','h5','h6')][1]"/>
      <assert test="$firstDescendantHeading/self::html:h1">Die erste Überschrift in einem Dokument muss h1 sein.</assert>
    </rule>
    <!--Die Grundüberlegung: h1- und h2-Elemente können niemals eine Überschriftenebene übersprungen haben. Erst ab der dritten Hierarchiestufe kann dieser Fall eintreten. Die erste Regel überprüft dementsprechend alle h3-Elemente darauf, ob die nächst vorangegangene Überschrift eine h1 ist (denn alle anderen Überschrift-Typen können richtig sein).-->
    <rule context="html:h3">
      <let name="firstPrecedingHeading" value="preceding::node()[name()=('h1','h2','h3','h4','h5','h6')][1]"/>
      <report test="$firstPrecedingHeading/self::html:h1" diagnostics="HierarchyCrashLevel1">Eine Überschriftenebene wurde ausgelassen: Eine h3 darf nicht auf eine <value-of select="$firstPrecedingHeading/name()"/> folgen.</report>
    </rule>
    <!-- Die nächste Regel geht analog zur vorherigen vor. Sie überprüft alle h4-Elemente auf zwei Bedingungen. Erstens: Die nächst vorangegangene Überschrift darf keine h1 sein. Zweitens: Die nächst vorangegangene Überschrift darf auch keine h2 sein. Mit Hilfe der Diagnostics bekommt der Benutzer eine konkrete Handlungsanweisung, um das Problem zu lösen. Ihm wird in diesem Fall vorgeschlagen, das fehlerhafte Tag zu ändern, sodass die Hierarchie wieder stimmt.-->
    <rule context="html:h4">
      <let name="firstPrecedingHeading" value="preceding::node()[name()=('h1','h2','h3','h4','h5','h6')][1]"/>
      <report test="$firstPrecedingHeading/self::html:h1" diagnostics="HierarchyCrashLevel1">Zwei Überschriftenebenen wurden ausgelassen: Eine h4 darf nicht auf eine <value-of select="$firstPrecedingHeading/name()"/> folgen.</report>
      <report test="$firstPrecedingHeading/self::html:h2" diagnostics= "HierarchyCrashLevel2">Eine Überschriftenebene wurde ausgelassen: Eine h4 darf nicht auf eine <value-of select="$firstPrecedingHeading/name()"/> folgen.</report>
    </rule>
  </pattern>
    <diagnostics>
      <diagnostic id="HierarchyCrashLevel1">Ändern Sie das Element für die Überschrift '<value-of select="."/>' in h2, um einen Unterabschnitt von '<value-of select="$firstPrecedingHeading"/>' einzuleiten!</diagnostic>
      <diagnostic id="HierarchyCrashLevel2">Ändern Sie das Element für die Überschrift '<value-of select= "."/>' in h3, um einen Unterabschnitt von '<value-of select="$firstPrecedingHeading"/>' einzuleiten!</diagnostic>
    </diagnostics>
</schema>

Die erste Regel prüft, ob die erste Überschrift des Dokuments als <h1> getaggt wurde. Dazu einige weitere Überlegungen: Wenn die erste Regel erfüllt ist, können <h1>- und <h2>-Elemente niemals eine Überschriftenebene übersprungen haben. Erst ab der dritten Hierarchiestufe kann dieser Fall eintreten. Eine weitere Regel überprüft dementsprechend alle <h3>-Elemente darauf, ob die vorangegangene Überschrift eine <h1> ist (denn alle anderen Überschrift-Typen können richtig sein).

An dieser Stelle greifen wir zum ersten Mal auf XPath-2.0-Funktionen zurück. Deshalb wird hier das queryBinding-Attribut benötigt, das mit dem Wert xstl2 den Schematron-Parser anweist, Funktionen aus XPath 2.0 auszuwerten. Mehr zu den möglichen Werten von queryBinding, siehe Element <schema> der Schematron-Referenz.

Der Titel der Seite

Das <title>-Element definiert den Dokumenttitel und wird von Browsern in der Titelzeile des Anzeigefensters angezeigt. Zudem wird dieser Titel auch von Suchmaschinen in den jeweiligen Ergebnislisten dargestellt. Inhaltlich sollte daher ein sinnvoller Titel der Seite gewählt werden, der auch gleichzeitig nicht zu lang sein darf. Als Richtlinie für die Länge gelten ca. 60 bis 70 Zeichen. Ist der Titel zu lang, wird er in den Ergebnislisten von Suchmaschinen nicht mehr komplett angezeigt, wie das folgende Beispiel zeigt.

Suchmaschinen-Resultat

Abbildung: Suchmaschinen-Resultat

Der Treffer hat eigentlich den Titel: „Ihre Spezialisten für XML XSL-FO - WordML - XSL - XSLT - XML aus einer Hand | Data2Type“, aber da der Titel mehr als 70 Zeichen enthält, wird unglücklicherweise nur noch das letzte Wort »Data2Type« angezeigt.

Das folgende Beispiel enthält zwei mögliche Fehlerquellen, die mit Hilfe von Schematron überprüft werden können.

<html xmlns="http://www.w3.org/1999/xhtml" lang="de" xml:lang="de">
  <head>
    <title>Ungeeignetes title-Element mit viel zu vielen Zeichen, es handelt sich hierbei um über 70 Zeichen <!-- verbotener Kommentar --></title>
  </head>
  <body> <!-- Content --> </body>
</html>

Neben einer angemessenen Anzahl der Zeichen in einem Titelelement sollte darin auch kein Kommentar auftreten. Das folgende Pattern überprüft dies:

<pattern>
  <rule context="html:title">
    <report test="comment()"> In title-Elementen sollen keine Kommentare vorkommen.</report>
    <report test="self::node()[string-length() &gt; 70]">Der Text im title-Element ist zu lang.</report>
    <report test="normalize-space(text()) = ''">Es fehlt der Titel-Text...</report>
  </rule>
</pattern>

XHTML-Tabellen I

XHTML-Tabellen sind wegen ihrer häufigen Verwendung im gesamten Publishingbereich (Online und Print) immer ein problembehaftetes Thema, bei dem es eine ganze Reihe an möglichen Schwierigkeiten gibt, die nicht durch ein XML-Schema oder eine DTD überprüfbar sind.

Ein weiteres Problemfeld sind die Breitenangaben von Spalten, Zellen und der gesamten Tabelle. Angaben dazu können widersprüchlich sein: beispielsweise, wenn der Wert des width-Attributs einer Tabelle 100 Prozent beträgt, aber die Summe aller width-Attribut-Angaben in den <col>-Elementen nicht genau auf 100 Prozent kommt. Die korrekte Darstellung durch einen Browser kann dann unter Umständen nicht mehr sichergestellt werden.

Es sei hier darauf hingewiesen, dass auch Schematron bei einer solchen Problemstellung an seine Grenzen stößt, weil die Anzahl der Fehlermöglichkeiten und deren Komplexität und Wechselwirkungen eine generische Lösung erschwert. Als Beispiel soll hier die Verwendung von verschiedenen Maßeinheiten für die Breitenangaben dienen. Wenn Prozent-, Pixel- und eventuell noch Punkt-Angaben gemischt verwendet werden und es dann nur von der Umgebungsbreite abhängt, ob die Gesamttabellenbreite eingehalten wird, ist eine Überprüfung mittels Schematron aufgrund der Vielzahl an Regeln, die interagieren, kaum noch sinnvoll möglich.

Der folgende Ausschnitt aus einer XHTML-Tabelle steht stellvertretend für Probleme bei dieser Tabellenthematik.

…
<table>
  <col valign="top" width="19%"/>
  <col valign="top" width="82%"/>
  <thead>
    <tr>
      <th>XSLT</th>
      <th>Schematron</th>
    </tr>
  </thead>
  <tbody>
    <tr>…</tr>
…

Die Summe der Breiten aller Spalten ist größer als 100 Prozent!

Das folgende Schematron-Pattern überprüft, ob die Summe der Breitenangaben in den col-Elementen der Spalten 100 Prozent übersteigt:

<pattern>
  <rule context="html:table">
    <let name="colWidthSummarized" value="number(sum(for $colWidth in html:col/@width return number(substring-before($colWidth,'%'))))"/>
    <assert test="$colWidthSummarized &lt;= 100">Die Summe der Attributangaben @width in allen Spaltendefinitionen col ist mit <value-of select="$colWidthSummarized"/>% größer als 100%!</assert>
  </rule>
</pattern>

XHTML-Tabellen II

Die Prüfung von Zellenüberspannungen beim Einsatz von Tabellen ist ein genauso komplexes wie häufiges Unterfangen beim Umgang mit Web- und Printdokumenten. Beim Einsatz von XHTML-Tabellen sollten für die korrekte Darstellung mindestens folgende Anforderungen eingehalten werden:

  • Alle Zeilen müssen gleich viele Zellen enthalten.
  • Überspannende Zellen dürfen sich nicht überschneiden.

Folgendes Beispiel enthält Fehler, die mit den üblichen Schema-Sprachen nicht überprüft werden können:

<table border="1">
  <tbody>
    <tr>
      <td rowspan="2">A1</td>
      <td>B1</td>
      <td>C1</td>
      <td rowspan="3">D1</td>
      <td>E1</td>
    </tr>
    <tr>
      <td>B2</td>
      <td>C2</td>
      <td>E2</td>
      <td>F2</td>
    </tr>
    <tr>
      <td>A3</td>
      <td>B3</td>
      <td colspan="2">C3</td>
      <td>E3</td>
    </tr>
  </tbody>
</table>

XHTML-Tabelle mit Zellüberspannungen

Abbildung: XHTML-Tabelle mit Zellüberspannungen

Die zweite Reihe enthält eine Zelle mehr als die anderen Reihen, obwohl diese weniger <td>-Elemente enthält als die erste Zeile. Die Zellen C3 und D1 überschneiden sich, da C3 die Spalten drei und vier überspannt. D1 steht in der Spalte vier und überspannt die Zeilen 1 bis 3.

Auch Schematron stößt bei einer solchen Überprüfung an seine Grenzen. Zur Umsetzung müssen selbstdefinierte XSLT-Funktionen in das Schematron-Schema eingebunden werden, die rekursiv aufgerufen werden.

Funktionen, die mit XSLT 2.0 mit dem Element <xsl:function> erzeugt werden, können in ein Schematron-Schema eingebunden werden, wenn sie als Top-Level-Element eingefügt werden. Innerhalb von <xsl:function> sind nun alle XSLT-Elemente erlaubt. Dabei muss beachtet werden, dass das <xsl:function>-Element und alle anderen verwendeten XSLT-Elemente dem Namensraum von XSLT angehören.

Während der XSLT-Namensraum im <schema>-Element angegeben wird, muss der Namensraum der Funktion und das zugewiesene Präfix mit dem Schematron-Element <ns> angegeben werden. Dann kann die Funktion in allen XPath-Ausdrücken des Schematron-Schemas verwendet werden.

<schema xmlns:html="http://www.w3.org/1999/xhtml" xmlns="http://purl.oclc.org/dsdl/schematron" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" queryBinding="xslt2">
  <ns prefix="d2t" uri="http://www.data2type.de/html"/>
  <ns prefix="html" uri="http://www.w3.org/1999/xhtml"/>
  <xsl:function name="d2t:getColAndRows">
    <xsl:param name="preCells"/>
    <xsl:param name="cells"/>
    <xsl:variable name="cellsWithRows">
      <xsl:choose>
        <!-- wird zur Initialisierung beim ersten Aufruf der Funktion verwendet: -->
        <xsl:when test="$cells[not(@d2t:rowNumber)] | $cells[not(@colspan|@rowspan)]">
          <xsl:for-each select="$cells">
            <xsl:copy>
              <xsl:attribute name="d2t:rowNumber" select="count(../preceding-sibling::html:tr | ../parent::*[not(self::html:table)]/preceding-sibling::*/html:tr) + 1"/>
              <xsl:attribute name="colspan" select="'1'"/>
              <xsl:attribute name="rowspan" select="'1'"/>
              <xsl:attribute name="d2t:tableID" select="generate-id(./ancestor::html:table[1])"/>
              <xsl:attribute name="d2t:cellID" select="generate-id()"/>
              <xsl:copy-of select="@*"/>
              <xsl:apply-templates/>
            </xsl:copy>
          </xsl:for-each>
        </xsl:when>
        <!-- für alle rekursiven Aufrufe: -->
        <xsl:otherwise>
          <xsl:copy-of select="$cells"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:variable name="cells" select="$cellsWithRows//html:td"/>
    <xsl:choose>
      <xsl:when test="count($cells)=0">
        <xsl:copy-of select="$preCells"/>
        <xsl:copy-of select="$cells"/>
      </xsl:when>
      <!-- falls nur die erste Zelle der Tabelle übergeben wird: -->
      <xsl:when test="count($cells)=1 and count($preCells)=0">
        <html:td>
          <xsl:attribute name="d2t:colNumber">1</xsl:attribute>
          <xsl:copy-of select="$cells/@*"/>
          <xsl:copy-of select="$cells/node()"/>
        </html:td>
      </xsl:when>
      <xsl:otherwise>
        <!-- rekursive Aufrufe jeweils mit der ersten und zweiten Hälfte der <td>-Elemente -->
        <xsl:variable name="half" select="count($cells) div 2"/>
        <xsl:variable name="firstHalf" select="d2t:getColAndRows($preCells, $cells[position() &lt; $half])"/>
        <xsl:variable name="secondHalf" select="d2t:getColAndRows(($firstHalf), $cells  [position() &gt;= $half][not(position()=last())])"/>
        <xsl:variable name="cell" select="$cells[last()]"/>
        <xsl:variable name="preCells" select="($secondHalf)"/>
        <xsl:variable name="preCell" select="$preCells[last()]"/>
        <!--  vorläufige Spaltennummer: -->
        <xsl:variable name="colOhneRowspan" select="if (($preCell/@d2t:rowNumber) &lt; ($cell/@d2t:rowNumber)) then (1) else (($preCell/@d2t:colNumber) + ($preCell/@colspan))"/>
        <!-- endgültige Spaltennummer: -->
        <xsl:variable name="colMitRowspan" select="d2t:getCol($colOhneRowspan, $cell/@d2t:rowNumber, $preCells[(@rowspan) &gt; 1])"/>
        <!-- Ausgabe der Zellen: -->
        <xsl:copy-of select="$preCells"/>
        <html:td>
          <xsl:attribute name="d2t:colNumber" select="$colMitRowspan"/>
          <xsl:copy-of select="$cell/@*"/>
          <xsl:copy-of select="$cell/node()"/>
        </html:td>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:function>
  <xsl:function name="d2t:getColAndRows">
    <xsl:param name="cells"/>
    <xsl:copy-of select="d2t:getColAndRows((),$cells)"/>
  </xsl:function>
  <xsl:function name="d2t:getCol">
    <xsl:param name="curCol"/>
    <xsl:param name="curRow" as="xs:integer"/>
    <xsl:param name="preCells"/>
    <xsl:choose>
      <!-- rekursiver Aufruf, falls eine zeilenüberspannende Zelle die Zeile $curRow in der Spalte $curCol überspannt -->
      <xsl:when test="$preCells [(@d2t:colNumber) &lt;= $curCol and $curCol &lt; @d2t:colNumber + @colspan][@d2t:rowNumber + @rowspan &gt; $curRow]">
        <xsl:value-of select="d2t:getCol($curCol + 1, $curRow, $preCells)"/>
      </xsl:when>
      <!-- Ausgabe der $curRow als endgültige Spaltennummer -->
      <xsl:otherwise>
        <xsl:value-of select="$curCol"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:function>
  <let name="cells" value="for $table in //html:table return d2t:getColAndRows($table//html:td)"/>
  <pattern>
    <rule context="html:td[(@colspan) &gt; 1]">
      <let name="tableID" value="generate-id(./ancestor::html:table[1])"/>
      <let name="cellID" value="generate-id()"/>
      <!-- alle Zellen dieser Tabelle:-->
      <let name="cells" value="$cells[@d2t:tableID=$tableID]"/>
      <let name="rowNumber" value="count(  ../preceding-sibling::html:tr | ../parent::*[not(self::html:table)]/preceding-sibling::*/html:tr)+1"/>
      <!-- akutelle Zelle mit ermittelter Spaltennummer:-->
      <let name="cell" value="$cells[@d2t:cellID=$cellID]"/>
      <!-- ermittelt alle Zellen, die in dieser Zeile stehen oder diese überspannen: -->
      <let name="row" value="$cells  [(@d2t:rowNumber) + (@rowspan) &gt; ($rowNumber)][(@d2t:rowNumber) &lt;= ($rowNumber)]"/>
      <!-- Spaltennummern, in denen sich Zellen überschneiden: -->
      <let name="cross" value="$row[(@d2t:colNumber) &lt; ($cell/@d2t:colNumber) + ($cell/@colspan)][(@d2t:colNumber) + (@colspan) &gt;= $cell/@d2t:colNumber + $cell/@colspan][not(@d2t:cellID=$cellID)]"/>
      <assert test="count($cross) = 0">Diese Zelle überschneidet sich mit der/n zeilenüberspannenden Zelle/n aus den Spalten <value-of select="for $x in $cross return if ($x = $cross[last()]) then ($x/@d2t:colNumber) else (concat($x/@d2t:colNumber, ','))"/>.</assert>
    </rule>
    <rule context="html:tr">
      <let name="tableID" value="generate-id(./ancestor::html:table[1])"/>
      <!-- alle Zellen dieser Tabelle:-->
      <let name="cells" value="$cells[@d2t:tableID=$tableID]"/>
      <!-- äußerste gefüllte Spalte: -->
      <let name="maxCol" value="xs:integer(max(for $cell in $cells return ($cell/@d2t:colNumber + $cell/@colspan - 1)))"/>
      <let name="rowNumber" value="count(preceding-sibling::html:tr | parent::*[not(self::html:table)]/preceding-sibling::*/html:tr)+1"/>
      <!-- gibt für jede mögliche Spalte die Zellen aus, die in dieser Spalte und in der aktuellen Zeile stehen: -->
      <let name="row" value="for $i in (1 to ($maxCol)) return ($cells[  (@d2t:colNumber) &lt;= $i and $i &lt; (@d2t:colNumber) + (@colspan)][(@d2t:rowNumber) + (@rowspan) &gt; ($rowNumber)][(@d2t:rowNumber) &lt;= ($rowNumber)])"/>
      <!-- verlan&gt;e Anzahl an Spalten: -->
      <let name="forcedColCount" value="if (ancestor::html:table[1]//html:col) then (count(ancestor::html:table[1]//html:col)) else ($maxCol)"/>
      <assert test="count($row) &gt;= $forcedColCount">Es fehlen Zellen. (Anzahl fehlender Zellen: <value-of select="$forcedColCount - count($row)"/> von <value-of select="$forcedColCount"/>)</assert>
      <report test="count($row) &gt; $forcedColCount">Es gibt zu viele Zellen. (Anzahl überflüssiger Zellen: <value-of select="count($row) - $forcedColCount"/>)</report>
    </rule>
  </pattern>
</schema>

Das Schematron-Schema lässt sich in drei Teile gliedern:

(1) die Funktion d2t:getColAndRows()

(2) die Funktion d2t:getCols()

(3) Schematron-Pattern

Die Funktion d2t:getColAndRows()

In dieser Funktion werden alle <td>-Elemente einer Tabelle als Parameter übergeben. Die Funktion gibt eine Sammlung von <td>-Elementen zurück, jedoch wird ihnen jeweils ein Attribut d2t:colNumber mit der Spaltennummer und ein Attribut d2t:rowNumber mit der Zeilennummer hinzugefügt. Die Aufgabe der Funktion ist also, die korrekte Zeilen- und Spaltennummer der Zelle zu ermitteln.

Im Schematron-Schema gibt es zwei d2t:getColAndRows()-Funktionen: Beim rekursiven Aufruf sind zwei Parameter notwendig. Wird die Funktion mit nur einem Parameter aufgerufen, ruft diese wiederum die 2-Parameter-Funktion auf und übergibt im ersten Parameter eine leere Sequenz und im zweiten Parameter alle <td>-Elemente.

Die Funktion basiert auf dem Divide-and-Conquer-Prinzip. Vereinfacht betrachtet übergibt sie alle <td>-Elemente, bis auf das letzte, rekursiv der Funktion. Wird die Funktion nur für ein <td>-Element – also das erste der Tabelle – aufgerufen, sind sowohl das Attribut d2t:colNumber als auch d2t:rowNumber gleich 1.

Wird der Funktion mehr als eine Zelle übergeben, wird für die letzte Zelle anhand der Spalten- und Zeilennummern der vorstehenden <td>-Elemente die Spaltennummer berechnet (die Zeilennummer wird für alle <td>-Elemente beim ersten Aufruf ermittelt). Hierzu wird die d2t:getCols()-Funktion eingesetzt.

Für eine bessere Performance, bzw. um bei sehr großen Tabellen die Anzahl der Rekursionen zu reduzieren, werden die <td>-Elemente in zwei Hälften ($firstHalf und $secondHalf) geteilt und für beide die Funktion jeweils rekursiv aufgerufen. Für diesen Zweck wird der zusätzliche bisher nicht erläuterte Parameter $preCells benötigt. Für die zweite Hälfte der <td>-Elemente können die jeweiligen Spaltennummern nur berechnet werden, wenn die Spaltennummern der ersten Hälfte als Information mitgeliefert werden.

Die Funktion d2t:getCols()

Bei der Ermittlung der Spaltennummer wird zunächst die Zeilenüberspannung der Zellen ignoriert und nur eine vorläufige Spaltennummer berechnet ($colOhneRowspan), die die Zelle erhält, wenn sie nicht von einer zeilenüberspannenden Zelle verdrängt wird. Die korrekte Spaltennummer zu ermitteln ist die Aufgabe der d2t:getCols()-Funktion. Hierzu wird im ersten Parameter die vorläufige Spaltennummer übermittelt, im zweiten Parameter die Zeilennummer und im dritten Parameter alle vorstehenden Zellen.

Nun wird anhand der Angaben überprüft, ob eine vorstehende Zelle in der vorläufigen Spalte die aktuelle Zeile überspannt. Ist dies nicht der Fall, wird die vorläufige Spaltennummer als endgültige ausgegeben. Wird die Zelle jedoch verdrängt, wird die Funktion rekursiv aufgerufen und dabei die vorläufige Spaltennummer um eins erhöht. Die Funktion wird somit so lange ausgeführt, bis keine Zeilenüberspannung die Zelle verdrängt. Die entsprechende Spaltennummer wird dann zurückgegeben.

Schematron-Pattern

Zur weiteren Verbesserung der Performance werden in einer Top-Level-Variablen $cells für jedes <table>-Element die Spaltennummern der jeweiligen <td>-Elemente ermittelt. Zur eindeutigen Zuordnung wird beim ersten Aufruf der d2t:getColAndRows()-Funktion mit der generate-id()-Funktion den <td>-Elementen die ID des jeweiligen <table>-Elements über ein d2t:tableID-Attribut zugewiesen.

Die eigentliche Schematron-Implementierung beruht auf zwei Regeln: Die erste überprüft für alle spaltenüberspannenden Zellen, ob sie sich mit einer zeilenüberspannenden Zelle überschneiden. Die zweite Regel zählt für jede Zeile die Anzahl der Zellen und überprüft, ob zu viele oder zu wenig Zellen in der Zeile vorkommen.

   

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Schematron bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © dpunkt.verlag GmbH 2011
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken. Ansonsten unterliegt dieses Kapitel aus dem Buch "Schematron - Effiziente Business Rules für XML-Dokumente" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

dpunkt.verlag GmbH, Ringstraße 19B, 69115 Heidelberg, fon 06221-14830, fax 06221-148399, hallo(at)dpunkt.de