Making of …

Über ein Jahr lang hat das Kochbuch treue Dienste geleistet – aber (schnief!) es hat sich nicht weiterentwickelt. Der Bau des Hauses hat alle Zeit aufgefressen. Nun endlich sieht es so aus, als ob es ab und an wieder einige zusammenhängende Stunden gibt, die für das Kochbuch genutzt werden können.

Eigentlich – es gibt kein geheimnisvolleres Wort als "eigentlich" – wollte ich mich in diesen zusammenhängenden Stunden bevorzugt meinem neuen Projekt "fruschtique graphLab" widmen, das ich in den beiden vorangehenden Posts ansatzweise beschrieben habe. In der Tat hat es da auch einigen Fortschritt gegeben, der sogar von Maria mit einiger Bewunderung zur Kenntnis genommen wurde. Was ich "graphLab" nenne, nennt sie "Böppelbilder", und Du, liebe Leserin, wirst leider noch ein bisschen darauf warten müssen.

In der Zwischenzeit quäle ich Dich mit einer anderen meiner Marotten: der semantischen Annotation von Webseiten.

schema.org Logo

Für die semantische Annotation von Webseiten kommt man meiner Meinung nach nicht an schema.org nicht vorbei. schema.org ist keine formale Ontologie im Sinne einer Beschreibungslogik bzw. im Sinne einer darauf aufbauenden Ontologiesprache wie z.B. OWL. Am besten könnte man schema.org meiner Meinung nach als kontrolliertes Vokabular zur Beschreibung des Inhalts von Webseiten charakterisieren, kontrolliert von Google und einigen anderen Größen des World Wide Web. Da Rezepte im WWW keine zu vernachlässigende Rolle spielen, enthält schema.org eine ausführliche Sammlung von Begriffen, mit denen sich Rezepte beschreiben lassen; man findet diese Begriffssammlung hier. (Selbstkritische Randbemerkung: Vielleicht wäre es besser gewesen, wenn auch ich die schema.org-Begriffe für die Entwicklung meines XML-Schemas für Rezepte verwendet hätte, möglicherweise unter Einbringung einiger Erweiterungen. Leider habe ich das nicht getan, und vor einer nachträglichen Umstellung fürchte ich mich erbärmlich.)

Was also ist zu tun? Zwei Aufgaben sind zu erledigen:

  • Zum einen muss ich eine Abbildung zwischen meinen Begriffen und den schema.org-Begriffen herstellen. Vorweg: Bei vielen Begriffen gelingt das leicht, aber es gibt diverse Begriffe, die es jeweils nur in der einen "Welt" gibt, in der anderen aber nicht. Ich habe mich dazu entschieden, nur die offensichtlichen Begriffsabbildungen zu implementieren, z.B. von recipeIntro (fruschtique) nach description (schema.org). Ich werde dieses Thema im folgenden nicht weiter verfolgen.
  • Ich muss die gewünschten semantische Annotation in einem geeigneten Format bereitstellen. Das Quellformat für meine Rezepte ist XML. XML tags (präziser: die tag names) kann man ohne Mühe als semantische Beschreibung der text values derzugehörigen XML-Elemente auffassen. Schema.org sieht für die semantische Annotation von HTML-codierten Rezepten eines dieser drei Non-XML-Formate vor: microdata, RDFa oder JSON-LD. Es wird also eine Umcodierung der in den Rezeptdaten enthaltenen semantischen Informationen vom XML-Format in eines der drei genannten Formate benötigt. Ich habe mich für JSON-LD entschieden (die JavaScript Object Notation for Linking Data), da hier HTML-codierter Rezepttext und semantische Annotation klar voneinander getrennt werden.

Nun können wir die Aufgabe präzise beschreiben: Es besteht die Aufgabe, den text value derjenigen XML-Elemente, für deren tags es eine Entsprechung in der Begriffswelt von schema.org/recipe gibt, unter Verwendung der schema.org-Begriffe in das JSON-LD-Format einzupacken, z.B. in die für JSON typischen key-value pairs (die im Jargon als map bezeichnet werden). Stark vereinfacht: Die Entsprechung des tag name eines XML-Elements wird zum JSON key, der text value des XML-Elements wird zum JSON value. Der erfahrene XSLT-Programmierer sagt sich: Kein Problem – und krempelt die Ärmel hoch. Bald darauf stellt er fest: Mit Hilfe eines XSLT-Stylesheet einen Text zu erzeugen, der später als JSON-Dokument mit all den erforderlichen Anführungszeichen, Klammern und Kommas durchgehen kann, ist nicht lustig – und weder das Stylesheet, noch das resultierende JSON-Dokument sind besonders übersichtlich; von "hübsch" mag man gar nicht reden.

Also: neue Recherche.

Und: Oh Wunder! XSLT 3.0 schenkt uns eine Funktion xml-to-json() – und damit sollte es doch ganz leicht gehen!

Doch nichts ist ohne Preis: Die xml-to-json()-Funktion erwartet als XML-Input eine Instanz dieses XML-Schemas. Es hat den target namespacehttp://www.w3.org/2005/xpath-functions, und ist ebenso wie die Spezifikation der xml-to-json()-Funktion in der W3C Recommendation: XSL Transformations (XSLT) Version 3.0 vom 8.6.2017 enthalten. (Naja, dieses Datum sagt mir, dass ich, wenn ich mich bereits zu der Zeit, als ich das Kochbuch zum ersten Mal veröffentlicht habe, mit der semantischen Annotation meiner Rezepte beschäftigt hätte, möglicherweise einen Frühschuss getan hätte.) Das xml-to-json-Input-Format hat leider gar nichts mit meinem Rezept-Schema zu tun. Es ähnelt eher einer XML-basierten Beschreibung des oben andiskutierten JSON-Formats mit maps, arrays usw.

