Eine Typst-Extension für Quarto erstellen

Seit der Version 1.4 ist in quarto das Textsatzsystem Typst integriert. Typst ist als Alternative zu LaTeX entwickelt worden und zeichnet sich vor allem durch deutlich schnelleres rendering aus. In diesem Blogpost zeige ich, wie ich Typst-Extensions für Quarto erstelle.
quarto
typst
ubuntuusers
Autor:in

Joe Slam

Veröffentlichungsdatum

16 Nov. 2024 - 14:33

Geändert

16 Nov. 2024 - 16:28

Typst ist ein relativ neues open-source Textsatzsystem, das als Alternative zu LaTeX entwickelt wurde. Es zeichnet sich durch eine einfachere Syntax und eine intuitivere Bedienung aus, während es dennoch die leistungsstarken Formatierungs- und Gestaltungsmöglichkeiten bietet, die für wissenschaftliche und technische Dokumente erforderlich sind.

Seit der Version 1.4 (Januar 2024) ist Typst in Quarto enthalten und kann daher direkt verwendet werden um PDF-Dokumente zu erzeugen, siehe https://quarto.org/docs/output-formats/typst.html.

Vorteile von Typst gegenüber LaTeX

  • Es ist unglaublich schnell! Typst ist in Rust geschrieben und verwendet inkrementelle Kompilierung, was schnellere Kompilierungszeiten ermöglicht. Selbst bei einfachen einseitigen Dokumenten ist es spürbar schneller als LaTeX.
  • Es ist direkt in Quarto enthalten und muss nicht zusätzlich installiert werden.
  • Die Syntax ist (ein wenig :-) ) einfacher als bei LaTeX.
  • Die Fehlermeldungen sind aussagekräftiger als bei LaTeX.

Nachteile

  • LaTeX kann viel mehr als Typst.
  • Typst ist noch relativ neu, es kann noch nicht alles. Es hat nichtmals einen Wikipediaeintrag (Stand 11/2024).
  • Falls ihr euch von “KI” inspirieren lassen wollt: ChatGTP und Co. können Typst quasi gar nicht und erzeugen großen Müll-Code.

PDF-Dokumente

Seit Jahrzehnten (ja, ich bin alt) verwende ich LaTeX. Mittlerweile erzeuge ich alle meine Dokument mit Quarto, aber immerhin habe ich meine schönsten LaTeX Vorlagen in Quarto-Extensions ausgelagert, so dass ich sie “unter der Haube” immer noch verwende (siehe z.B. diesen Blogpost). Das funktioniert sehr gut, und eigentlich bin ich auch zufrieden damit.

Dennoch habe ich einfach mal aus Spaß Typst ausprobiert, und vor allem die Geschwindigkeit des rendering hat mich sehr begeistert. Meine LaTeX Dokumente benötigen mindestens 2 (manchmal sogar 3) LaTeX-Läufe, die für sich schon relativ lange brauchen, um durchzulaufen. Typst hingegen ist nach einem Lauf fertig, und dieser Lauf ist einfach unglaublich schnell.

Also habe ich auf den Bahnfahrten angefangen, meine LaTeX-Extensions um Typst zu erweitern.

Eine Quarto Extension erstellen

Um eine neue Extension zu erstellen folge ich grob der Anleitung auf der Quarto Webseite.

Für meine Extensions habe ich ein eigenes Verzeichnis. Per Terminal wechsel ich in dieses Verzeichnis und starte dort ein neues Extensionprojekt mit dem Befehl

Terminal
$ quarto create extension format

Nun wähle ich typst aus der Auswahlliste und gebe erneut den Namen der Extension (foobar) ein. Anschließend kann der Ordner z.B. mit vscodium geöffnet werden.

Ich bearbeite die Dateien nicht in RStudio, da dort kein Syntax Highlighting für Typst angeboten wird. Ich nutze Positron, aber grundsätzlich geht es natürlich mit jedem Texteditor.

