Lorem Ipsum mit 3

In diesem Monat werden wir uns erneut mit den Möglichkeiten von 3 befassen und einen Generator für Blind- bzw. Lorem-Ipsum-Texte programmieren. Der Generator kann mit einer Folge von Sätzen „gefüttert“ werden und gibt diese dann in zufälliger Reihenfolge – kombiniert mit einem ebenfalls zufälligen Satzzeichen – aus.

Dieser Beitrag geht wie der letzte auch auf eine Frage auf TeX.SX zurück.

Einleitung

Die grundlegenden Neuerungen in 3 haben wir bereits im letzten Monat kennengelernt, sodass wir nun direkt einsteigen können.

Ziel ist es, den Befehl

\lipsum{n}

so zu implementieren, dass er n aus einem gegebenen Vorrat zufällig gewählte Sätze und Satzzeichen kombiniert. Ergänzend soll es die folgenden Benutzerbefehle geben:

  • \SetLipsumPool{Satzvorrat} erzeugt den Satzvorrat aus einem Text. Dabei wird der Text am Punkt . in Sätze geteilt.
  • \AddToLipsumPool{Satzvorrat} ergänzt den bestehenden Satzvorrat.
  • \SetPunctPool{Satzzeichen} erzeugt den Satzzeichenvorrat. Dabei werden alle Zeichen als einzelnes Symbol gewertet.
  • \AddToPunctPool{Satzzeichen} ergänzt den bestehenden Satzzeichenvorrat.

Außerdem führen wir einen Debug-Modus ein, um die Generierung der Zufallszahlen bei Bedarf überwachen zu können.

Das Modul, also den Präfix für alle internen Befehle, nennen wir dieses Mal lipgen – von Lorem-Ipsum-Generator.

Datei lipsum-generator.tex

Wir laden zuerst die Dokumentklasse (FAQ 2) ohne Optionen. Eine Sprachoption für die Silbentrennung ist für unseren pseudolateinischen Blindtext unnötig.

\documentclass{scrartcl}

Als nächstes laden wir wie üblich die Pakete zur Kodierung (FAQ 3) und Schrift (FAQ 4)

\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}

Das Paket xparse laden wir, um \NewDocumentCommand (siehe FAQ 11) nutzen zu können.

\usepackage{xparse}

Um eine (pseudo-)zufällige Zahl zu generieren, wollen wir die Funktion random aus dem Paket pgfmath, das wir nun laden, verwenden.

\usepackage{pgfmath}

Für den Debug-Modus wollen wir farbige Elemente ausgeben. (Die Funktionen von xcolor wurden im Januar ausführlich besprochen.)

\usepackage{xcolor}

3-Syntax einschalten

\ExplSyntaxOn

Variablen und Konstanten

Zuerst definieren wir ein paar Variablen und eine Konstante, mit denen spätere Änderungen an den Einstellungen leicht machbar sind und mehr Konsistenz im Code gewährleistet wird. Wir verwenden dabei ganze Zahlen (int), Sequenzen (seq) sowie eine Bool’sche Variable (bool), die wir jeweils mit \xx_new:N definieren und mit \xx_set:Nn setzen. Außerdem werden wir mit \newkomafont noch ein KOMA-Script-Schriftelement für die Debug-Informationen definieren.

Vorrat der Sätze

\seq_new:N \g_lipgen_lipsum_pool_seq

Anzahl der Sätze im Vorrat

\int_new:N \g_lipgen_lipsum_pool_int

Vorrat der Satzzeichen

\seq_new:N \g_lipgen_punct_pool_seq

Anzahl der Satzzeichen im Vorrat

\int_new:N \g_lipgen_punct_pool_int

aktuelle Zufallszahl

\int_new:N \g_lipgen_current_int

vorherige Zufallszahl bei Sätzen

\int_new:N \g_lipgen_last_lipsum_int

vorherige Zufallszahl bei Satzzeichen

\int_new:N \g_lipgen_last_punct_int

aktuelle Zahl der Versuche zur Ermittlung einer Zufallszahl

\int_new:N \g_lipgen_tries_int

maximale Anzahl an Versuchen, um eine von der vorhergehenden verschiedene Zufallszahl zu finden

\int_new:N \c_lipgen_max_tries_int
\int_set:Nn \c_lipgen_max_tries_int { 10 }

Schalter für den Debug-Modus. Wenn diese Konstante true ist, werden in der PDF zusätzliche Informationen ausgegeben.

\bool_new:N \c_lipgen_debug_bool
%\bool_set_true:N \c_lipgen_debug_bool

