# Codelistings und Tastenkappen

> Ein Shell-Leitfaden, dessen Codeblöcke als dunkle Listing-Kästen gesetzt sind, nach Token eingefärbt, mit Tasten als Chips in Form von Tastenkappen.

- HTML-Fassung: https://postext.dev/de/cookbook/code-listings-and-keycaps
- Rezept Nº 048 · Kästen und Anmerkungen · Stufe 2 (Mittel) · Ausgaben: Canvas
- Gattungen: Handbücher und Nachschlagewerke
- Benötigt postext ≥ 1.25.0 · getestet mit 1.31.0 am 2026-10-13
- Seiten: [49](https://postext.dev/cookbook/code-listings-and-keycaps/en/p01.webp?v=b3085fe5), [50](https://postext.dev/cookbook/code-listings-and-keycaps/en/p02.webp?v=b3085fe5), [51](https://postext.dev/cookbook/code-listings-and-keycaps/en/p03.webp?v=b3085fe5)
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=code-listings-and-keycaps&lang=en (.postext: https://postext.dev/cookbook/code-listings-and-keycaps/en/code-listings-and-keycaps.postext)
- Zuletzt aktualisiert: 2026-10-10
- Andere Sprachen: [en](https://postext.dev/en/cookbook/code-listings-and-keycaps.md), [es](https://postext.dev/es/cookbook/code-listings-and-keycaps.md), [ca](https://postext.dev/ca/cookbook/code-listings-and-keycaps.md), [fr](https://postext.dev/fr/cookbook/code-listings-and-keycaps.md), [it](https://postext.dev/it/cookbook/code-listings-and-keycaps.md), [pt](https://postext.dev/pt/cookbook/code-listings-and-keycaps.md), [zh](https://postext.dev/zh/cookbook/code-listings-and-keycaps.md), [ja](https://postext.dev/ja/cookbook/code-listings-and-keycaps.md), [ar](https://postext.dev/ar/cookbook/code-listings-and-keycaps.md)

## Kurz gesagt

Drei Seiten aus einem Einsteigerleitfaden für die Kommandozeile. Sie zeigen, wie Codebeispiele in dunklen farbigen Kästen stehen und Tasten wie Ctrl als kleine Knöpfe im Text erscheinen.

## Was Sie setzen

Drei Seiten aus Kapitel 4 von *The Shell, Gently*, einem erfundenen Taschenleitfaden für die Kommandozeile, auf einer Seite von 178 × 229 mm. Der Text steht im Blocksatz aus der Charis SIL in einer 100 mm breiten Spalte, mit einer Randspalte für Marginalien an der Außenseite. Jedes Listing ist ein fast schwarzer Kasten aus JetBrains Mono, der in diese Spalte hineinreicht, unter einem Tab mit dem Namen der Datei oder der Sitzung. Schlüsselwörter und getippte Befehle erscheinen in Bernstein, Zeichenketten in Grün und Kommentare in Grau. Tasten wie Ctrl und Tab sind kleine umrandete Tastenkappen in den Blocksatzzeilen, und ein zweispaltiger Spickzettel mit Ctrl-Kürzeln füllt den Fuß von Seite 50. Das Markdown behält gewöhnliche Codeblöcke, und die Engine setzt sie, wie sie sind.

**Dieses Rezept beantwortet:**

- Wie zeige ich Codelistings und Tastenkürzel?
- Wie erstelle ich Inline-Chips: Tastaturtasten, Schlagwörter, Wortspeicher für Übungen?
- Wie schreibe ich Dialogstriche, Jahreszahlen am Absatzanfang, Preise und wörtliche Symbole, ohne dass Markdown sie falsch liest?

## Die kurze Antwort

````js
// script.js, Zeilen 26–41
// A ``` fence is a listing set line by line as written: indents, blank lines and the output
// columns of a session are kept. The rest of the fence line (Terminal, backup.sh) is its
// title; with a label set, it goes on the tab.
// The text after a listing goes in :::paragraphs{style="resume"}: flush, as after a heading.
const resume = { id: 'resume', firstLineIndent: ZERO };
const codeStyle = {
  fontFamily: MONO, fontSize: pt(8.6), lineHeight: pt(12.4), color: col('code'),
  background: col('night'), span: 'page', // across the text and the margin
  padding: { top: mm(4), right: mm(5), bottom: mm(4), left: mm(5) },
  marginTop: mm(6), marginBottom: mm(2.5),
  label: { fontFamily: MONO, fontSize: pt(7), fontWeight: 700, color: col('phosphor'),
    background: col('night'), height: mm(5), offset: mm(5), paddingX: mm(3), // the tab
    position: 'top-left' },
  tokens: tokens(), // the colours, below
  inline: { fontSize: em(0.88) }, // `grep` in running text: the code face, no box
};
````

## Zutaten

**Zeigt**

- [Code-Listings](https://postext.dev/de/docs/configuration-styles.md#code-listings): ```-Blöcke als Code gesetzt: Zeile für Zeile wie getippt, in einem Kasten, mit Syntaxfarben, Zeilennummern, hervorgehobenen Zeilen und umbrochenen, kleiner gesetzten oder abgeschnittenen langen Zeilen.
- [Inline-Chips](https://postext.dev/de/docs/configuration-styles.md#chip-stile): Abgerundete Kästen um Wörter, die als Einheit umbrechen und nie gedehnt werden: Tastenkappen, Schlagwörter, Wörterlisten, Silben.
- [Kästen](https://postext.dev/de/docs/configuration-styles.md#kastenstile): Benannte Kastenformate für Hinweise, Tipps und Warnungen: Hintergrund, Rahmen, Radius, Randstreifen, Titel und eigene Typografie für Inhalt und Listen.

**Verwendet außerdem**

- [Nummerierte Kastenreiter](https://postext.dev/de/docs/configuration-styles.md#kastenstile)
- [Spalten in einem Kasten oder im Text](https://postext.dev/de/docs/document-format.md#columns)
- [Kästen über die Seite](https://postext.dev/de/docs/configuration-styles.md#der-container-callout)
- [Gleitende Kästen](https://postext.dev/de/docs/configuration-styles.md#der-container-callout)
- [Marginalien](https://postext.dev/de/docs/configuration-styles.md#kastenstile)
- [Anderthalb Spalten](https://postext.dev/de/docs/configuration-page-layout.md#layout-typen)
- [Randspalte für Gleitobjekte](https://postext.dev/de/docs/configuration-page-layout.md#layout)
- [Fett, kursiv und ihre Farben](https://postext.dev/de/docs/configuration-text.md#fließtext)
- [Nummerierte Listen](https://postext.dev/de/docs/configuration-text.md#nummerierte-listen)
- [Gestaltete Auftakte](https://postext.dev/de/docs/configuration-text.md#seitenbreite-und-erweiterte-gestaltung)
- [Überschriftenattribute](https://postext.dev/de/docs/document-format.md#attribute-von-überschriften)
- [Kolumnentitel und Seitenzahlen](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-page-layout.md#textelemente)
- [Gespiegelte Ränder](https://postext.dev/de/docs/configuration-page-layout.md#gespiegelte-ränder)
- [Semantische Farbpalette](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Optimaler Zeilenumbruch (Knuth-Plass)](https://postext.dev/de/docs/justification.md#knuth-plass-der-blick-auf-den-ganzen-absatz)
- [Mathematik](https://postext.dev/de/docs/document-format.md#mathematische-formeln)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`calloutStyles`](https://postext.dev/de/docs/configuration-styles.md#kastenstile), [`chipStyles`](https://postext.dev/de/docs/configuration-styles.md#chip-stile), `codeStyle`, [`colorPalette`](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette), [`footer`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), [`header`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), [`headings`](https://postext.dev/de/docs/configuration-text.md#überschriften), [`layout`](https://postext.dev/de/docs/configuration-page-layout.md#layout), [`locale`](https://postext.dev/de/docs/configuration-text.md#silbentrennung), [`orderedLists`](https://postext.dev/de/docs/configuration-text.md#nummerierte-listen), [`page`](https://postext.dev/de/docs/configuration-page-layout.md#seite), [`paragraphStyles`](https://postext.dev/de/docs/configuration-styles.md#absatzformate)

**APIs**

- [`buildDocument`](https://postext.dev/de/docs/configuration-programmatic-usage.md#ein-dokument-erzeugen), [`prepareFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden), [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern), [`withLoadedFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden)

**Schriften**

- Charis SIL (OFL-1.1), Sora (OFL-1.1), JetBrains Mono (OFL-1.1)

## Zubereitung

### 1 · Den Codeblöcken einen Kasten geben

Der Code für diesen Schritt steht oben in [der kurzen Antwort](https://postext.dev/de/cookbook/code-listings-and-keycaps.md#die-kurze-antwort). Jeder Block zwischen zwei Zeilen ```` ``` ```` im Markdown ist ein [Codeblock](https://postext.dev/de/docs/document-format.md#codeblöcke): Postext setzt seine Zeilen so, wie sie getippt wurden, eine Codezeile pro Satzzeile, mit den Einzügen, den Leerzeilen des Skripts und der zweispaltigen Ausgabe von `du`. `codeStyle` gestaltet alle Listings auf einmal ([Codelistings](https://postext.dev/de/docs/configuration-styles.md#code-listings)): Schrift und Zeilenabstand, die fast schwarze Fläche, den Innenabstand und den Raum darüber und darunter. Was in der öffnenden Zeile des Codeblocks auf die Sprache folgt (`Terminal`, `backup.sh`), ist der Titel des Listings, und weil `codeStyle.label` gesetzt ist, erscheint er auf einem [Tab](https://postext.dev/de/docs/configuration-styles.md#kastenstile) an der Oberkante des Kastens statt in einer Titelzeile. `codeStyle.inline` setzt den Inline-Code des Fließtexts, `ls` oder `grep -c`, in derselben Schrift in 0,88 em ([Inline-Code](https://postext.dev/de/docs/configuration-styles.md#inline-code)). Der Absatz nach einem Listing steht in `:::paragraphs{style="resume"}`, sodass er stumpf beginnt, wie nach einer Überschrift.

### 2 · Den Text schmal halten und den Code über den Rand laufen lassen

```js
// script.js, Zeilen 124–127
  page: { sizePreset: 'custom', width: mm(178), height: mm(229), margins: {
    top: mm(22), bottom: mm(21), left: mm(20), right: mm(OUTER), mirror: true } },
  layout: { layoutType: 'oneAndHalf', sideColumnPercent: 26, gutterWidth: mm(6),
    sideColumnRole: 'floats', sideColumnSide: 'outer' },
```

In 10 pt setzt die Charis SIL etwa 62 Zeichen in die 100 mm breite Spalte. Die Seitenspalte dieses [anderthalbspaltigen Layouts](https://postext.dev/de/docs/configuration-page-layout.md#layout-typen) nimmt nur Gleitobjekte und Marginalien auf, sodass der Text nie in sie hineinläuft. `span: 'page'` in `codeStyle` zieht jedes Listing über beide Spalten, 143 mm. Innerhalb seines Innenabstands fasst ein Kasten 73 Zeichen JetBrains Mono in 8,6 pt; die längste Zeile auf diesen Seiten, der Kommentar zu Beginn von `backup.sh`, hat 71.

### 3 · Den Code nach Token einfärben

```js
// script.js, Zeilen 45–54
function tokens() {
  const plain = { color: col('code') }; // every other kind in the listing's own colour
  return {
    keyword: { color: col('amber'), bold: true }, function: { color: col('amber'), bold: true },
    string: { color: col('phosphor'), italic: true },
    comment: { color: col('slate'), italic: false }, // upright: the default comment is italic
    prompt: { color: col('amber'), bold: true }, output: plain, // what you type, the answer
    number: plain, type: plain, variable: plain, meta: plain, operator: plain, punctuation: plain,
  };
}
```

Die Sprache in der öffnenden Zeile (`console`, `bash`) wählt den eingebauten Tokenizer, und `codeStyle.tokens` gibt jeder Token-Art Farbe, Schriftstärke und Neigung ([Syntaxhervorhebung](https://postext.dev/de/docs/configuration-styles.md#syntaxhervorhebung)). In `backup.sh` erscheinen Schlüsselwörter wie `for` und `done` und eingebaute Befehle wie `set` und `echo` in fettem Bernstein, Zeichenketten grün kursiv und Kommentare grau; Programme wie `mkdir` und `rsync` bleiben schlicht. Der Standardstil setzt Kommentare kursiv, deshalb hält `italic: false` sie aufrecht. In einem Codeblock `console` ist eine Zeile, die mit einem Prompt beginnt, ein Token `prompt` und jede andere Zeile `output`, sodass die getippten Befehle in fettem Bernstein erscheinen und die Antwort der Shell im hellen Grau des Listings bleibt. Die Arten, die das Rezept nicht einfärben will, darunter Zahlen, Variablen und Optionen, nehmen ebenfalls dieses Grau an.

### 4 · Tasten als Chips setzen

```js
// script.js, Zeilen 58–65
// Chips never break or stretch, so a line with keys puts all its slack in its word spaces;
// the breaker tries other breaks before a space passes 140 % (default 200 %).
const spacing = { maxWordSpacing: 1.4 }; // spread into bodyText
const chipStyles = [
  { id: 'key', fontFamily: MONO, fontSize: pt(7.8), bold: true, color: col('ink'),
    background: col('code'), borderColor: col('slate'), borderWidth: pt(0.6),
    borderRadius: pt(1.6), paddingX: em(0.45), paddingY: em(0.14), gap: em(0.3) },
];
```

Tasten sind Chips `key` mit heller Fläche und einer Kontur von 0,6 pt. Ihre Größe ist in Punkt angegeben, sodass eine Taste im Text zu 10 pt, in der Marginalie zu 8,6 pt und im Spickzettel zu 8,4 pt gleich 7,8 pt misst; `em(0.78)` würde die Tasten des Spickzettels auf 6,6 pt schrumpfen lassen. Ein Chip bricht nie über Zeilen um und dehnt sich nie ([Inline-Chips](https://postext.dev/de/docs/document-format.md#inline-chips)), sodass eine Zeile mit Tasten ihren ganzen Spielraum in die Wortabstände legt. `spacing` lässt den Zeilenumbruch andere Umbruchstellen versuchen, bevor ein Wortabstand über 140 % seiner normalen Breite wächst. Beim Standardwert von 200 % dehnen sich sechs Zeilen dieser Seiten weiter; mit der Einstellung erreicht die breiteste 132 %.

### 5 · Die Schritte im Raster nummerieren

```js
// script.js, Zeilen 91–94
const orderedLists = { fontFamily: DISPLAY, fontWeight: 800, color: col('ember'),
  gap: em(0.7), separator: '›', separatorGap: em(0.25), separatorFontFamily: MONO,
  separatorFontWeight: 700, separatorColor: col('muted'),
  marginTop: ZERO, marginBottom: ZERO }; // the default 1.5 em opens 5.3 mm above and below
```

Die Nummern stehen in der Sora 800 in der Akzentfarbe, der Trenner ist ein dicktengleiches `›` in Grau. Der Trenner hat eigene Schrift und Farbe und wird deshalb als eigener Lauf gezeichnet ([Nummerierte Listen](https://postext.dev/de/docs/configuration-text.md#nummerierte-listen)). Die Listenränder sind null, sodass die Schritte im 14,5-pt-Raster des umgebenden Texts weiterlaufen. Der Standardwert von 1,5 em würde über den Schritten 5,3 mm öffnen und den Spickzettel auf Seite 51 schieben.

### 6 · Den Spickzettel an den Fuß der Seite gleiten lassen

```js
// script.js, Zeilen 69–76
const sheet = { id: 'sheet', background: col('night'), span: 'page', placement: 'bottom',
  marginTop: mm(6), marginBottom: mm(2.5),
  columnGap: mm(8), padding: { top: mm(5), right: mm(6), bottom: mm(5.5), left: mm(6) },
  titleStyle: { fontFamily: MONO, fontSize: pt(7.5), fontWeight: 700, gap: mm(3.5),
    color: col('phosphor'), textTransform: 'uppercase', letterSpacing: pt(1.5) },
  body: { fontFamily: DISPLAY, fontSize: pt(8.4), lineHeight: pt(13), textAlign: 'left',
    color: col('code'), boldColor: col('amber'), italicColor: col('phosphor'),
    paragraphSpacing: false, firstLineIndent: ZERO } };
```

Der Spickzettel ist ein Kastenstil mit der Fläche der Listings, der Sora für die Aktionen und `placement: 'bottom'`, das ihn an den Fuß von Seite 50 gleiten lässt ([Gleitende Kästen](https://postext.dev/de/docs/configuration-styles.md#der-container-callout)). Bliebe er im Textfluss, bräuchte ein Kasten über die ganze Seitenbreite Platz für sich und zwei Textzeilen darunter. Hier würde der Kasten auf Seite 51 rücken, unter den Schritten 58 mm leer lassen, und das Kapitel liefe auf vier Seiten. Im Markdown eröffnet `:::columns{count=2 breaks="8"}` die rechte Spalte beim achten Block, ihrer Überschrift. Der Ausgleich allein bricht nach Höhe um, und wenn eine Aktion auf eine zweite Zeile läuft, bleibt *Commands and history* am Fuß der linken Spalte stehen. Jeder Eintrag beginnt mit denselben zwei Chips, `Ctrl` und einem Buchstaben, beide in der dicktengleichen Schrift, sodass jede Aktion im selben Abstand vom linken Rand ihrer Spalte beginnt.

## Das ganze Rezept

Eine Datei, aus dem Ordner des Rezepts zusammengesetzt, mit dem Beispieltext und dem gemeinsamen Baukasten der Rezepte bereits eingebettet; sie baut ihre eigene Seite auf. Zum Ausführen setzen Sie sie in ein `<script type="module">` auf einer leeren Seite oder fügen sie in das JS-Feld eines neuen CodePen-Pens ein (als Modul). Sie importiert postext von esm.sh, es gibt also nichts zu installieren oder zu bauen.

- Quellordner: https://github.com/drnachio/postext/tree/main/cookbook/code-listings-and-keycaps

### script.js

````js
// ═══ Postext Cookbook · Nº 048 · Code listings and keycaps ═══
// https://postext.dev/en/cookbook/code-listings-and-keycaps
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Charis SIL, Sora, JetBrains Mono (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import {
  buildDocument, prepareFonts, withLoadedFonts, renderPageToCanvas,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'code-listings-and-keycaps';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { ink: '#1b1f24', muted: '#5c636b', ember: '#9a5410', // text, heads, accent
  night: '#0e1116', code: '#d3d9df', amber: '#f2b134', phosphor: '#3ddc84', // the listings
  slate: '#8a939d' }; // comments in a listing, the outline of a key (code is its face)
// Every colour is linked to its palette entry by id.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// The engine's defaults link to 'main-color': point it at the accent, so nothing prints blue.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.ember })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
const [TEXT, DISPLAY, MONO] = ['Charis SIL', 'Sora', 'JetBrains Mono'];
const LEAD = 14.5; // pt: the body leading, the page's baseline grid
const ZERO = pt(0);

// #region answer: a fence is a code listing; codeStyle gives it a dark box
// A ``` fence is a listing set line by line as written: indents, blank lines and the output
// columns of a session are kept. The rest of the fence line (Terminal, backup.sh) is its
// title; with a label set, it goes on the tab.
// The text after a listing goes in :::paragraphs{style="resume"}: flush, as after a heading.
const resume = { id: 'resume', firstLineIndent: ZERO };
const codeStyle = {
  fontFamily: MONO, fontSize: pt(8.6), lineHeight: pt(12.4), color: col('code'),
  background: col('night'), span: 'page', // across the text and the margin
  padding: { top: mm(4), right: mm(5), bottom: mm(4), left: mm(5) },
  marginTop: mm(6), marginBottom: mm(2.5),
  label: { fontFamily: MONO, fontSize: pt(7), fontWeight: 700, color: col('phosphor'),
    background: col('night'), height: mm(5), offset: mm(5), paddingX: mm(3), // the tab
    position: 'top-left' },
  tokens: tokens(), // the colours, below
  inline: { fontSize: em(0.88) }, // `grep` in running text: the code face, no box
};
// #endregion

// #region paint: keywords amber, strings green, comments slate; a session's typed lines amber
function tokens() {
  const plain = { color: col('code') }; // every other kind in the listing's own colour
  return {
    keyword: { color: col('amber'), bold: true }, function: { color: col('amber'), bold: true },
    string: { color: col('phosphor'), italic: true },
    comment: { color: col('slate'), italic: false }, // upright: the default comment is italic
    prompt: { color: col('amber'), bold: true }, output: plain, // what you type, the answer
    number: plain, type: plain, variable: plain, meta: plain, operator: plain, punctuation: plain,
  };
}
// #endregion

// #region keycaps: keys are chips
// Chips never break or stretch, so a line with keys puts all its slack in its word spaces;
// the breaker tries other breaks before a space passes 140 % (default 200 %).
const spacing = { maxWordSpacing: 1.4 }; // spread into bodyText
const chipStyles = [
  { id: 'key', fontFamily: MONO, fontSize: pt(7.8), bold: true, color: col('ink'),
    background: col('code'), borderColor: col('slate'), borderWidth: pt(0.6),
    borderRadius: pt(1.6), paddingX: em(0.45), paddingY: em(0.14), gap: em(0.3) },
];
// #endregion

// #region sheet: a two-column cheat sheet floated to the foot of its page
const sheet = { id: 'sheet', background: col('night'), span: 'page', placement: 'bottom',
  marginTop: mm(6), marginBottom: mm(2.5),
  columnGap: mm(8), padding: { top: mm(5), right: mm(6), bottom: mm(5.5), left: mm(6) },
  titleStyle: { fontFamily: MONO, fontSize: pt(7.5), fontWeight: 700, gap: mm(3.5),
    color: col('phosphor'), textTransform: 'uppercase', letterSpacing: pt(1.5) },
  body: { fontFamily: DISPLAY, fontSize: pt(8.4), lineHeight: pt(13), textAlign: 'left',
    color: col('code'), boldColor: col('amber'), italicColor: col('phosphor'),
    paragraphSpacing: false, firstLineIndent: ZERO } };
// #endregion

const aside = { id: 'aside', span: 'side', backgroundEnabled: false, // notes in the margin
  stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('ember') },
  padding: { top: mm(2.2), right: ZERO, bottom: ZERO, left: ZERO },
  titleStyle: { fontFamily: MONO, fontSize: pt(7.5), fontWeight: 700, color: col('ember'),
    textTransform: 'uppercase', letterSpacing: pt(1.2), gap: mm(1.2) },
  body: { fontFamily: TEXT, fontSize: pt(8.6), lineHeight: pt(12.5), textAlign: 'left',
    firstLineIndent: ZERO } };
const colophon = { ...aside, id: 'colophon', stripe: { enabled: false }, body: { ...aside.body,
  fontFamily: MONO, fontSize: pt(7.5), lineHeight: pt(10.5), color: col('muted'),
  italicColor: col('muted') } };

// #region steps: numbered steps on the grid, a prompt sign for a separator
const orderedLists = { fontFamily: DISPLAY, fontWeight: 800, color: col('ember'),
  gap: em(0.7), separator: '›', separatorGap: em(0.25), separatorFontFamily: MONO,
  separatorFontWeight: 700, separatorColor: col('muted'),
  marginTop: ZERO, marginBottom: ZERO }; // the default 1.5 em opens 5.3 mm above and below
// #endregion

const OUTER = 15; // mm: the outer margin; the running heads align to it
const text = (id, content, family, size, look, placement) => ({ kind: 'text', id, content,
  fontFamily: family, fontSize: pt(size), color: col('ink'), placement, ...look,
  align: 'left' }); // design text is centred by default; an opener wraps its text
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' },
  offset: { x: ZERO, y: mm(y) }, size: { width } });
const opener = { enabled: true, slot: { elements: [
  text('kicker', '{attr.kicker}', MONO, 8, { fontWeight: 700, letterSpacing: pt(1.6),
    textTransform: 'uppercase', color: col('ember') },
  { anchor: { to: 'container', edge: 'top-left' }, offset: { x: ZERO, y: mm(4) } }),
  text('title', '{titleText}', DISPLAY, 33, { fontWeight: 800, lineHeight: 1.04 },
    below('kicker', 3.5, mm(118))),
  text('lead', '{attr.lead}', TEXT, 12, { italic: true, lineHeight: 1.36 },
    below('title', 5, 'fill')),
] } };
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body', fontFamily: MONO, fontSize: pt(7.5),
  letterSpacing: pt(1.1), textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...extra,
});
const folio = { fontWeight: 700, color: col('ember') };

const config = () => ({
  locale: t({ en: 'en-us', es: 'es' }), // the hyphenation patterns of each edition
  colorPalette, chipStyles, orderedLists, paragraphStyles: [resume],
  calloutStyles: [sheet, aside, colophon], codeStyle,
  // #region page: a text column and a margin column that only listings and notes enter
  page: { sizePreset: 'custom', width: mm(178), height: mm(229), margins: {
    top: mm(22), bottom: mm(21), left: mm(20), right: mm(OUTER), mirror: true } },
  layout: { layoutType: 'oneAndHalf', sideColumnPercent: 26, gutterWidth: mm(6),
    sideColumnRole: 'floats', sideColumnSide: 'outer' },
  // #endregion
  bodyText: { ...spacing, // keycaps
    fontFamily: TEXT, fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    firstLineIndent: mm(4.5), indentAfterHeading: false },
  headings: { fontFamily: DISPLAY, color: col('ink'), fontWeight: 800, levels: [
    // parity 'odd': the next recto; the default 'always-odd' also leaves a blank page before it.
    { level: 1, breakBefore: { enabled: true, parity: 'odd' }, advancedDesign: opener },
    { level: 2, fontSize: pt(13), lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: ZERO },
  ] },
  header: { elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 8),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 8)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ] },
  footer: { elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
    ...folio, pages: 'opener', // the opener has no running head: its folio drops to the foot
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: ZERO, y: mm(9) } } })] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = `---
title: "The Shell, Gently"
subtitle: "A pocket guide to the command line"
author: "Tove Ahlberg"
---

# Small tools, joined {kicker="Chapter 4" lead="How the pipe character chains programs that each do one job to answer a question about a folder."}

Each program in this chapter does one job. \`ls\` lists the names in a folder, \`grep\` keeps the lines that match a pattern, \`sort\` puts lines in order and \`du\` reports how much disk space a file takes. The pipe, the \`|\` character, sends whatever one program prints into the next one, so you can chain them on a single line and read the answer at the end.

:::callout{type="aside" title="The prompt"}
On a Mac, zsh prints \`%\` instead of \`$\`.
:::

Try it in a folder of photographs. In the listings, a line that starts with a dollar sign is one you type, leaving out the dollar, and run with :chip[Enter]{style="key"}. The dollar is the prompt, which the shell prints to show it is waiting for you. The lines under it are the shell’s answer.

\`\`\`console Terminal
$ cd ~/Pictures/2025
$ ls | grep -c 'JPG$'
268
$ ls | grep -v 'JPG$'
IMG_0413.MOV
IMG_0977.MOV
IMG_1502.PNG
$ du -sh *.MOV | sort -rh
812M    IMG_0977.MOV
455M    IMG_0413.MOV
\`\`\`

:::paragraphs{style="resume"}
\`grep -c\` counts the matching lines instead of printing them, and \`-v\` keeps the lines that do not match. The single quotes hand the pattern to \`grep\` as typed; in it, \`$\` marks the end of the line. The star in the last command belongs to the shell: before \`du\` starts, \`*.MOV\` is already the list of names ending in \`.MOV\`.
:::

:::callout{type="aside" title="On a Mac"}
:chip[Ctrl]{style="key"} is :chip[control]{style="key"}

:chip[Enter]{style="key"} is :chip[return]{style="key"}

Shortcuts use :chip[control]{style="key"}, not :chip[command]{style="key"}.
:::

## When a command will not stop

Sooner or later you will start a command that does not finish. Type \`grep JPG\` with no file after it, and \`grep\` sits waiting for you to type the lines it should search. To stop it, hold :chip[Ctrl]{style="key"} and press :chip[C]{style="key"}; the prompt comes back and nothing has changed. To end its input properly instead, press :chip[Ctrl]{style="key"} :chip[D]{style="key"} at the start of an empty line; \`grep\` reads it as the end of its input. :chip[Ctrl]{style="key"} :chip[C]{style="key"} also stops a \`ping\`, which would otherwise print a line every second until you close the window.

The shell also saves you typing. After the first letters of a file or folder name, press :chip[Tab]{style="key"} and the shell fills in the rest; when more than one name fits, it lists them (bash waits for a second :chip[Tab]{style="key"}). :chip[↑]{style="key"} brings back the last command, and each press goes one further back, so a pipeline with a typo can be mended instead of typed again.

1. Type \`cd ~/Pic\` and press :chip[Tab]{style="key"} to complete the folder name, \`Pictures/\`, then add \`2025\` and press :chip[Enter]{style="key"}.
2. Press :chip[↑]{style="key"} until \`ls | grep -v 'JPG$'\` is back on the line.
3. Hold :chip[Ctrl]{style="key"} and press :chip[A]{style="key"} to jump to the start of the line, then :chip[Ctrl]{style="key"} :chip[E]{style="key"} to return to the end.
4. Type \`| sort -r\` and press :chip[Enter]{style="key"}. The same names come back in reverse order.

:::callout{type="sheet" title="Cheat sheet · bash and zsh"}
:::columns{count=2 breaks="8"}
**On the line**

:chip[Ctrl]{style="key"} :chip[A]{style="key"} start of the line

:chip[Ctrl]{style="key"} :chip[E]{style="key"} end of the line

:chip[Ctrl]{style="key"} :chip[W]{style="key"} cut the word to the left

:chip[Ctrl]{style="key"} :chip[K]{style="key"} cut to the end of the line

:chip[Ctrl]{style="key"} :chip[Y]{style="key"} paste what you cut

:chip[Ctrl]{style="key"} :chip[T]{style="key"} swap two letters

**Commands and history**

:chip[Ctrl]{style="key"} :chip[R]{style="key"} search earlier commands

:chip[Ctrl]{style="key"} :chip[P]{style="key"} the previous command

:chip[Ctrl]{style="key"} :chip[C]{style="key"} stop the running command

:chip[Ctrl]{style="key"} :chip[Z]{style="key"} pause it; \`fg\` resumes it

:chip[Ctrl]{style="key"} :chip[L]{style="key"} clear the screen

:chip[Ctrl]{style="key"} :chip[D]{style="key"} close the shell (empty line)
:::
:::

## A script to keep

Commands you type every week are worth keeping in a file. The one below copies each folder in Documents to an external disk, into a new folder named after the day’s date. Save it as \`backup.sh\` in your home folder.

\`\`\`bash backup.sh
#!/usr/bin/env bash
# Copy each folder in ~/Documents to a dated folder on the backup disk.
set -euo pipefail

src="$HOME/Documents"
dest="/Volumes/Backup/$(date +%F)"

mkdir -p "$dest"
for dir in "$src"/*/; do
  name=$(basename "$dir")
  rsync -a "$dir" "$dest/$name/"
  echo "copied $name"
done
\`\`\`

:::callout{type="aside" title="Archive mode"}
\`rsync -a\` copies the subfolders too and keeps each file’s dates and permissions.
:::

:::paragraphs{style="resume"}
The first line, the *shebang*, names the program that runs the file. \`set -euo pipefail\` stops the script at the first command that fails, so it never carries on with half a backup. \`$(date +%F)\` runs \`date\` and puts what it prints, such as 2026-09-26, into the path. The quotes round each variable keep a folder called My Taxes in one piece; without them the shell would split the name at the space and \`rsync\` would look for two folders that do not exist.
:::

Run \`chmod +x backup.sh\` once to make the file executable, then start it with \`./backup.sh\`. On Linux an external disk usually appears under \`/media\`, in a folder named after your user, so change the \`dest\` line to match.

:::callout{type="colophon"}
*The Shell, Gently* is a fictional book written for the Postext Cookbook. Set in Charis SIL, Sora and JetBrains Mono (SIL OFL). Text: original, CC BY 4.0.
:::

Chapter 5 points \`grep\` at the log files under \`/var/log\`, where a pipeline of three commands counts how many errors the system logged on each day of the past week.
`; // content.<lang>.md, inlined by the Cookbook
const continuation = { pageIndexOffset: 48, pageNumbering: { startAt: 49 } }; // p. 49, a recto

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { 'Charis SIL': ['400', '400i'], Sora: ['400', '700', '800'],
  'JetBrains Mono': ['400', '400i', '700'] };

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await prepareFonts(markdown, config(), kitFonts(FONTS));
const build = () => buildDocument({ markdown, continuation }, config());
const doc = await withLoadedFonts(build, { ...kitFonts(FONTS), text: markdown });
showPages(doc, { title: t({ en: 'The Shell, Gently', es: 'La terminal, con calma' }) });

// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ─────

// ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook
function mm(value) { return { value, unit: 'mm' }; }
function pt(value) { return { value, unit: 'pt' }; }
function em(value) { return { value, unit: 'em' }; }
/** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */
function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; }
/** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */
function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; }

// ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook
// Postext loads the faces a config asks for before layout (prepareFonts,
// buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the
// files the PDF embeds too.

/** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts:
 *  the Fontsource resolver, and the faces of FONTS loaded with the ones the
 *  config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */
function kitFonts(FONTS = {}) {
  const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) =>
    ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' })));
  return { resolve: fontsourceResolver, faces };
}

/** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek
 *  when `text` needs them and the family ships them (kitSubsetsFor), each with
 *  its unicode-range. Null for a weight or slant Fontsource does not ship. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null;
  kitStatus('Loading fonts…');
  const at = await fontsourceAt(id);
  return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({
    source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`,
    unicodeRange: kitRanges()[subset],
  }));
}

/** The unicode-range of each Fontsource file the kit loads (a function: the
 *  kit is inlined after the pen's code, which runs before a const would be set). */
function kitRanges() {
  return {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
    greek: 'U+0370-03FF',
  };
}

/** For the cjk, arabic and comics blocks, which add their own files: the
 *  faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  The files are added once all have loaded, in the order asked (latin, then
 *  latin-ext, then greek), whatever order they arrive in: the last one added
 *  wins a character two files hold. Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = kitRanges();
  const jobs = [];
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal'])
      .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await
    const at = fontsourceAt(id);
    const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null;
    const subsets = ['latin', ...kitSubsetsFor(text, meta)];
    for (const [weight, style] of todo) {
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().catch(() => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return ready.filter((face) => face && document.fonts.add(face)).length;
}

/** A loaded FontFace covers this family, weight and style (fonts.check() would
 *  also say yes for families nobody declared). */
function hasFace(family, weight, style) {
  for (const face of document.fonts) {
    if (face.status !== 'loaded' || face.style !== style) continue;
    if (face.family.replace(/^["']|["']$/g, '') !== family) continue;
    const [low, high = low] = face.weight.split(' ').map(Number);
    if (weight >= low && weight <= high) return true;
  }
  return false;
}

/** The files beyond latin `text` needs that `meta`'s family ships. */
function kitSubsetsFor(text, meta) {
  return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']]
    .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x);
}

/** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */
function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); }

/** A package's folder at the one version @5 names now (#766). */
function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) {
  return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json())
    .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`);
}

/** The family's Fontsource metadata (weights, styles, subsets), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

// ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook
/** The pages as spreads on a dark desk, page 1 alone, then verso | recto,
 *  each painted when it scrolls near. Other options go to renderPageToCanvas. */
function showPages(docs, { title, width = 460, ...render } = {}) {
  const root = viewer(title);
  const pages = [docs].flat().flatMap((doc) =>
    doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index })));
  const spreads = [];
  let verso = null;
  for (const p of pages) {
    if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; }
    else { spreads.push([verso, p]); verso = null; }
  }
  if (verso) spreads.push([verso, null]);
  const density = Math.min(window.devicePixelRatio || 1, 2);
  showPages.painter?.disconnect();
  const painter = new IntersectionObserver((entries) => {
    for (const { isIntersecting, target } of entries) {
      if (!isIntersecting) continue;
      painter.unobserve(target);
      const { doc, page } = target.postext;
      renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width });
    }
  }, { rootMargin: '800px' });
  showPages.painter = painter;
  root.replaceChildren(...spreads.map((pair) => {
    const spread = document.createElement('div');
    spread.className = 'pt-spread';
    for (const p of pair) {
      const figure = document.createElement('figure');
      if (p) {
        const label = p.page.pageLabel || String(p.n + 1);
        const canvas = document.createElement('canvas');
        canvas.postext = p;
        canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`;
        canvas.setAttribute('role', 'img');
        canvas.setAttribute('aria-label', `Page ${label}`);
        const folio = document.createElement('figcaption');
        folio.textContent = label;
        figure.append(canvas, folio);
        painter.observe(canvas);
      } else figure.className = 'pt-blank';
      spread.append(figure);
    }
    return spread;
  }));
  kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`);
  document.documentElement.dataset.postext = 'ready';
  return pages.length;
}

/** The desk, the bar and the error reporting, created once. */
function viewer(title) {
  if (!document.getElementById('pt-kit')) {
    document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit">
      :root { color-scheme: dark; }
      body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; }
      #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center;
        gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px);
        border-bottom: 1px solid #23262d; }
      #pt-bar strong { color: #f4f1ea; font-weight: 600; }
      #pt-actions { display: flex; gap: 12px; margin-left: auto; }
      #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
      #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; }
      .pt-spread { display: flex; }
      .pt-spread figure { margin: 0; width: min(460px, 44vw); }
      .pt-spread canvas { display: block; width: 100%; background: #fff;
        box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif;
        letter-spacing: .18em; text-transform: uppercase; color: #6c7079; }
      .pt-blank { visibility: hidden; }
      @media (max-width: 760px) {
        .pt-spread { flex-direction: column; gap: 32px; }
        .pt-spread figure { width: min(460px, 92vw); }
        .pt-blank { display: none; }
      }
    </style>`);
    document.body.insertAdjacentHTML('afterbegin',
      '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>');
    document.getElementById('pt-title').textContent = document.title || 'Postext';
    addEventListener('error', (event) => kitFail(event.error ?? event.message));
    addEventListener('unhandledrejection', (event) => kitFail(event.reason));
  }
  if (title) document.getElementById('pt-title').textContent = title;
  return document.getElementById('pages')
    ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' }));
}

function kitStatus(text) {
  viewer();
  document.getElementById('pt-status').textContent = text;
}

function kitFail(error) {
  document.documentElement.dataset.postext = 'error';
  kitStatus(`Error: ${error?.message ?? error}`);
}

// ─── /Kit ───────────────────────────────────────────────────────────────────────
````

## Varianten

### Den Tab rechts aufhängen

Der Tab rückt an die obere rechte Ecke des Kastens, die auf einer rechten Seite über der Randspalte steht.

```diff
-    position: 'top-left' },
+    position: 'top-right' },
```

### Die Zeilen nummerieren

Jedes Listing bekommt graue Zeilennummern in einem Steg innerhalb seines Kastens. Der Steg nimmt etwa vier der 73 Zeichen ein, sodass der Kommentar zu Beginn von `backup.sh` nicht mehr passt: Er bricht nach `backup` um, und `disk.` läuft hinter einem `»` darunter weiter. Die Prüfung des Rezeptbuchs meldet das als Warnung `codeOverflow`; kürzen Sie den Kommentar, damit das Skript bei seinen 13 Zeilen bleibt. Ein einzelnes Listing bekommt Nummern mit `{lineNumbers}` in seiner öffnenden Zeile.

```diff
+  lineNumbers: true, lineNumberColor: col('slate'),
   tokens: tokens(), // the colours, below
```

### Kommentaren die Farbe der Zeichenketten geben

Die Kommentare nehmen das Grün der Zeichenketten an, kursiv.

```diff
-    comment: { color: col('slate'), italic: false }, // upright: the default comment is italic
+    comment: { color: col('phosphor'), italic: true }, // the strings' green italic
```

## Häufige Fehler

- **Ein Seitenkasten beginnt auf Höhe des Blocks nach seiner Einfassung.** In postext 1.4.1 steht ein Kasten mit span: 'side' in der Seitenspalte auf der Höhe, die der Text an seiner Einfassung erreicht hat, auf der nächsten Rasterlinie und unter einem bereits vorhandenen Kasten. Setzen Sie eine Glosse direkt vor den Absatz, den sie erläutert: Steht sie danach, beginnt sie neben dem nächsten Absatz. Ein Kasten, der über den Fuß der Spalte hinauslaufen würde, rutscht nach oben, bis sein Fuß auf dem Fuß der Spalte sitzt, soweit der Kasten darüber es zulässt; passt er dann immer noch nicht, wartet er auf die Seitenspalte der nächsten Seite.

- Halten Sie jede Codezeile bei höchstens 73 Zeichen. Eine längere bricht an ihrem letzten passenden Leerzeichen oder Satzzeichen um und läuft hinter einem `»` darunter weiter, und die Engine meldet eine Warnung `codeOverflow`; `overflow: 'shrink'` setzt stattdessen das ganze Listing kleiner, und `'clip'` schneidet die Zeile an der Kante des Kastens ab.
- Ein Tabulator in einem Listing springt zum nächsten Vielfachen von vier Spalten (`codeStyle.tabSize`). Ein mit Tabulatoren eingerücktes Skript, das Ihr Editor acht Spalten breit zeigt, fällt hier schmaler aus.
- Ist `codeStyle.inline` gesetzt, bricht Inline-Code wie ein Chip nie über Zeilen um. Ein langer Pfad im Fließtext wie `~/Pictures/2025/IMG_0977.MOV` rückt ganz in die nächste Zeile und kann davor eine zu lockere Zeile hinterlassen.

## Nachweise

- Rezept: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Schrift: Charis SIL (OFL-1.1), Sora (OFL-1.1), JetBrains Mono (OFL-1.1)
- Code: MIT · Beispielinhalt: CC-BY-4.0

## Verwandte Rezepte

- [Nº 050 · Bedienungsanleitung mit Sicherheitshinweisen](https://postext.dev/de/cookbook/product-manual-warnings.md): Eine deutsche Wasserkocher-Anleitung, deren Kästen WARNUNG und VORSICHT das Warndreieck auf einem Band in Signalfarbe tragen, mit deutschen Beschriftungen. · Stufe 2 (Mittel) · Handbücher und Nachschlagewerke
- [Nº 008 · Eine farbcodierte Familie von Lehrbuchkästen](https://postext.dev/de/cookbook/textbook-box-family.md): Sechs Arten von Lehrbuchkästen in drei Farben, unterschieden durch Streifen mit Icon, Badge, nummerierten Reiter, Piktogrammleiste oder Marke neben dem Rahmen. · Stufe 2 (Mittel) · Lehrbücher
- [Nº 022 · Arbeitsblatt mit Antwortkästen und Wortspeicher](https://postext.dev/de/cookbook/worksheet-answer-boxes.md): Ein vierseitiges Sachkunde-Arbeitsblatt: weiße Antwortkästen in hellgrünen Karten, 2 mm unter jeder Frage, ohne Raster, mit Wortspeichern und Lücken aus Chips. · Stufe 2 (Mittel) · Arbeitshefte und Übungen