In meinem Ordner liegt nun ein neues Verzeichnis foobar, und in diesem sind die folgenden Dateien und Verzeichnisse zu finden:

Dateistruktur im Ordner foobar
template.qmd
README.md
_extensions/
_extensions/foobar/
_extensions/foobar/_extension.yml
_extensions/foobar/typst-show.typ
_extensions/foobar/typst-template.typ

Die Dateien sind bereits mit Standardinhalt befüllt, so dass die die Datei template.qmd direkt gerendert werden kann. Probiert es mal direkt aus!

Schaut euch die generierten Dateien an:

  • template.qmd - Das Dokument, welches gerendert werden soll.
  • README.md - Eine README-Datei mit allen Infos zur Extension.
  • _extensions/ - Hier liegen alle Extensions, auf die template.qmd zugreifen kann.
  • _extensions/foobar/ - Hier liegt unsere Extension.
  • _extensions/foobar/_extension.yml - Hier geben wir die Metadaten unserer Extension (Name, Autor, Beschreibung, etc.) an, sowie die bereitsgestellten Formate nebst Standardparameter
  • _extensions/foobar/typst-show.typ - Diese Datei ruft unser eigentliches Template auf. Hier können zudem Pandoc-Metadaten an Funktionsparameter gemappt werden.
  • _extensions/foobar/typst-template.typ - Unsere eigentliche Templatedatei.

Für mich als Autodidakt war es herausfordernd, die Typst-Syntax zu lernen. Eine sehr wichtige Nachschlageseite hierfür ist https://typst.app/docs/. Die Standardinhalte von typst-show.typ und typst-template.typ haben mich zunächst stark verwirrt, so dass ich in diesem Blogbeispiel mit leeren .typ-Dateien anfangen möchte.

typst-show.typ

Die Datei typst-show.typ ruft unser eigentliches Template auf. Ich lösche den Standardinhalt und ersetze ihn mit dieser einzigen Funktion.

typst-show.typ
#show: foobar.with(

)

Innerhalb dieser Funktion können wir (später) YAML-Parameter abgreifen. Dies funktioniert allerdings auch in der Datei typst-template.typ, so dass ich es hier zunächst auslasse.

typst-template.typ

In der Datei typst-template.typ lebt unser eigentliches Template. Auch hier lösche ich den Standardinhalt und ersetze ihn durch

typst-template.typ
#let foobar(
  // Welche Werte erwartet mein Template?
  body
) = {
  // Hier beginnt die Ausgabe
  body
}

Die Datei besteht aus 2 Teilen, #let foobar() definiert unsere Templatefunktion (die, die von typst-show.typ aufgerufen wird) und legt fest, welche Parameter diese Funktion erwartet. Standardmäßig steht hier nur body, womit der Inhalt unserer template.qmd gemeint ist.

Im zweiten Teil, welcher mit ={ ... } eingeleitet wird, erfolgt das eigentliche Styling der Ausgabe. Auch hier steht derzeit nur body, was bedeutet, dass der Inhalt auf einer “blanken” Seite ausgegeben wird. Klickt mal jetzt auf “rendern”.

Typst-Befehle mit und ohne #

Was mich am Anfang am meisten verwirrt hat ist, dass Typst-Befehle und -Variablen manchmal ein # vorangestellt benötigen, und manchmal nicht. So wie ich es verstanden habe

  • wird bei jedem Befehl und jeder Variable ein # benötigt.
  • innerhalb von Funktionsdefinitionen (z.B. der zweite Teil unserer foobar Funktion) dürfen keine # verwendet werden. Alles, was in runden Klammern steht, benötigt kein #.
  • innerhalb von Funktionsaufrufen müssen wieder # gesetzt werden. Alles, was in eckigen Klammern steht, benötigt ein #.

YAML-Parameter direkt auslesen

Parameter aus dem YAML-Header können wie gewohnt über Pandoc gemappt werden. In unserer Extension wollen wir zunächst den title-Parameter auslesen und ausgeben.