Der erfahrene XSLT-Programmierer krempelt also zum zweiten Male die Ärmel hoch.

Dieses Mal klappt es. In meinem Stylesheetgen_json-ld.xsl befülle ich zunächst eine XSLT-Variable mit einer XML-Baumstruktur, die dem xml-to-json-Schema genügt und die text values der entsprechenden XML-Elemente aufnimmt, und transformiere dann diese Baumstruktur mit der xml-to-json()-Funktion in das JSON-Format. Das Ergebnis der Transformation ist das gewünschte Output-Dokument. Das Stylesheetgen_json-ld.xsl inkludiere ich in das übergeordnete Stylesheetgen_rcp.xsl. Dieses sorgt dafür, dass in den Kopf eines jedes HTML-codierten Rezepts die passende JSON-LD-codierte semantische Annotation gelangt und in script tags eingeschlossen wird. Das Stylesheet gen_rcp.xsl wird selber wiederum in das Stylesheet book-builder.xsl inkludiert.

War doch gar nicht schwer. Oder?

Selbstverständlich habe ich die von mir erzeugten semantischen Annotationen mit Hilfe des Google-Testtool für strukturierte Daten syntaktisch überprüft.

Aber nun ist doch wirklich alles dafür getan (heul!), dass Google das fruschtique-Kochbuch mit lauter ersten Plätzen in den Trefferlisten der Rezeptsuche bedenkt! Damit Du, liebe Leserin, dazu verführt wirst, niemals eine andere Sultanrolle zu kochen als meine!

In meinem letzten Post habe ich mir überlegt, wie denn der "ideale" Zutatengraph aussehen könnte. Das war vielleicht nicht die richtige Frage. Heute würde ich die Frage anders stellen:

Gibt es eine Kollektion von Zutatengraphen, die geeignet ist, mein Kochbuch zu charakterisieren?

Nach wie vor kann ich nur eine vorläufige Antwort geben, denn zur Erzeugung der Zutatengraphen verfüge ich immer noch nicht über eine Sammlung aller Zutaten aus allen Rezepten meines Kochbuchs. Es geht immer noch um die Zutaten der vier Rezepte, die ich im Post vom 14.02.2018 genannt habe. Der Charme dieses Mankos liegt darin, dass man dank der Datensparsamkeit einige Dinge klarer sehen kann, als wenn man alle Daten hätte: Erkenntnischance vor Datensättigung.

Die Frage also ist: Wie kann man einen "großen" Zutatengraphen so in eine Menge von "kleinen" Zutatengraphen aufteilen, dass einerseits keine Information verlorgen geht, dass aber andererseits neue Erkenntnisse entstehen? Anders formuliert: Kann ich den Zutatengraphen geeignet partitionieren? Scheint wie: eine Variante des klassischen divide and conquer-Problems.

(Für den Ingenieur ist das klassische divide and conquer-Problem ein konstruktives Problem: Wie zerlege ich eine große konstruktive Aufgabe in eine Vielzahl von Detailkonstruktionen? Im Kontext meines Kochbuchs geht es aber nicht um ein konstruktives, sondern um ein analytisches Problem: Wie kann ich aus einer Vielzahl von Einzelbeobachtungen (= Teilgraphen) einen Blick auf das große Ganze gewinnen? Aber solche methodischen Abschweifungen tun nicht wirklich etwas zur Sache. Oder doch?)

Eine kurze Auseinandersetzung mit der Graphentheorie hilft: Es gibt zwei graphentheoretische Maße, mit denen man einen Graphen partitionieren kann: die betweenness centrality und die eccentricity. Versuchen wir, das zu klären, und zwar sowohl graphentheoretisch als auch kulinarisch.

Betweenness centrality: Stellen wir uns den Graphen als das Streckennetz der Bundesbahn vor; die Knoten des Graphen repräsentieren die Bahnhöfe. Dann kann man sicherlich die kürzesten Wege von jedem Bahnhof A zu jedem Bahnhof B ausrechnen. Jeder Weg wird beschrieben durch eine Folge von Bahnhöfen. Diejenigen Bahnhöfe, durch die die meisten dieser kürzesten Wege gehen, haben die höchste betweenness centrality. Anders ausgedrückt: Wenn Du von A nach B willst, dann kommst Du mit Sicherheit an einem dieser Bahnhöfe vorbei. Kulinarisch formuliert: Es gibt einige Zutaten, an denen man beim Kochen nicht vorbeikommt. Pfeffer und Salz gehören dazu; sie können ruhig auf der Einkaufsliste fehlen, da man sie immer vorrätig haben muss. (Wehe, wenn nicht!) Es gibt andere Zutaten, die braucht man auf jeden Fall für eine bestimmte Art von Gerichten: Für eine gute Brühe braucht man Sellerie, egal ob es sich um eine Gemüsebrühe oder eine Fleischbrühe handelt. Wenn ich also vom Fleisch zum Fenchel (der in jede gute Gemüsebrühe hineingehört) reisen möchte, dann komme ich beim Sellerie vorbei.

graph with high betweenness centrality nodes

