Stichwortverzeichnis
Aufgabe
Der Confixa-Katalog aus dem Rezept Inhaltsverzeichnis bekommt hinten ein Register: Für jeden Untergrund (Holz, Beton, Mauerwerk, …) steht dort, auf welchen Seiten passende Artikel zu finden sind. Die Stichwörter sind alphabetisch sortiert und nach Anfangsbuchstaben gruppiert, mehrfach vorkommende Seitenzahlen werden zusammengefasst.

Entscheidung
Vom Inhaltsverzeichnis unterscheidet sich das Stichwortverzeichnis in zwei Punkten, und beide vereinfachen oder verändern den Weg:
- Es steht hinten. Wenn das Register gesetzt wird, sind alle Katalogseiten bereits fertig; die gesammelten Seitenzahlen sind vollständig. Das Speichern und Wiederladen über mehrere Durchläufe (
<SaveDataset>/<LoadDataset>) entfällt, ein einziger Durchlauf reicht. - Die Einträge kommen nicht in Dokumentreihenfolge. Ein Register muss sortiert, nach Anfangsbuchstaben gruppiert und von Doppelnennungen befreit werden. Dafür gibt es
<Makeindex>, das sortiert, gruppiert und die Seitenzahlen gleicher Stichwörter aneinanderhängt, sowie die Funktionsd:merge-pagenumbers(), die solche Seitenzahllisten aufräumt.
Wer nur sortieren will, ohne Abschnitte und Seitenzahlen, nimmt stattdessen <SortSequence>; beide Befehle stellt das Kapitel Sortieren und Gruppieren gegenüber.
Lösung
Schritt 1: Beim Setzen die Einträge sammeln
Wie beim Inhaltsverzeichnis wird beim Setzen des Katalogs eine Variable mit Einträgen gefüllt, hier ein Eintrag je Artikel mit dem Untergrund und der aktuellen Seite. Das Sammeln übernimmt ein eigener Abschnitt für das Element article, den der Katalogabschnitt vor der Ausgabe der Tabelle aufruft:
<Record element="group">
<ProcessNode select="article"/>
<!-- danach Überschrift und Tabelle wie im Rezept Inhaltsverzeichnis -->
</Record>
<Record element="article">
<SetVariable variable="indexentries">
<Copy-of select="$indexentries/indexentry"/>
<Element name="indexentry">
<Attribute name="name" select="@base"/>
<Attribute name="page" select="sd:current-page()"/>
</Element>
</SetVariable>
</Record>
Zwei Details sind hier wichtig:
- Das Stichwort muss im Attribut
namestehen;<Makeindex>erwartet diesen Namen beim Zusammenfassen gleicher Einträge. <Copy-of>übernimmt die bisherigen Einträge über den Pfad$indexentries/indexentry, nicht über$indexentriesallein. Der Pfad hält die Liste flach; mit$indexentriesentsteht bei jedem Schritt eine tiefer geschachtelte Struktur, aus der<Makeindex>später nur den letzten Eintrag herausliest.
Das Sammeln könnte statt über den eigenen Abschnitt auch direkt in einem <ForAll select="article"> im Katalogabschnitt stehen; der eigene <Record> hält hier lediglich Sammeln und Ausgabe getrennt.
Schritt 2: Sortieren und Gruppieren mit Makeindex
Nach dem Katalog enthält $indexentries einen Eintrag je Artikel, in Dokumentreihenfolge und voller Wiederholungen. <Makeindex> macht daraus die fertige Registerstruktur:
<Record element="data">
<SetVariable variable="indexentries"/>
<ProcessNode select="group"/>
<SetVariable variable="index">
<Element name="baseindex">
<Makeindex select="$indexentries/indexentry" sortkey="name"
section="section" pagenumber="page"/>
</Element>
</SetVariable>
<ProcessNode select="$index/baseindex"/>
</Record>
Sortiert wird nach dem Attribut aus sortkey, für jeden Anfangsbuchstaben entsteht ein Element mit dem Namen aus section, und die Seitenzahlen gleichnamiger Einträge werden im Attribut aus pagenumber aneinandergehängt. Das Ergebnis sieht so aus:
<baseindex>
<section name="C">
<indexentry name="chipboard" page="1, 1, 1, 1, 1, 1, 1"/>
<indexentry name="concrete" page="2, 3, 3, 3, 3, 3, 3, 3, 3"/>
</section>
<section name="D">
<indexentry name="drywall" page="2, 2"/>
</section>
<!-- weitere Abschnitte -->
</baseindex>
Das abschließende <ProcessNode> interpretiert die Variable als Datenstruktur und springt in den Abschnitt für baseindex, der das Register ausgibt.
Schritt 3: Ausgeben und Seitenzahlen zusammenfassen
Die Ausgabe sind zwei verschachtelte <ForAll>: außen die Abschnitte, innen die Einträge. Die aufgesammelten Seitenzahllisten räumt sd:merge-pagenumbers() auf: Die Funktion sortiert, entfernt Doppelnennungen und fasst ab drei aufeinanderfolgenden Seiten zu Bereichen zusammen; aus 2, 3, 3, 3 wird 2, 3, aus 4, 5, 6 wird 4–6.
<Record element="baseindex">
<PlaceObject>
<Textblock>
<Paragraph fontfamily="title"><Value>Register</Value></Paragraph>
</Textblock>
</PlaceObject>
<PlaceObject row="{sd:current-row() + 1}">
<Table padding="3pt">
<Columns>
<Column width="6cm"/>
<Column width="3cm"/>
</Columns>
<ForAll select="section">
<Tr break-below="no" top-distance="8pt">
<Td colspan="2" border-bottom="1pt">
<Paragraph><B><Value select="@name"/></B></Paragraph>
</Td>
</Tr>
<ForAll select="indexentry">
<Tr>
<Td><Paragraph><Value select="@name"/></Paragraph></Td>
<Td align="right">
<Paragraph><Value select="sd:merge-pagenumbers(@page)"/></Paragraph>
</Td>
</Tr>
</ForAll>
</ForAll>
</Table>
</PlaceObject>
</Record>
Da das Register schmal ist, bekommt die Tabelle hier feste Spaltenbreiten statt stretch="max"; die Möglichkeiten zeigt das Rezept Spaltenbreiten steuern.
Vollständiges Beispiel
Das lauffähige Projekt liegt auch im Beispiele-Repository. Die Daten sind die Confixa-Artikelliste aus dem ersten Rezept, ergänzt um das Attribut base mit dem Untergrund; die Katalogtabelle zeigt es in einer eigenen Spalte:
<data company="Confixa">
<group name="Chipboard screws" code="CS" material="steel, zinc plated">
<article number="CS-3012" dim="3.0 × 12" drive="TX10" pu="1000"
price="4.90" base="chipboard"/>
<!-- weitere Artikel: base="chipboard" oder base="wood" -->
</group>
<group name="Wall plugs" code="WP" material="nylon">
<article number="WP-0525" dim="5 × 25" pu="100"
price="2.10" base="drywall"/>
<!-- weitere Artikel: base="drywall", "masonry" oder "concrete" -->
</group>
<group name="Wedge anchors" code="WA" material="steel, zinc plated">
<article number="WA-0875" dim="M8 × 75" pu="50"
price="12.40" base="concrete"/>
<!-- weitere Artikel: base="concrete" -->
</group>
</data>
<Layout xmlns="urn:speedata.de:2009/publisher/en"
xmlns:sd="urn:speedata:2009/publisher/functions/en">
<Pagetype name="catalog" test="true()">
<Margin left="20mm" right="20mm" top="20mm" bottom="20mm"/>
<AtPageShipout>
<PlaceObject column="20mm" row="280mm" allocate="no">
<Textblock width="17" textformat="centered">
<Paragraph><Value select="sd:current-page()"/></Paragraph>
</Textblock>
</PlaceObject>
</AtPageShipout>
</Pagetype>
<DefineFontfamily name="title" fontsize="18" leading="22">
<Regular fontface="sans-bold"/>
</DefineFontfamily>
<Record element="data">
<SetVariable variable="indexentries"/>
<ProcessNode select="group"/>
<SetVariable variable="index">
<Element name="baseindex">
<Makeindex select="$indexentries/indexentry" sortkey="name"
section="section" pagenumber="page"/>
</Element>
</SetVariable>
<ProcessNode select="$index/baseindex"/>
</Record>
<Record element="group">
<ProcessNode select="article"/>
<PlaceObject>
<Textblock>
<Paragraph fontfamily="title"><Value select="@name"/></Paragraph>
</Textblock>
</PlaceObject>
<PlaceObject row="{sd:current-row() + 1}">
<Table stretch="max" padding="3pt">
<Tablehead>
<Tr background-color="lightgray">
<Td><Paragraph><B><Value>Artikelnummer</Value></B></Paragraph></Td>
<Td><Paragraph><B><Value>Abmessung</Value></B></Paragraph></Td>
<Td><Paragraph><B><Value>Untergrund</Value></B></Paragraph></Td>
<Td align="right"><Paragraph><B><Value>VE</Value></B></Paragraph></Td>
<Td align="right"><Paragraph><B><Value>Preis in €</Value></B></Paragraph></Td>
</Tr>
</Tablehead>
<ForAll select="article">
<Tr>
<Td><Paragraph><Value select="@number"/></Paragraph></Td>
<Td><Paragraph><Value select="@dim"/></Paragraph></Td>
<Td><Paragraph><Value select="@base"/></Paragraph></Td>
<Td align="right"><Paragraph><Value select="@pu"/></Paragraph></Td>
<Td align="right"><Paragraph><Value select="@price"/></Paragraph></Td>
</Tr>
</ForAll>
</Table>
</PlaceObject>
<ClearPage/>
</Record>
<Record element="article">
<SetVariable variable="indexentries">
<Copy-of select="$indexentries/indexentry"/>
<Element name="indexentry">
<Attribute name="name" select="@base"/>
<Attribute name="page" select="sd:current-page()"/>
</Element>
</SetVariable>
</Record>
<Record element="baseindex">
<PlaceObject>
<Textblock>
<Paragraph fontfamily="title"><Value>Register</Value></Paragraph>
</Textblock>
</PlaceObject>
<PlaceObject row="{sd:current-row() + 1}">
<Table padding="3pt">
<Columns>
<Column width="6cm"/>
<Column width="3cm"/>
</Columns>
<ForAll select="section">
<Tr break-below="no" top-distance="8pt">
<Td colspan="2" border-bottom="1pt">
<Paragraph><B><Value select="@name"/></B></Paragraph>
</Td>
</Tr>
<ForAll select="indexentry">
<Tr>
<Td><Paragraph><Value select="@name"/></Paragraph></Td>
<Td align="right">
<Paragraph><Value select="sd:merge-pagenumbers(@page)"/></Paragraph>
</Td>
</Tr>
</ForAll>
</ForAll>
</Table>
</PlaceObject>
</Record>
</Layout>
Grenzen
- Umbrechende Inhalte.
sd:current-page()wird beim Aufbau der Tabelle ausgewertet, nicht beim Seitenumbruch. Hier stimmen die Seitenzahlen, weil jede Gruppe mit<ClearPage>auf ihrer eigenen Seite steht. Umbricht die Tabelle dagegen automatisch über Seiten, bekommen die Artikel der Folgeseiten die falsche Zahl. Dann sammelt man mit Marken statt mit einer Variablen:<Mark append="yes">wird beim Ausliefern der Seite ausgewertet,sd:pagenumber()liefert die Seitenliste undsd:merge-pagenumbers()fasst sie genauso zusammen; siehe Marken. - Einfache Sortierung.
<Makeindex>sortiert und gruppiert nach dem Anfangszeichen ohne Sprachregeln: Stichwörter mit Umlauten landen hinter »Z« und bilden keinen brauchbaren Abschnittsbuchstaben. Für deutschsprachige Register die Stichwörter in den Daten normalisieren oder die Daten vorab sortieren; die Wege dahin zeigt Datenaufbereitung. - Nur eine Ebene. Zweistufige Register (Haupt- und Untereinträge) kann
<Makeindex>nicht; dafür baut man die Struktur selbst auf oder bereitet sie vorab mit XSLT auf. - Referenz:
<Makeindex>,<SortSequence>,sd:merge-pagenumbers()undsd:pagenumber()in den Layoutfunktionen.