Wir können dies einerseits direkt in der typst-template.typ vornehmen

typst-template.typ
#let foobar(
  // Welche Werte erwartet mein Template?
  body
) = {
  // Hier beginnt die Ausgabe
  
  // title anzeigen
  text(36pt)[$title$]
  
  body
}

Wenn ihr jetzt rendert, seht ihr den großen Titel am Dokumentanfang.

YAML-Parameter über typst-show.typ anmelden

Ein anderer Weg, um den Parameter title aus dem YAML-Header zu lesen, erfolgt in 2 Schritten.

  1. In der Datei typst-show.typ lesen wir den Parameter aus und weisen ihn der Variable meintitel zu:
typst-show.typ
#show: foobar.with(
  meintitel: "$title$",
)

Beachtet, dass $title$ in Anführungszeichen gewickelt werden muss. Wir können hier noch sicherstellen, dass die Zuweisung nur dann erfolgt, wenn der Parameter auch wirklich im YAML angegeben ist:

typst-show.typ
#show: foobar.with(
  $if(title)$
    meintitel: "$title$",
  $endif$
)
  1. In der Datei typst-template.typ geben wir an, dass die Variable meintitel zuvor ausgelesen wurde. Somit steht uns im unteren Teil des Templates die Variable #meintitel zur Verfügung, und der Befehl zur Titelausgabe ändert sich entsprechend in text(36pt)[#meintitel].
typst-template.typ
#let foobar(
  // Welche Werte erwartet mein Template?
  meintitel: none,
  body
) = {
  // Hier beginnt die Ausgabe
  
  // title anzeigen
  text(36pt)[#meintitel]
  
  body
}

Im oberen Teil haben wir meintitel: none, angegeben, was bedeutet, dass die Variable leer bleibt, sofern sie nicht im YAML angegeben ist. Natürlich können wir hier auch Standardwerte hinterlegen, z.B:

typst-template.typ
#let foobar(
  // Welche Werte erwartet mein Template?
  meintitel: "Du hast den Titel vergessen",
  body
) = {
  // Hier beginnt die Ausgabe
  
  // title anzeigen
  text(36pt)[#meintitel]
  
  body
}

Wenn Sie nun in template.qmd den YAML-Parameter title entfernen, greift typst auf den Standardwert "Du hast den Titel vergessen" zurück.

Was ist nun besser?

Ich habe noch nicht genau verstanden, wie die Parameter “am besten” abgegriffen werden. Es funktioniert jedenfalls alles wunderbar, wenn typst-show.typ quasi leer bleibt, und alle Parameter direkt in der typst-template.typ verarbeitet werden. Naja, ihr kennt nun beide Vorgehensweisen und könnt selbst auswählen…. :-)

Bilddateien bereitstellen (Sonderzeichen escapen)

Manche meiner Extensions bringen Logo-Dateien mit (z.B. meine Briefvorlage an der Hochschule). Die Logos liegen ebenfalls im _extensions-Ordner und werden über die _extension.yml angegeben:

_extension.yml
title: Foobar
author: Joe Slam
version: 1.0.0
quarto-required: ">=1.5.0"
contributes:
  formats:
    typst:
      logo: MeinLogo.png
      template-partials:
        - typst-template.typ
        - typst-show.typ
Dateistruktur im Ordner foobar
template.qmd
README.md
_extensions/
_extensions/foobar/
_extensions/foobar/_extension.yml
_extensions/foobar/typst-show.typ
_extensions/foobar/typst-template.typ
_extensions/foobar/MeinLogo.png

Das Problem besteht darin, dass der relative Pfad zur Logodatei ein Sonderzeichen _ enthält. Dieser wird von Typst in \_ “escaped”, so dass der Pfad nicht als _extensions/foobar/MeinLogo.png sonder als \_extensions/foobar/MeinLogo.png erkannt wird. Um dies wieder rückgängig zu machen, kann innerhalb der .typ-Dateien die Funktion OBJEKT.replace("\\", "") verwendet werden.