Ich habe also den oben gezeigten Graphen erzeugt, der (bezogen auf die genannten Rezepte) nur aus solchen Zutatenknoten erzeugt wird, die eine höhere betweenness centrality haben. Wie erwartet ergibt sich - wie schon im vorigen Post - ein partitionierter Graph: Ein Teilgraph zeigt die Zutaten, die sowohl in der Tomatensuppe als auch in der Tomatensauce enthalten sind; der andere Teilgraph zeigt die Zutaten, die sowohl in den Gnocchi als auch in den Blini enthalten sind. Man könnte sagen: Bei diesen Zutaten hat der Koch keine Wahl: Diese Zutaten gehören unbedingt in ein Tomaten-Rezept hinein bzw. in ein Rezept für Beilagen auf Weizenmehlbasis.

Weiter im Text.

Eccentricity:Wieder stellen wir uns den Graphen als das Streckennetz der Bundesbahn vor. Dieses Mal wollen wir zu den entlegenen Destinationen dieses Netzes fahren. "Entlegen" heißt hier: Wir suchen die längsten Wege im Netz, d.h. diejenigen Wege, auf denen man die größte Anzahl an Knoten passieren muss, wenn man von einem Knoten A zu einem Knoten B reisen will. Wir schließen dabei selbstverständlich aus, dass man im Kreise fährt, oder dass wir irgendeinen kürzeren Weg von A nach B übersehen haben. Die Endpunkte eines solchen Weges haben eine hohe eccentricity. Kulinarisch formuliert: Exzentrische Zutaten kommen in nur einem einzigen oder in nur sehr wenigen Rezepten vor; sie veredeln den "Stampf", den sie mit anderen Rezepten teilen, zu einer eigenständigen, genusswürdigen Speise.

graph with high eccentricity nodes

Nach der Wahl geeigneter Werte für die eccentricity zeigt der resultierende Graph ein Netz mit vier Partitionen. Jede Partition steht für eins der vier Rezepte, die ich in die Analyse einbezogen habe. Eine solche Partition zeigt diejenigen Zutaten, die nur im jeweiligen Rezept vorkommen. Von links oben nach rechts unten: Tomatensuppe, Gnocchi, Blini, Tomatensauce.

Würde man alle in den beiden Bildern dieses Posts gezeigten Partitionen geschickt "zusammenbasteln", könnte man den Graphen rekonstruieren, den ich im vorigen Post gezeigt habe. Und das finde ich sehr spannend.

Ich zitiere jetzt mal für alle, die's ganz genau wissen wollen, Wolfram: "BetweennessCentrality will give high centralities to vertices that are on many shortest paths of other vertex pairs. BetweennessCentrality for a vertex \(i\) in a connected graph is given by \(\sum_{s,t \in V \land s\neq i \land t\neq i} \big({n_{s,t}^i} \big/ {n_{s,t}}\big),\) where \(n_{s,t}\) is the number of shortest paths from \(s\) to \(t\) and \(n_{s,t}^i\) is the number of shortest paths from \(s\) to \(t\) passing through \(i\). The ratio \({n_{s,t}^i} \big/ {n_{s,t}}\) is taken to be zero when there is no path from \(s\) to \(t\)."

Und weiter: "The eccentricity \(\epsilon(\nu)\) of a graph vertex \(\nu\) in a connected graph \(G\) is the maximum graph distance between \(\nu\) and any other vertex \(\upsilon\) of \(G\). For a disconnected graph, all vertices are defined to have infinite eccentricity (West 2000, p. 71)." (Weisstein, Eric W. "Graph Eccentricity." From MathWorld - A Wolfram Web Resource.)

Um einen Zutatengraphen wie den zuvor gezeigten zu erzeugen, ist leider eine Menge "Handarbeit" nötig. Dabei geht es vor allem um die normierte Bezeichnung der Zutaten. Es soll ja nicht sein, dass "Apfel" und "Äpfel" als zwei unterschiedliche Zutaten angesehen werden. Kulinarisch haben sie ja schließlich die gleiche Bedeutung.

Was habe ich getan?

Ich habe einen Zutatenkatalog entwickelt, der für je Zutat die folgenden Informationen enthält: ein label (eine normierte Bezeichnung, wie sie im Graphen auftauchen soll) und die Zuordnung der jeweiligen Zutat zu einer class, d.h. zu einer Zutatenklasse, wie sie auch in dem Zutatengraphen erkennbar ist, den ich im vorigen Post gezeigt habe. Referenziert wird jeder Katalogeintrag durch einen eindeutigen identifier. Hier ist ein Beispiel:

<fc:ingredient id="zitrone">
<fc:igtLabel>Zitrone</fc:igtLabel>
<fc:igtClass>fruit</fc:igtClass>
</fc:ingredient>

Um diesen Zutatenkatalog zu erstellen, habe ich mir alle Zutaten aus allen Rezepten des fruschtique-Kochbuchs in eine lange Liste schreiben lassen (Danke, Saxon, danke XSLT!) und diese Liste dann "von Hand" so eingedampft, dass für jede kulinarisch interessante Zutat genau nur ein Eintrag so wie der gerade gezeigte für den Katalog übrig bleibt.

Wieviele Zutaten der Katalog enthält, möchtest Du nun bestimmt gerne wissen, liebe Leserin, oder nicht?

Es sind: 198 Zutaten! Gepriesen sei die Speisekammer!

Aber leider war die Arbeit damit noch nicht fertig!

Nun müssen ja noch die Zutatenlisten der einzelnen Rezepte des fruschtique-Kochbuchs bearbeitet werden. Wenn in einer Zutatenliste "Zitronen" auftaucht, dann muss irgendwo stehen, dass es sich hier um eine Zutat handelt, die in normierter Darstellung "Zitrone" heißt.