Schrift der Debug-Informationen

\newkomafont { debuginfo } { \scriptsize \color { magenta } }

Befehle für den Satzvorrat

Nun definieren wir zwei Benutzerbefehle, um den Satzvorrat zu definieren (\SetLipsumPool) bzw. um ihn zu erweitern (\AddToLipsumPool). Beide greifen auf den ebenfalls zu definierenden internen Befehl \lipgen_add_to_lipsum_pool:n zurück, den wir als erstes implementieren werden.

Der Befehl wird mit \cs_new:Npn definiert und hat ein Argument (#1).

\cs_new:Npn \lipgen_add_to_lipsum_pool:n #1 {

Den Inhalt des Argumentes zerteilen wir am Punkt . in eine Sequenz, die in der temporären Variable \l_tmpa_seq gespeichert wird.

   \seq_gset_split:Nnn \l_tmpa_seq { . } { #1 }

Anschließend fügen wir die temporäre Variable und den bisherigen Vorrat zusammen und speichern das Ergebnis wieder in der Variable für den Vorrat.

   \seq_gconcat:NNN
      \g_lipgen_lipsum_pool_seq
      %   =
      \g_lipgen_lipsum_pool_seq
      %   +
      \l_tmpa_seq

Nun entfernen wir alle Dubletten und leeren Elemente aus dem Vorrat.

   \seq_gremove_duplicates:N \g_lipgen_lipsum_pool_seq
   \seq_remove_all:Nn \g_lipgen_lipsum_pool_seq { }

Zuletzt zählen wir noch die Anzahl der Elemente im Vorrat und speichern das Ergebnis in \g_lipgen_lipsum_pool_int.

   \int_gset:Nn \g_lipgen_lipsum_pool_int {
      \seq_count:N \g_lipgen_lipsum_pool_seq
   }

Ende der Definition

}

Mit \NewDocumentCommand (FAQ 11) definieren wir \SetLipsumPool nun so, dass zunächst der vorhandene Vorrat geleert wird, und dann nutzen wir \lipgen_add_to_lipsum_pool:n, um den neuen Vorrat anzulegen.

\NewDocumentCommand { \SetLipsumPool } { m } {
   \seq_gclear:N \g_lipgen_lipsum_pool_seq
   \lipgen_add_to_lipsum_pool:n { #1 }
}

Analog dazu definieren wir \AddToLipsumPool, nur dass wir hier den bestehenden Vorrat nicht löschen.

\NewDocumentCommand { \AddToLipsumPool } { m } {
   \lipgen_add_to_lipsum_pool:n { #1 }
}

Befehle für den Satzzeichenvorrat

Bei der Definition der Befehle für den Satzzeichenvorrat, gehen wir genau so vor, wie zuvor beim Satzvorrat.

Zunächst definieren wir wieder einen internen Befehl: Dieses Mal wird das Argument aber am leeren Trennzeichen { } getrennt, also in einzelne Zeichen zerlegt. Daher müssen wir auch keine leeren Elemente aussortieren. Die Suche nach doppelten Einträgen führen wir auch nicht durch, um das Vorkommen eines bestimmten Satzzeichens durch Erhöhen seines Anteils im Vorrat beeinflussen zu können.

\cs_new:Npn \lipgen_add_to_punct_pool:n #1 {
   \seq_gset_split:Nnn \l_tmpa_seq { } { #1 }
   \seq_gconcat:NNN
      \g_lipgen_punct_pool_seq
      %   =
      \g_lipgen_punct_pool_seq
      %   +
      \l_tmpa_seq
   \int_gset:Nn \g_lipgen_punct_pool_int {
      \seq_count:N \g_lipgen_punct_pool_seq
   }
}

Die Nutzerbefehle sind im Prinzip so aufgebaut wie ihre Pendants.

\NewDocumentCommand { \SetPunctPool } { m } {
   \seq_gclear:N \g_lipgen_punct_pool_seq
   \lipgen_add_to_punct_pool:n { #1 }
}
 
\NewDocumentCommand { \AddToPunctPool } { m } {
   \lipgen_add_to_punct_pool:n { #1 }
}

Befehl zum Generieren einer Zufallszahl

Eine (Pseudo-)Zufallszahl können wir einfach mit der PGF-Funktion random erzeugen, allerdings kann es dann passieren, dass eine so ermittelte Zahl (und damit ein Satz) mehrfach hintereinander wiederholt wird. Um das zu verhindern, definieren wir in \lipgen_get_random_number:NN eine etwas aufwändigere Variante zur Ermittlung einer Zufallszahl.

Der Befehl hat zwei Argumente:

  • #1: höchst mögliche Zufallszahl
  • #2: vorhergehende Zufallszahl, bzw. einfach eine Zahl, von der sich die neue Zufallszahl unterscheiden soll
\cs_new:Npn \lipgen_get_random_number:NN #1#2 {

Als erstes setzen wir die Anzahl der Versuche auf Null.

   \int_gzero:N \g_lipgen_tries_int

Dann beginnen wir eine Schleife, die solange läuft, wie die neue Zufallszahl der alten (#2) entspricht und die maximale Anzahl an Versuchen nicht erreicht ist.

Die Versuche werden von Null an gezählt, weshalb wir < und nicht <= für den Vergleich verwenden müssen.

   \bool_do_while:nn {
      \int_compare_p:n { #2 = \g_lipgen_current_int }
      &&
      \int_compare_p:n { \g_lipgen_tries_int < \c_lipgen_max_tries_int }
   } {

In der Schleife erzeugen wir als erstes eine neue Zufallszahl im Bereich von 1 bis #1, das Ergebnis steht uns anschließend als \pgfmathresult zur Verfügung.

      \pgfmathparse { random(1,#1) }

Dieses speichern wir in der Variable für die aktuelle Zufallszahl und erhöhen den Zähler für die Versuche.

      \int_gset:Nn \g_lipgen_current_int { \pgfmathresult }
      \int_gincr:N \g_lipgen_tries_int

Zuletzt geben wir mit \iow_term:x noch ein paar Informationen über den aktuellen Status der Zufallszahlenermittlung in der Konsole bzw. der .log-Datei aus. Das x in der Argumentdefinition bedeutet, dass der Argumentinhalt vor der Ausgabe komplett expandiert wird, andernfalls würden hier die Befehle direkt ausgegeben und nicht ausgewertet werden.

Vergleichen Sie die Ausgabe in der Konsole/.log, wenn Sie x durch n ersetzen

      \iow_term:x {
         [
            find ~ random ~ number: ~
            curr = \int_use:N \g_lipgen_current_int \c_space_tl
            last = \int_use:N #2 \c_space_tl
            try = \int_use:N \g_lipgen_tries_int
         ]
      }

Ende der Schleife

   }

Nachdem die Schleife beendet wurde, d. h. es wurde entweder eine von der vorigen verschiedene Zahl gefunden oder die maximale Anzahl an Versuchen erreicht, geben wir auch in der PDF ein paar Informationen aus, sofern der Debug-Modus eingeschaltet ist (also \c_lipgen_debug_bool gleich true ist).

   \bool_if:NT \c_lipgen_debug_bool {
      \par { \usekomafont{ debuginfo }
      ( berechne ~ zufällige ~ Nummer: ~
      aktuell\,=\,\int_use:N \g_lipgen_current_int ; ~
      letzte\,=\,\int_use:N #2 ; ~
      Versuche\,=\,\int_use:N \g_lipgen_tries_int )
      \par }
   }

Zum Schluss setzen wir als Vorbereitung für die nächste Suche nach einer Nummer die Variable der vorigen Nummer (#2) auf den Wert der aktuellen.

   \int_gset_eq:NN #2 \g_lipgen_current_int

Ende der Definition

}

Befehl für einen zufälligen Satz inkl. Satzzeichen

Nun wollen wir das eben definierte Makro benutzen, um einen Satz inkl. Satzzeichen aus dem Vorrat auszuwählen.

\cs_new_protected:Nn \lipgen_get_random_sentence: {

Dafür rufen wir als erstes unsere eigene Funktion auf und geben dabei die Anzahl der Sätze im Vorrat sowie die Nummer des letzten Satzes an.

   \lipgen_get_random_number:NN
      \g_lipgen_lipsum_pool_int
      \g_lipgen_last_lipsum_int

Die so ermittelte Zahl wurde in \g_lipgen_current_int gespeichert und die verwenden wir nun, um den entsprechenden Satz aus dem Vorrat mit \seq_item:Nn auszugeben.

Hier wäre eigentlich eine Variante \seq_item:NN praktisch. Diese ist aber nicht vorhanden und sie für diesen Zweck extra zu generieren lohnt sich auch nicht. Das ginge ggf. ganz einfach mit \cs_generate_variant:Nn \seq_item:Nn { NN }.

   \seq_item:Nn \g_lipgen_lipsum_pool_seq { \g_lipgen_current_int }

Auf die gleiche Weise wählen wir jetzt ein Satzzeichen aus.

   \lipgen_get_random_number:NN
      \g_lipgen_punct_pool_int
      \g_lipgen_last_punct_int
   \seq_item:Nn \g_lipgen_punct_pool_seq { \g_lipgen_current_int }

Und fügen zuletzt noch ein Leerzeichen an.

   \c_space_tl

Ende der Definition

}

Der Nutzerbefehl \lipsum

Nach der ganzen Vorarbeit können wir endlich den zentralen Nutzerbefehl \lipsum definieren.

\NewDocumentCommand { \lipsum } { m } {
 

Zunächst geben wir in der Konsole bzw. der .log-Datei aus, dass ein Block zufälliger Sätze beginnt. Dabei steht ^^J für einen Zeilenumbruch, der hier explizit angeben werden muss.

   \iow_term:x {
      ========================================= ^^J
      START ~ LIPSUMS ^^J
      -----------------------------------------
   }

Wenn wir uns im Debug-Modus befinden, geben wir den Beginn eines Blocks auch in der PDF aus.

\bool_if:NT \c_lipgen_debug_bool {
   \par \bigskip
   { \usekomafont{ debuginfo }
   \textbf { #1 ~ ZUFÄLLIGE ~ SÄTZE }
   \par }
}

Wenn der Benutzer den Befehl mit einer Zahl kleiner als Eins aufruft, geben wir FEHLER! aus, denn eine negative Zahl von Sätzen kann es nicht geben und null Sätze sind auch sinnlos.

   \int_compare:nTF { #1 < 1 } {
      FEHLER!
   } {

Mit \int_step_inline:nnnn durchlaufen wir dann die Zahlen von Eins (erstes Argument) bis #1 (drittes Argument) mit einer Schrittweite von Eins (zweites Argument) und wiederholen dabei jeweils den Code im vierten Argument: Zunächst geben wir hier wieder ein paar Debug-Informationen aus und dann mit \lipgen_get_random_sentence: einen zufälligen Satz mit Satzzeichen.

      \int_step_inline:nnnn { 1 } { 1 } { #1 } {
         \iow_term:x {
            ##1 . ~ SENTENCE
         }
         \bool_if:NT \c_lipgen_debug_bool {
            \par \smallskip
            { \usekomafont { debuginfo }
            \textbf { ##1. ~ Satz }
            \par }
         }
         \lipgen_get_random_sentence:
      }

Ende von \int_compare:nTF

   }

Auch das Blockende soll in der Konsole/.log markiert werden.

   \iow_term:x {
      ----------------------------------------- ^^J
      END ~ LIPSUMS ^^J
      =========================================
   }

Da von \lipgen_get_random_sentence: bereits ein Leerzeichen hinzugefügt wurde, können wir die auf \lipsum{n} folgenden Leerzeichen ignorieren – bzw. müssen das sogar, um ein doppeltes Leerzeichen zu verhindern.

   \ignorespaces

Ende der Definition

}

3-Syntax abschalten

\ExplSyntaxOff

Funktionen testen

Nun ist es an der Zeit, unsere Funktionen zu testen.

\begin{document}
 
\SetLipsumPool{
   Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis lectus elit,
   tempus quis efficitur ut, consequat sed lorem. Cras convallis et nibh id
   accumsan.
}

Wir geben hier den Punkt öfter an, um die Wahrscheinlichkeit, dass ein Satz mit einem Punkt endet, zu erhöhen.

\SetPunctPool{........:?!}
\lipsum{1}
 
\lipsum{10}
 
\lipsum{0}
 
\AddToLipsumPool{Dolorem sit lipsum amet consequtor.}
 
\lipsum{10}
 
\AddToLipsumPool{
   Pellentesque mollis, magna sed placerat mattis, quam nisl euismod dui, et
   sollicitudin orci lacus ac dolor. Phasellus pretium, purus ac lobortis
   mattis, leo libero lobortis ligula, ut rhoncus mauris purus at odio. Morbi
   vitae mollis ipsum. Morbi risus augue, feugiat quis posuere a, dapibus sed
   arcu. Fusce pharetra massa vitae tristique auctor. Sed sagittis placerat
   vestibulum. Curabitur a tempus arcu, eget ullamcorper urna. Maecenas
   malesuada placerat enim, sit amet commodo risus scelerisque vel. Sed vel
   feugiat est. Pellentesque habitant morbi tristique senectus et netus et
   malesuada fames ac turpis egestas. Praesent rhoncus facilisis quam in
   faucibus.
}
 
\lipsum{10}
 
\SetLipsumPool{Nur ein Satz im Vorrat.}
\SetPunctPool{;}
 
\lipsum{5}
 
\end{document}