Zunächst greifen wir den Logopfad in der Datei typst-show.typ ab.

typst-show.typ
#show: foobar.with(
  $if(title)$
    meintitel: "$title$",
  $endif$
  $if(logo)$
    logo: "$logo$",
  $endif$
)

In der Datei typst-template.typ melden wir die Variable logo an und reparieren den kaputten Pfad.

typst-template.typ
#let foobar(
  // Welche Werte erwartet mein Template?
  meintitel: "Du hast den Titel vergessen",
  logo: none,
  body
) = {
  // Hier beginnt die Ausgabe

  // Logo-Pfad escapen
  let logo_path = logo.replace("\\", "")
  // Logo anzeigen
  image(logo_path, width: 70mm)

  // title anzeigen
  text(36pt)[#meintitel]

  body
}

Ausgabe stylen

Auf diese Weise erweitere ich meine Extension nach und nach, so dass alles nach meinen Wünschen ausgegeben wird.

Wie man genau die Seiteneigenschaften (DIN-A4, deutsche Sprache, Kopf- und Fußzeilen, etc.) beeinflusst, habe ich in der Typst-Dokumentation nachgelesen, siehe https://typst.app/docs/. Sehr hilfreich war für mich deren Guide for LaTeX users.

Beispiel

Herausgekommen ist beispielsweise diese simple Extension, die ich für Aushänge an der Hochschule nutze. Sie nimmt die Parameter title und logo entgegen. In der Kopfzeile wird rechts das Logo ausgegeben, und links ein schöner blauer Kasten mit dem title in weißer Schrift.

In unserer foobar-Extension müssen die Dateien wie folgt geändert werden:

_extension.yml
title: Foobar
author: Joe Slam
version: 1.0.0
quarto-required: ">=1.5.0"
contributes:
  formats:
    typst:
      logo: MeinLogo.png
      lang: de
      font: "Times New Roman"
      font-size: 12pt
      template-partials:
        - typst-template.typ
        - typst-show.typ
typst-show.typ
#show: foobar.with(
    // Diese Werte kommen aus dem YAML
    title: "$title$",
    lang: "$lang$",
    logo: "$logo$",
    font: "$font$",
    font-size: $font-size$,
)
typst-template.typ
#let foobar(
  // welche Wert werden aus dem YAML-Header gelesen?
  title: none,
  logo: none,
  lang: none,
  font: none,
  font-size: none,
  body
) = {

  // Hier startet die eigentliche Funktion

  // Der Logo-Pfad muss escaped werden
  let logo_path = logo.replace("\\", "")

  // Schriftart und Sprache einstellen
  set text(font: font,
           size: font-size,
           lang: lang,)
  // Blockschrift aktivieren
  set par(justify: true)
  
  // Farben definieren
  let HSNRblue1 = rgb("185191")
  let HSNRblue2 = rgb("07A1E2")

  // Seitengröße und -ränder festlegen
  set page(width: 210mm,
           height: 297mm,
           margin: (top: 30mm, bottom: 30mm, left: 20mm, right: 20mm),
           numbering: "1",
           number-align: center,
           // Kopfzeile
           header: grid(
                        columns: (1fr, 1fr),
                        align: (left, right),
                          rect(fill: HSNRblue1,
                               width: 70%,
                               outset: (x: 54pt))[#text(white, 18pt)[#title]],
                          image(logo_path, width: 70mm),
           ),
           // Fußzeile
           footer: align(center)[#context counter(page).display("1 von 1",both: true,)]
  )

  // Abstand zwischen Header und Body
  v(5mm)

  // Hauptteil des Dokuments
  body
}

Das sieht dann so aus:

Zudem habe ich meine quarto-letter-Extension auf typst angepasst und bei Github veröffentlicht, siehe https://github.com/produnis/quarto-letter.