Wieder ist computerunterstützte Handarbeit erforderlich: Ich habe das Formular zur Eingabe von Rezepte um ein Feld je Zutat erweitert, in den ich den o.a. identifier eintragen muss, wenn ich will, dass diese Zutat in einem Zutatengraphen korrekt berücksichtigt wird.

Damit ich mich bei der Eingabe eines solchen identifier nicht vertippen kann, werden mir beim Tippen Vorschläge gemacht, aus denen ich den richtigen identifier auswählen kann. Diese Vorschlagsliste wird aus dem Zutatenkatalog automatisch erzeugt.

So, das war jetzt wieder mal genug Nerd-Kram!

Liebe Leserin, Du wirst bemerkt haben, dass ich mich mehrfach zum Thema food pairing geäußert habe: Es geht um die Frage, welche Zutaten zueinander passen und welche nicht.

Man kann diese Frage auf viele unterschiedliche Arten und Weisen anpacken. Ich habe mir die folgende herausgesucht: Ich erstelle für alle Zutaten, die ich in all meinen Rezepten verwende, einen network graph. Der wird wie folgt gebaut: Die Zutaten werden durch Kreise repräsentiert (in der Graphentheorie: die Knoten des Graphen), und die Beziehung zwischen zwei Zutaten wird durch eine Linie repräsentiert (in der Graphentheorie: die Kanten des ungerichteten Graphen). Eine Beziehung zwischen zwei Zutaten existiert immer dann, wenn es mindestens ein Rezept in meinem Kochbuch gibt, in dem die beiden Zutaten auftauchen. Aus dem unten gezeigten Bild kann ich also ableiten, dass es mindestens ein Rezept gibt, in dem sowohl Estragon als auch Knoblauch verwendet werden, dass es aber kein Rezept gibt, in dem Estragon und Tonkabohne verwendet werden.

(Im folgenden werde ich die Terminologie Zutatengraph, Zutatenknoten und Kante verwenden.)

Einige weitere Eigenschaften meines Zutatengraphen sind besonders zu erwähnen: Jede Zutat habe ich einer "Zutatenklasse" zugeordnet: Fleisch, Fisch, Gemüse, Milchprodukte, Kräuter, Gewürze, sauer, süß usw. Jeder Zutatenklasse habe ich eine Farbe zugeordnet, und die Farbe eines Zutatenknotens entspricht der Farbe der zugeordneten Klasse (s. Legende). Die Größe eines Zutatenknotens ist proportional zur Häufigkeit, mit der die entsprechende Zutat in meinem Rezeptbuch auftaucht ("Prävalenz"). Die Dicke einer Kante zwischen zwei Zutatenknoten ist proportional zur Anzahl der Rezepte, in denen diese Zutatenkombination verwendet wird (in der Graphentheorie: das Kantengewicht). Zur besseren Sichtbarkeit des Kantengewichts färbe ich außerdem die Kante zwischen zwei Zutatenknoten rot ein, wenn diese Kombination mehr als einmal auftaucht.

Nun habe ich es aus Gründen, die später dargestellt werden, noch nicht geschafft, alle Zutaten bzw. Zutatenpaare zu ermitteln. Das Bild zeigt nur ein erstes Ergebnis; dafür habe ich nur vier Rezepte ausgewertet.

ingredient graph for 4 recipes
Frucht
Fleisch
Brühe
Zubereitung
Gewürz
Nüsse
süß
Kräuter
Gemüse
Ei
Kohlenhydrate
Zwiebel
Fisch
sauer
Milch
Fett
etc
Alkohol

Das Ergebnis dieser ersten Auswertung war für mich auf den ersten Blick überraschend (auf den zweiten Blick nicht, denn ich kenne ja meine Rezepte): Der Zutatengraph für diese vier Rezepte ist partitioniert, d.h. er zerfällt in zwei Teilgraphen, die keinen einzigen gemeinsamen Knoten (keine einzige gemeinsame Zutat) haben. Oder anders formuliert: Es gibt keine Zutat, die sowohl in den Rezepten des oberen Teilgraphen als auch in den Rezepten des unteren Teilgraphen verwendet wird. Naja, das ist ja auch kein Wunder, wenn man sich die Rezepte anschaut, die ich ausgewertet habe: Tomatensuppe, Tomatensauce, Blini und Gnocchi fruchtig. Ganz offensichtlich repräsentiert der obere Teilgraph die Blini und die Gnocchi, während der untere Teilgraph die Tomatensuppe und die Tomatensauce repräsentiert.

Maria würde allerdings sofort erkennen, dass das so nicht genau stimmt. Denn: Liest man sich die Zutatenlisten durch, so sieht man, dass in allen vier Rezepten "Öl" (Olivenöl) verwendet wird. Also hätte doch eine Beziehung zwischen dem oberen und dem unteren Teilgraphen über einen Zutatenknoten für "Öl" bestehen müssen! Und warum ist diese Beziehung nicht da?

Ich habe mich entschieden, für die Erstellung des Zutatengraphen die Zutat "Öl" nicht zu berücksichtigen. Öl ist (wenn man von Süßspeisen bzw. speziell parfümierten Ölen absieht) eine Allerweltszutat, deren Auftauchen keine neuen Erkenntnisse zum Thema food pairing bringt. Ebenso habe ich Salz und Pfeffer weggelassen, weil diese in so gut wie allen Rezepten auftauchen. Außerdem habe ich alle Trieb- und Verdickungshilfmittel wie z.B. Hefe, Backpulver, Johannisbrotkernmehl weggelassen: Diese tragen bei richtiger Anwendung nichts zum Geschmack bei. Wenn ich alle Rezepte auswerte, werde ich mich wahrscheinlich entscheiden müssen, noch weitere Zutaten von der Auswertung auszuschließen, da der Zutatengraph wahrscheinlich sonst zu unübersichtlich wird. Wir werden sehen.

Bleibt nun noch die entscheidende Frage: Wie sieht der "ideale" Zutatengraph aus? Vielleicht kommt der Antwort näher, wenn man zwei Extreme betrachtet: Alle Zutaten sind mit allen anderen Zutaten verbunden. Oder: Keine Zutat ist mit keiner andern verbunden. Im ersten Fall würde alle Zubereitungen (fast) gleich schmecken, da nur Mengenvariationen einen Geschmacksunterschied bewirken können. Der zweite Fall ist nicht wirklich diskutierbar: Kann man sich ein Rezept vorstellen, in dem "Mehl" die einzige Zutat ist?

Das hat uns eine Antwort auf die gestellte Frage noch nicht wirklich nähergebracht. Ein neuer Versuch ist nötig.

Wir starten wieder mit einer Frage: Gibt es Zutaten, die nur in einem einzigen Rezept auftauchen? Ich denke hier an Nelken: Ich kenne Nelken vor allem in Verbindung mit Rotkohl. Im Zutatengraphen würde man (wenn es kein anderes Rezept mit Nelken gibt) sehen, dass alle Kantengewichte der von "Nelke" ausgehenden Linien = 1 sind.

Aus der Beobachtung solcher Fälle könnte man ableiten, dass hier vielleicht eine Chance für ein neues ungewöhnliches food pairing besteht. Könnte man sich z.B. ein Rezept vorstellen, in dem Nelken mit Honig kombiniert werden? Bestimmt. Aber auch eine andere Schlussfolgerung kann man treffen: Wenn es viele solche Zutaten gibt, die nur in einem Rezept auftauchen, dann verspricht das Kochbuch eine abwechslungsreiche Küche, da jedes Rezept seine eigene Note hat. (Allerdings wird die Vorratswirtschaft dadurch nicht einfacher.)

Aus dem Gesagten folgt, dass der Zutatengraph noch viel Spielraum für weitergehende Analysen bietet. Ich werde mich also mit der Analyse "sozialer Beziehungen" zwischen Kochzutaten beschäftigen müssen. Um meine Rezepte besser zu verstehen. Na, wer hätte das gedacht!

Das Layout des Graphen folgt dem Paradigma des force-directed graph und wurde mit Hilfe des Pakets GraphViz erstellt.

In einem nächsten Post werde ich mich mit der Aufbereitung meiner Rezeptdaten beschäftigen. Und bald schon werde ich eine neue Seite in mein Kochbuch aufnehmen, auf der einige Zutatengraphen gezeigt werden.

Wie ausführlich hier begründet, habe ich mich ja entschieden, mein Kochbuch als Sammlung statischer HTML-Seiten zu veröffentlichen. Statische HTML-Seiten? Das klingt, als ob auf den Seiten nix los wäre. Schrecklich. Naja, immerhin habe ich ja etwas Dynamik in die Rezeptseiten hineingesteckt: Der Rezepttext entfaltet sich per Click in einer sehr netten Animation. Aber: Reicht das an Dynamik?

Ich habe mir gedacht, dass es im Kochbuch eine Stelle gibt, an der ich "Dynamik" und "Vereinfachung des Workflow" geschickt miteinander verbinden kann. Es geht um die Deckblätter für die elf Kapitel des Kochbuchs.

Der zunächst vorgesehene Workflow sah vor, dass die Schlüsselworte aus allen Rezepten eines Kapitels in einer kapitelbezogenen Datei zusammengetragen wurden, dass ich mit diesen Schlüsselwort-Dateien im Gepäck auf diese Website gehe, mir dort die word clouds erzeugen lasse, die ich für die Kapiteldeckblätter benötige, und diese word clouds dann als png-Dateien herunterlade. Oh Mann! Was für ein Aufwand für ein Schmuckelement! Ein Aufwand, den ich bei jeder Änderung, jedem Löschen und jedem Hinzufügen eines Schlüsselworts hätte treiben müssen. Keine gute Idee.

Die Idee: Ich packe die Schlüsselwortlisten in die Deckblattseiten hinein und lasse die word clouds dynamisch durch ein js-Script erzeugen, das ebenfalls auf der Deckblattseite zur Verfügung gestellt wird. Keine lange Suche und ein solches js-Script war hier gefunden. Dieses Script erzeugt eine word cloud auf einem HTML canvas.

Naja, bei der Erstellung der Schlüsselwortlisten war noch ein bisschen Trickserei erforderlich: Es mussten ja nicht nur die Schlüsselworte aus den Rezepten extrahiert werden, sondern es musste auch noch eine Häufigkeitsinformation zu jedem Schlüsselwort hinzugefügt werden. Dank des xslt-Konstrukts for-each-group war der Aufwand dafür allerdings bescheiden. Das Result: Dynamik! Jedes Mal, wenn Du, liebe Leserin, den refresh-Knopf des Browsers (↻) drückst, erscheint eine neue word cloud. Das ist doch sehr hübsch.

Aber dann!

Aber dann hat mich ein kleiner Teufel geritten. Was musst Du tun, hat mich der kleine Flausen-Teufel gefragt, damit sich die word cloud dynamisch an die Größe des zur Verfügung gestellten canvas anpasst? Will sagen: Die Größe der word cloud wird nicht nur beim Laden der Seite festgelegt, sondern auch, wenn die Größe einer bereits geladenen Seite durch Benutzereingriff geändert wird. So wie hier:

resizing

Das Prinzip ist klar: Das word cloud-Script muss bei jeder Größenänderung neu aktiviert werden. Das kriegt man hin, wenn man das Script auf die entsprechenden js events reagieren lässt. Aber kann man das so 1:1 umsetzen? Bei jeder Mausbewegung werden schließlich viele, viele, viele Größenänderungen bewirkt.

Nach einigem Suchen habe ich auch für dieses Problem eine Lösung gefunden: das js-Script debounce aus der js-Bibliothek lodash.

Nach dem Zusammenbasteln der beiden js-Scripte hat sich das dynamische Verhalten eingestellt, das ich mir für meine Seiten gewünscht habe. Happy viewing!

Die fruschtique-Rezeptseiten gibt es in zwei unterschiedlichen Ansichten: view-only und form. Beide Ansichten werden aus dem XML-Code für das jeweilige Rezept, der im Verzeichnis recipes_xml abgespeichert ist, durch das Transformationsscript gen_pages-rcp.xsl erzeugt. Für die Validierung des XML-Codes gibt es ein XML Schema mit dem Namen recipe.xsd; der namespace ist http://fruschtique.de/ns/recipe, als Präfix verwende ich fr.

Die form-Ansicht erlaubt es der Benutzerin, das dargestellte Rezept zu verändern oder ein neues Rezept einzugeben. Die Formulareingabe wird durch diverse js-Funktionen verarbeitet, die in Formularseiten eingebettet sind. Insbesondere wird der Formular-Input durch die js-Funktion save in einen XML-Baum konvertiert, serialisiert und im Download-Ordner als XML-Datei abgespeichert.

workflow

Das Bild zeigt den Knackepunkt: Auch nach einer Bearbeitung durch die Benutzerin bzw. durch die angesprochene js-Funktion save soll die Rezeptdatei immer noch Schema-valide sein. Mit anderen Worten: Die Schema-Validität der Rezeptdateien soll auch nach einer Rezeptbearbeitung gegeben sein.

Man könnte meinen, sich die genannte js-Funktion save leicht so schreiben lässt, dass ein Schema-valider Output erzeugt wird. Wo also liegt das Problem?

In den XML-codierten Rezeptdateien werden nicht nur tags aus dem fr-namespace verwendet, sondern auch tags aus dem namespacehttp://www.w3.org/1999/xhtml, hier mit dem Präfix ht bezeichnet. Zum Glück gibt es diese offizielle Abbildung der XHTML-DTD auf ein XML-Schema, sonst könnte ich die von mir genutzten HTML-tags wie z.B. <i> und <a> nicht in den XML-codierten Rezepten verwenden.

Um die Validität eines XML-codierten Rezepts zu etablieren, wird es sowohl gegen mein Rezept-Schema als auch gegen das XHTML-Schema validiert. Um diese Validierung zu ermöglichen, muss ich die beiden namespaces im Rezepttext erkennbar machen. Das sieht dann z.B. so aus:

<fr:introText>Mirepoix verwende ich z.B. beim <ht:a href="Chili%20con%20carne.html">Chili con carne</ht:a>. </fr:introText>

Wenn ich den Text, der in <fr:introText></fr:introText> eingeschlossen ist, unverändert in ein Formularfeld übernehmen würde, könnte der Rezeptautor, der vielleicht nur ein bisschen HTML versteht, nichts damit anfangen. Also muss mindestens der Präfix ht: "abgestreift" werden, bevor das Formularfeld angezeigt werden kann.

Stellen wir uns nun also vor, dass wir in einigen Feldern eines Rezeptformulars <i> und/oder <a>tags verwenden haben und nun unser Rezept in das XML-Format re-konvertieren und herunterladen wollen. Ganz unabhängig von der js-Funktion save würde JavaScript die spitzen Klammern, die es in einem string findet, vor dem Herunterladen durch die entities&lt; bzw. &gt; ersetzen. Damit ist aber die Schema-Validierung der heruntergeladenen XML-codierten Rezepte müßig geworden, da die entsprechenden XHTML-Elemente nicht erkannt werden. Also muss ich in der js-Funktion save die entities&lt; bzw. &gt; wieder durch spitze Klammern ersetzen und außerdem das namespace-Präfix ht: hinzufügen.

Gerade eben habe ich ganz lässig behauptet, dass der Präfix ht: "abgestreift" werden müsse, wenn man Text in ein Formularfeld übernehmen will. Das ist leicht, wenn es sich beim Formularfeld um eine HTML textarea handelt: In diesem Fall wird der anzuzeigende Text in die textareatags eingeschlossen. Bei einem HTML input-Element wird der anzuzeigende Text jedoch als Wert des value-Attributs übergeben. Man müsste also spitze Klammern in einen Attributwert hineinschreiben. Da macht XSLT nicht mit. Mir blieb nichts anderes übrig, als eine js-Funktion zu schreiben, die onload den entsprechenden Attribut-Wert schreibt.

In Summe führen alle diese Tricks dazu, dass die XML-codierten Texte auch nach einem Durchgang durch eine Formularbearbeitung Schema-valide sind.

In diesem Post werde ich die Punkte 3 - 5 des Vorgehensmodells, das ich im vorigen Post vorgestellt habe, zusammenfassend diskutieren, nämlich Create a grid container, Calculate column-width, Determine gutter positions.

Ich habe einen grid container definiert und ihn boxtainer genannt, ein container für grid boxes.

Den boxtainer habe ich nicht in regelmäßige Spalten aufgeteilt. Ich habe mir vielmehr einige Flächen"typen" definert, die recht gut die Aufteilung der unterschiedlichen Seitentypen des Kochbuchs wiedergeben:

  • Es gibt Flächen, die die zur Verfügung stehende Seitenbreite voll ausnutzen; Klasse .full.
  • Es gibt breite Flächen links; Klasse .l-big.
  • Es gibt schmale Flächen rechts; Klasse .r-small.
  • Es gibt breite Flächen rechts; Klasse .r-big.
  • Es gibt schmale Flächen links; Klasse .l-small.
  • Es gibt eine Fläche für das fruschtique-Logo; Klasse .logo.
  • Es gibt eine Fläche für den Seitentitel; Klasse .title.

Hinzu kommen ggf. noch leere Flächen, die im grid-template durch . codiert werden.

Die Vorstellung ist, dass

  • Flächen der Klasse .full nur mit leeren Flächen links und rechts kombiniert werden sollen (aber nicht müssen),
  • Flächen der Klasse .r-small nur mit Flächen der Klasse .l-big und ggf. leeren Flächen (links, rechts und/oder dazwischen) kombiniert werden sollen,
  • Flächen der Klasse .l-small nur mit Flächen der Klasse .r-big und ggf. leeren Flächen (links, rechts und/oder dazwischen) kombiniert werden sollen,
  • Flächen der Klasse .logo und Flächen der Klasse .title sollen entweder die gesamte Breite einnehmen, oder es soll dazwischen eine leere Fläche eingebaut werden.

Mit den genannten Klassen zeichne ich die diversen Elemente der unterschiedlichen Seiten aus. Ich erläutere hier nur die Rezeptseiten:

  • Das fruschtique-Logo und das darunter angeordnete Menü gehören zur Klasse .logo.
  • Der Rezeptname gehört zur Klasse .title.
  • Das Rezeptbild gehört zur Klasse .l-big.
  • Der gesamte Rezepttext gehört zur Klasse .r-small.

Je nach Gerätetype weise ich diesen Klassen und den zusätzlichen leeren Flächen eine bestimmte Anzahl von fractions oder Pixeln zu. Ich zähle die unterschiedlichen Aufteilungen in Richtung von small nach big auf:

  • Mindestbreite 280px: Allen Klassen wird eine Fläche mit einer Breite von 1frzugewiesen, d.h. alle klassifizierten Elemente nehmen die volle Seitenbreite ein.
  • Mindestbreite 480px: Die Breite der Seite wird in 12 fractions im Verhältnis 5:2:5 aufgeteilt; zwischen .title- und .logo-Fläche wird eine leere Fläche eingeschoben; .l-big- und .r-small-Flächen haben ein Breitenverhältnis von 7:5; links und rechts davon gibt es keine leeren Flächen.
  • Mindestbreite 800px: Die Flächenaufteilung bleibt erhalten, zusätzlich werden links und rechts leere Flächen mit fester Breite von 8px vorgesehen.
  • Mindestbreite 1400px: Die Breite der Seite wird von links nach rechts wie folgt aufgeteilt: Leere Fläche mit automatischer Breite/600px/200px/600px/leere Fläche mit automatischer Breite. Die Aufteilung der Flächen mit fester Breite erfolgt nach dem gleichen Strickmuster wie oben.

So, jetzt geht's los mit der Implementierung einer Version des Kochbuchs, die nach den Regeln des responsive design gestaltet ist.

Hier habe ich unter der Überschrift Building your grid system ein für mich plausibles Vorgehensmodell gefunden. Es schlägt die folgenden acht Schritte vor:

  1. Choose a spec to create your grid with
  2. Set box-sizing to border-box
  3. Create a grid container
  4. Calculate column-width
  5. Determine gutter positions
  6. Create a debug grid
  7. Make layout variations
  8. Make your layouts responsive

Mein Kochbuch soll über Chrome, Firefox und Edge nutzbar sein. Chrome und Firefox unterstützen CSS Grids schon seit einiger Zeit. Endlich unterstützt nun auch die allerneuste Version von MS Edge, die im Creators Update von Windows 10 enthalten ist, alle Features von CSS Grids, wie hier berichtet wird. Deshalb habe ich mich entschlossen, mein responsives Kochbuch mit CSS Grids zu implementieren. Damit ich den Punkt 1 meiner Vorgehensweise abgehakt.

Punkt 2 ist eine Zeile Code. Das werde ich nicht weiter diskutieren.

Bevor ich die folgenden Punkte diskutiere, schiebe ich ein paar Vorüberlegungen ein.

Für alle Kochbuchseiten werde ich Grids für die folgenden Geräte-Typen entwickeln (Angaben in [css-px]:

  1. min-width: 280px:
    Smartphones im portrait mode
  2. min-width: 480px:
    Smartphones im landscape mode und Tablets im portrait mode
  3. min-width: 800px:
    Tablets im landscape mode
  4. min-width: 1400px:
    Desktops

Achtung beim Aufschreiben der css-Anweisungen! Ich werde mich an das Muster mobile first halten, und ich werde in den @media-Abfragen, die zur Erzeugung eines responsiven Verhaltens erforderlich sind, mit den oben dargestellten min-width-Bedingungen arbeiten. Diese beiden Dinge zusammengenommen haben mindestens zwei Implikationen:

  1. Alle css-Anweisungen, die nicht durch eine @media-Abfrage modifiziert sind, werden für das "kleinste" Gerät ausgelegt. Deshalb gibt es in meiner recipe.css-Datei z.B. die css-Anweisung .croppedImageRight {display: none;}, mit der verhindert wird, dass auf den Seiten about me und about cookbook "Schmuckbilder" angezeigt werden. Diese Bilder sollen nur auf größeren Bildschirmen widergegeben werden.
  2. Da der Browser die css-Anweisungen in der Reihenfolge abarbeitet, wie sie aufgeschrieben sind, und da die zuletzt abgearbeiteten Anweisungen das Aussehen des entsprechenden HTML-Elements bestimmen, müssen insbesondere die @media-Abfragen in einer bestimmten Reihenfolge aufgeschrieben werden. Bei der Kombination des Paradigmas mobile first mit min -width-Abfragen muss man die @media-Abfragen zwingend in der Reihenfolge "von klein nach groß" aufschreiben. Würde man die css-Anweisungen in der umgekehrten Reihenfolge aufschreiben, dann würde immer die css-Anweisung für das kleinste Gerät "gewinnen", da diese als letzte ausgeführt würde und die entsprechende min-width-Bedingung auch für alle "größeren" Geräte erfüllt ist.

Quintessenz: CSS hat ein bestimmtes processing model, und das ist leider nicht so deklarativ, wie die css-Anweisungen es vielleicht vermuten lassen. Und wie man es sich vielleicht wünschen würde.

Im nächsten Post werde ich erläutern, wie die Fläche in den verschiedenen Modi aufgeteilt wird.

Mein Grau ist rgb(77,77,77).

Warum?

Weil die Woche 7 Tage hat.

Punkt.

Bislang habe ich versucht, die Icons, die ich benötige, aus vorhandenen Icon-Sammlungen zu übernehmen, z.B. von der Site material.io. Keine gute Idee. Es hat sich gezeigt, dass diese Icons schlecht gemacht sind. Das betrifft nicht die Gestaltung, sondern die technische Umsetzung: Nicht alle dieser Icons haben einen transparenten Hintergrund. Das Problem dabei: Wenn man Icons ohne transparenten Hintergrund vor einem weißem Hintergrund zeigt, merkt man nicht, dass die Icons keinen transparenten, sondern einen weißen Hintergrund haben. Man merkt das Problem erst, wenn man diese Icons vor einem farbigen Hintergrund zeigt.

Ich habe versucht, die fehlerhaften Icons umzuprogrammieren. Keine gute Idee. Irfanview versagt, bei Xnview tappt man im Nebel.

Also: Selber Icons erstellen.

Das erwies sich als leichter, als ich erwartet hatte: Inkscape ist ein perfektes Tool, um Icons zu programmieren. Inkscape liefert Vorlagen in den für Icons üblichen Größen, z.B. 32 ✕ 32 px2, und das Ergebnis lässt sich leicht nach png exportieren. Der Hintergrund ist dabei automatisch transparent.

Also: Keine Angst vor der Erstellung von Icons.

Jetzt habe ich endlich verstanden, warum einige Leute mein Kochbuch in der Version 4.01 nicht über das Smartphone öffnen können:

Der übliche Smartphone-Benutzer hält sein Gerät im portrait mode, d.h. im Hochformat. In diesem Format zeigt der Smartphone-Bildschirm vom Kochbuch nur das Logo und den Schriftzug "Das Kochbuch". Dieser Bereich der Bildfläche ist nicht aktiv, d.h. bei Click passiert nichts. Erst, wenn man das Smartphone ins Querformat dreht, wird so viel vom Bild sichtbar, dass man in das Bild clicken kann. Dann klappt alles.

Diesen Umstand werde ich bei der nächsten Version berücksichtigen müssen.

Ich habe eine wunderbare Kollektion von historischen Kochbüchern gefunden:

Peter Breuer
Kochtagebücher von 1781–1957
Greenpeace Magazin Edition

Es ist Zeit, die weitere Entwicklung des Kochbuchs zu planen. Ich denke, für die Weiterentwicklung der Version 4 werden folgende Punkte eine Rolle spielen:

  • Eine auch für die Leserin erkennbare Versionierung ist erforderlich. Major: 4, minor: 01, 02 usw. Vielleicht sollte ein Punkt "Version" in das more dropdown aufgenommen werden.
  • Das Kochbuch muss auf kleinen Bildschirmen besser dargestellt werden: responsive design!
  • Bei einem kleinen Bildschirm im Hochformat soll nur die Zutatenliste eines Rezepts dargestellt werden: Einkaufshilfe.
  • Eine Menge von Rezepten ist inhaltlich zu überarbeiten.
  • Es fehlen immer noch viele Bilder.
  • Neue Rezepte sollen aufgenommen werden.
  • Der Erstellungsprozess muss noch verbessert werden; möglicherweise bedeutet das, dass ich nur noch mit IntelliJ arbeiten werde und Oxygen aufgebe.

Naja, und dann sollte natürlich auch noch eine Version 5 vorbereitet werden. Ziele:

  • Die Gestaltung des Kochbuchs folgt den Material Design-Regeln.
  • Das Kochbuch ist nicht nur für den Desktop verfügbar, sondern auch als Android-App.

So, mehr schreibe ich jetzt erst einmal nicht.