# Tang-Gedichte gegenüber ihren englischen Versen

> Jedes Tang-Gedicht eröffnet eine getönte rechte Seite, Giles’ Englisch die linke gegenüber: Ein Format bricht auf gerade Seiten um, das andere auf ungerade.

- HTML-Fassung: https://postext.dev/de/cookbook/tang-poems-facing-english
- Rezept Nº 083 · Seite und Raster · Stufe 2 (Mittel) · Ausgaben: Canvas, PDF
- Gattungen: Lyrik
- Benötigt postext ≥ 1.28.0, postext-pdf ≥ 1.28.0 · getestet mit 1.31.0, postext-pdf 1.31.0 am 2026-10-13
- Seiten: [1](https://postext.dev/cookbook/tang-poems-facing-english/en/p01.webp?v=e0655ce7), [2](https://postext.dev/cookbook/tang-poems-facing-english/en/p02.webp?v=e0655ce7), [3](https://postext.dev/cookbook/tang-poems-facing-english/en/p03.webp?v=e0655ce7), [4](https://postext.dev/cookbook/tang-poems-facing-english/en/p04.webp?v=e0655ce7), [5](https://postext.dev/cookbook/tang-poems-facing-english/en/p05.webp?v=e0655ce7), [6](https://postext.dev/cookbook/tang-poems-facing-english/en/p06.webp?v=e0655ce7), [7](https://postext.dev/cookbook/tang-poems-facing-english/en/p07.webp?v=e0655ce7), [8](https://postext.dev/cookbook/tang-poems-facing-english/en/p08.webp?v=e0655ce7), [9](https://postext.dev/cookbook/tang-poems-facing-english/en/p09.webp?v=e0655ce7), [10](https://postext.dev/cookbook/tang-poems-facing-english/en/p10.webp?v=e0655ce7)
- PDF: https://postext.dev/cookbook/tang-poems-facing-english/en/tang-poems-facing-english.pdf?v=e0655ce7
- In der Sandbox öffnen: https://postext.dev/de/sandbox#recipe=tang-poems-facing-english&lang=en (.postext: https://postext.dev/cookbook/tang-poems-facing-english/en/tang-poems-facing-english.postext)
- Zuletzt aktualisiert: 2026-10-11
- Andere Sprachen: [en](https://postext.dev/en/cookbook/tang-poems-facing-english.md), [es](https://postext.dev/es/cookbook/tang-poems-facing-english.md), [ca](https://postext.dev/ca/cookbook/tang-poems-facing-english.md), [fr](https://postext.dev/fr/cookbook/tang-poems-facing-english.md), [it](https://postext.dev/it/cookbook/tang-poems-facing-english.md), [pt](https://postext.dev/pt/cookbook/tang-poems-facing-english.md), [zh](https://postext.dev/zh/cookbook/tang-poems-facing-english.md), [ja](https://postext.dev/ja/cookbook/tang-poems-facing-english.md), [ar](https://postext.dev/ar/cookbook/tang-poems-facing-english.md)

## Kurz gesagt

Ein kleines Buch mit chinesischen Gedichten aus der Tang-Dynastie und einer alten englischen Übersetzung auf der gegenüberliegenden Seite. Jede Reihe chinesischer Verse steht auf gleicher Höhe mit den englischen Zeilen, die sie übersetzen.

## Was Sie setzen

Ein kleiner zweisprachiger Band mit Tang-Gedichten, gesetzt, wie Paralleltextausgaben einen Klassiker setzen: das Chinesische auf der rechten Seite, auf einem Blatt, das wie altes Papier getönt ist, und Herbert A. Giles’ gereimtes Englisch von 1901 auf der linken Seite gegenüber. Vier Gedichte von Li Bai, Du Fu und Wang Wei nehmen je eine Doppelseite ein, betitelt in jeder Sprache und mit dem Namen des Dichters gezeichnet. Das Chinesische steht in der LXGW WenKai, einer Kai nach dem Pinsel gezeichnet, zwei Verszeilen pro Reihe, sodass jede Reihe auf gleicher Höhe mit den zwei englischen Zeilen steht, die sie übersetzen. Glossen in der Sprache des Lesers stehen am Fuß der chinesischen Seite. [Übersetzung gegenüber, Strophe für Strophe](https://postext.dev/de/cookbook/bilingual-facing-verse.md) setzt ein spanisches Gedicht neben sein Englisch auf einer einzigen Seite; hier hat jede Sprache eine eigene Seite.

**Dieses Rezept beantwortet:**

- Wie setze ich ein Gedicht und seine Übersetzung auf gegenüberliegende Seiten, das Original immer rechts?
- Wie setze ich Lyrik: eine Zeile pro Vers, Abstand zwischen Strophen, hängende Einzüge für umbrochene Zeilen, keine Silbentrennung?
- Wie mache ich Fußnoten?
- Wie erzwinge ich einen Seiten- oder Spaltenumbruch und lasse jedes Kapitel auf einer rechten Seite beginnen?

## Die kurze Antwort

```js
// script.js, Zeilen 39–63
const SINK = 6; // lines of LEAD under both titles, so the two poems start on the same line
const at = (y) => ({ anchor: { to: 'container', edge: 'top' }, offset: { y: mm(y) } });
// A title whose foot is 23 mm down at any size, and the poet's line under it at 24 mm.
const titled = (size, title, poet, under = []) => ({
  enabled: true, minHeight: pt(SINK * LEAD),
  slot: { elements: [...under,
    { kind: 'text', id: 'title', content: '{titleText}', fontSize: pt(size), lineHeight: 1.2,
      align: 'center', overflow: 'wrap', color: col('ink'),
      placement: at(23 - (size * 1.2 * 25.4) / 72), ...title },
    { kind: 'text', id: 'poet', align: 'center', overflow: 'wrap', color: col('seal'),
      placement: at(24), ...poet },
  ] },
});
// Each style breaks to its own side of the spread (gotcha: style-inherits-break).
const english = { id: 'en', breakBefore: { enabled: true, parity: 'even' }, // a verso
  advancedDesign: titled(19, { fontFamily: ROMAN, italic: true },
    { content: '{attr.kicker}', fontFamily: LABEL, fontSize: pt(7.5), fontWeight: 600,
      letterSpacing: pt(1.5), textTransform: 'uppercase' }) };
// The original on a tinted leaf: span 'page' lets the tint reach past the text column.
const chinese = { id: 'zh', span: 'page', breakBefore: { enabled: true, parity: 'odd' },
  advancedDesign: titled(28, { fontFamily: SERIF, fontWeight: 700, letterSpacing: pt(7) },
    { content: '{attr.poet}', fontFamily: HEI, fontSize: pt(9), letterSpacing: pt(1.5) },
    [{ kind: 'box', id: 'leaf', reserve: false, style: { backgroundColor: col('leaf') },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: 'fill' } } }]) };
```

## Zutaten

**Zeigt**

- [Kapitel, die rechts beginnen](https://postext.dev/de/docs/configuration-text.md#umbruch-davor): Eine Überschriftenebene beginnt eine neue Seite: die nächste, die nächste rechte oder die nächste linke Seite, bei Bedarf mit einer eingefügten Vakatseite.
- [Überschriftenformate](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile): Benannte Varianten einer Überschriftenebene, gewählt mit {style="…"}: ein anderer Auftakt, andere Typografie, Kolumnentitel oder Seiteneinrichtung je Kapitel.
- [Fußnoten](https://postext.dev/de/docs/document-format.md#fußnoten): Mit [^id] zitierte Anmerkungen, gesetzt am Fuß der Spalte, die sie zitiert, oder nach dem Kapitel, nummeriert je Kapitel oder durch das ganze Buch.

**Verwendet außerdem**

- [Verse Zeile für Zeile](https://postext.dev/de/docs/document-format.md#verse)
- [Absatzformate](https://postext.dev/de/docs/configuration-styles.md#absatzformate)
- [Zeichenraster](https://postext.dev/de/docs/configuration-east-asian.md#zeichenraster)
- [Chinesische, japanische und koreanische Schriften](https://postext.dev/de/docs/configuration-programmatic-usage.md#chinesische-japanische-und-koreanische-schriften)
- [Chinesischer Zeilenumbruch](https://postext.dev/de/docs/configuration-east-asian.md#ostasiatische-typografie)
- [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)
- [Text, Linien und Kästen in Seitengestaltungen](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen)
- [Umschläge, Titelseiten und Impressum](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Kolumnentitel und Seitenzahlen](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen)
- [Kolumnentitel je Abschnitt](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile)
- [Gespiegelte Ränder](https://postext.dev/de/docs/configuration-page-layout.md#gespiegelte-ränder)
- [Papierfarbe](https://postext.dev/de/docs/configuration-page-layout.md#seite)
- [Semantische Farbpalette](https://postext.dev/de/docs/configuration-fonts-colors-viewers.md#farbpalette)
- [Expliziter vertikaler Abstand](https://postext.dev/de/docs/document-format.md#space)
- [PDF-Export](https://postext.dev/de/docs/configuration-programmatic-usage.md#pdfs-erzeugen)
- [Im PDF eingebettete Schriften](https://postext.dev/de/docs/configuration-programmatic-usage.md#warum-ein-font-provider)
- [Kopfzeilen nach Seitenrolle](https://postext.dev/de/docs/configuration-page-layout.md#textelemente)

**Die Konfiguration auf einen Blick**

- [`bodyText`](https://postext.dev/de/docs/configuration-text.md#fließtext), [`cjk`](https://postext.dev/de/docs/configuration-east-asian.md#ostasiatische-typografie), [`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), `footnotes`, [`header`](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen), [`headingStyles`](https://postext.dev/de/docs/configuration-styles.md#überschriftenstile), [`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), [`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), [`decompressWoff2`](https://postext.dev/de/docs/configuration-programmatic-usage.md#font-provider-im-browser-fontsource--woff2), [`renderPageToCanvas`](https://postext.dev/de/docs/configuration-programmatic-usage.md#eine-seite-als-bitmap-rendern), [`renderToPdf`](https://postext.dev/de/docs/configuration-programmatic-usage.md#pdfs-erzeugen), [`withLoadedFonts`](https://postext.dev/de/docs/configuration-programmatic-usage.md#schriften-vor-dem-layout-laden)

**Schriften**

- Noto Serif TC (OFL-1.1), LXGW WenKai TC (OFL-1.1), Noto Sans TC (OFL-1.1), Source Serif 4 (OFL-1.1), Source Sans 3 (OFL-1.1)

## Zubereitung

### 1 · Ein Überschriftenformat für jede Seite der Doppelseite

Der Code steht oben in [der kurzen Antwort](https://postext.dev/de/cookbook/tang-poems-facing-english.md#die-kurze-antwort). Jedes Gedicht besteht aus zwei Überschriften der Ebene 1: `# Night Thoughts {style="en" …}` mit Giles’ Versen, dann `# 靜夜思 {style="zh" …}` mit dem Gedicht. Das Format `en` bricht auf die nächste gerade Seite um, eine linke; das Format `zh` auf die nächste ungerade, die rechte Seite gegenüber. Solange jedes Gedicht auf seine Seite passt, kommt keine leere Seite hinzu; eine englische Fassung, die überliefe, schöbe ihr Original auf die folgende rechte Seite, hinter eine leere linke ([Umbruch davor](https://postext.dev/de/docs/configuration-text.md#umbruch-davor)). Beide Gestaltungen reservieren sechs Rasterzeilen, sodass Englisch und Chinesisch auf derselben Zeile beginnen. Das chinesische Format hat `span: 'page'`, weil sein erstes Element, ein Kasten in Seitengröße, das getönte Blatt ist, und eine in der Spalte gehaltene Gestaltung am Fuß der Spalte endet ([Seitenbreite und erweiterte Gestaltung](https://postext.dev/de/docs/configuration-text.md#seitenbreite-und-erweiterte-gestaltung)).

![Opening page 2: Night Thoughts.](https://postext.dev/cookbook/tang-poems-facing-english/en/p02.webp?v=e0655ce7)

*Seite 2: Das Englische beginnt auf der linken Seite sechs Zeilen tiefer, auf der Rasterzeile, auf der das Chinesische auf Seite 3 beginnt.*

### 2 · Eine Reihe Chinesisch für zwei Zeilen Englisch

```js
// script.js, Zeilen 67–76
// Each English poem is a :::verse block: a line of verse a line of Markdown, Giles's indents
// as two leading spaces (two ems: bodyText.verse.indentStep), a blank line between stanzas.
// The block is centred on its longest line in the 103.7 mm measure, so a short poem and a
// long one both sit in the middle.
const verse = { id: 'verse', fontFamily: ROMAN, fontSize: pt(BODY), lineHeight: pt(LEAD) };
const shi = { id: 'shi', fontFamily: KAI, fontSize: pt(SHI), lineHeight: pt(2 * LEAD),
  textAlign: 'left', firstLineIndent: pt(0), // two lines of verse to a row: 2 × LEAD
  // Five characters and a full-width mark, twice: 12 ems, centred in the 28 of the grid.
  // A fixed left edge, and the note numbers after each row's 。: every character keeps its column.
  indent: pt((CHARS * BODY - 12 * SHI) / 2) };
```

Die chinesischen Reihen haben den doppelten Zeilenabstand der englischen Zeilen, 32 pt gegen 16, sodass eine Reihe aus zwei fünfzeichigen Zeilen dem Verspaar gegenübersteht, das Giles daraus macht. Im Mondscheingedicht auf [den Seiten 4 und 5](https://postext.dev/cookbook/tang-poems-facing-english/en/p04.webp?v=e0655ce7) lassen eine Leerzeile zwischen den englischen Strophen und ein `:::space{lines=1}` zwischen den chinesischen je eine Zeile frei, und seine drei Strophen bleiben über die Doppelseite auf gleicher Höhe. Die chinesische Reihe beginnt 51 pt eingerückt, was ihre zwölf Geviert in den 28 des Rasters zentriert, und jedes Fußnotenzeichen steht nach dem 。 seiner Reihe, außerhalb der zwölf: Innerhalb der Reihe würde es jedes folgende Zeichen um sechs Zehntel Geviert nach rechts schieben, aus der Flucht mit der Reihe darunter. Jedes englische Gedicht ist ein Block `:::verse`, auf seine längste Zeile zentriert, sodass kein Einzug gemessen werden muss. Giles zieht jede zweite Zeile ein; das Markdown behält seine zwei Leerzeichen, die `bodyText.verse.indentStep` zwei Geviert tief setzt.

### 3 · Glossen am Fuß der chinesischen Seite

```js
// script.js, Zeilen 80–84
const footnotes = { placement: 'column', numbering: 'page', // the page foot; ① again on each page
  numberFormat: 'circled-decimal', markerSize: em(0.6), fontSize: pt(8), lineHeight: pt(11),
  color: col('ink'), textAlign: 'left', spaceAbove: pt(LEAD), spaceBelowRule: pt(5),
  spaceBetween: pt(3), separator: { width: 0.12, lineWidth: pt(0.6), color: col('seal') },
  wordBreak: 'keep-all' }; // a reading a note quotes (山月, 看月光) is never broken
```

Die Glossen sind Fußnoten, eine pro Reihe: `疑是地上霜。[^bed]` schließt die erste Reihe des Gedichts, und `[^bed]: …` folgt ihr und nennt jedes erklärte Wort fett, da die Ziffer nicht mehr neben ihm steht. Sie sind nummeriert, wie ein chinesisches Buch seine Anmerkungen nummeriert (页下注): eingekreiste Ziffern auf der Grundlinie, auf jeder Seite wieder ab ① gezählt (`numberFormat: 'circled-decimal'`, `numbering: 'page'`). Im Vers steht das Zeichen in sechs Zehnteln der Gedichtgröße, `markerSize: em(0.6)`, nahe am Grad der Fußnoten; am Fuß der Seite beginnt jede Fußnote mit ihrem ① im Grad des Fußnotentextes ([Fußnoten](https://postext.dev/de/docs/configuration-notes-references.md#fußnoten)). Fußnoten stehen in der Grundschrift, und die Grundschrift dieses Buches ist die Noto Serif TC, weil eine Glosse Schriftzeichen und Pinyin mischt; die lateinischen Buchstaben der Noto Serif TC stammen aus der Source Serif, deshalb sind die englischen Seiten in der Source Serif 4 gesetzt. Die Fußnoten sind in der Sprache der Ausgabe geschrieben: Mit dem Kolumnentitel der linken Seite und der Anmerkung zum Text sind sie das, was die spanische Ausgabe ändert.

### 4 · Kolumnentitel in der Sprache jeder Seite

```js
// script.js, Zeilen 117–128
const head = (id, content, parity, edge, x, style) => ({ kind: 'text', id, content, parity,
  pages: 'opener', // every poem page opens with its title; a blank page stays bare
  fontFamily: LABEL, fontSize: pt(7.5), fontWeight: 600, color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...style });
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', EDGE, { color: col('ink') }),
  head('verso-book', '{subtitle}', 'even', 'top-left', EDGE + 8,
    { letterSpacing: pt(1.2), textTransform: 'uppercase' }),
  head('recto-poet', '{attr.name}', 'odd', 'top-right', -(EDGE + 8),
    { fontFamily: HEI, fontWeight: 400, fontSize: pt(8), letterSpacing: pt(3) }),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -EDGE, { color: col('ink') }),
] };
```

Jede Seite dieses Buches beginnt mit einer Überschrift, jede Seite ist also eine Eröffnungsseite, und die Kolumnentitel verwenden `pages: 'opener'`. Eine durch einen Umbruch eingefügte leere Seite bliebe leer, und das Titelblatt räumt sie mit einer eigenen leeren Kopfzeile ab. Die linke Seite nimmt den Buchtitel aus `subtitle` im Frontmatter, das jede Ausgabe in ihrer Sprache schreibt; die rechte nimmt den Namen des Dichters aus dem Attribut `name` der chinesischen Überschrift ([Kopf- und Fußzeilen](https://postext.dev/de/docs/configuration-page-layout.md#kopf--und-fußzeilen)).

### 5 · Ein Titelblatt auf einem Mond

```js
// script.js, Zeilen 88–113
const onPage = (y, size) => ({ anchor: { to: 'page', edge: 'top' }, offset: { y: mm(y) },
  ...(size ? { size: { width: mm(size), height: mm(size) } } : {}) });
const [MOON_Y, MOON, HAN] = [36, 78, 60]; // mm: the moon's top and width; pt: the title
const UNDER = MOON_Y + MOON; // mm: the English lines start under the moon
const line = (id, content, y, style) => ({ kind: 'text', id, content, align: 'center',
  overflow: 'wrap', color: col('ink'), placement: onPage(y), ...style });
const titlePage = { id: 'title', span: 'page', runningChapter: false,
  header: { elements: [] }, footer: { elements: [] },
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'box', id: 'moon', placement: onPage(MOON_Y, MOON),
      style: { backgroundColor: col('moon'), borderRadius: mm(MOON / 2) } },
    line('han', '{titleText}', MOON_Y + MOON / 2 - (HAN * 25.4) / 144, { fontFamily: SERIF,
      fontWeight: 900, fontSize: pt(HAN), letterSpacing: pt(HAN / 4), lineHeight: 1 }),
    line('en', '{attr.en}', UNDER + 10, { fontFamily: LABEL, fontSize: pt(10), fontWeight: 600,
      letterSpacing: pt(3), textTransform: 'uppercase', color: col('seal') }),
    line('poets', '{attr.poets}', UNDER + 19, { fontFamily: ROMAN, fontSize: pt(12) }),
    line('giles', '{attr.english}', UNDER + 26, { fontFamily: ROMAN, fontSize: pt(11),
      italic: true }),
    // A seal reading 明月, the bright moon of three of the four poems, set down its square.
    { kind: 'box', id: 'seal', placement: onPage(170, 13),
      style: { backgroundColor: col('seal'), borderRadius: mm(1) } },
    line('seal-text', '明月', 170, { placement: onPage(170, 13), writingMode: 'vertical-rl',
      fontFamily: KAI, fontSize: pt(14), lineHeight: 1, color: col('paper'),
      verticalAlign: 'middle' }),
  ] } },
};
```

Auch das Titelblatt ist ein Überschriftenformat, mit `runningChapter: false`, damit es kein Kapitel nennt. Der Mond ist ein Gestaltungskasten, dessen Eckenradius die Hälfte seiner Seitenlänge ist, und 唐詩 wird rechnerisch darauf zentriert: die Mitte des Mondes abzüglich der halben Höhe des Titels. Das Siegel setzt 明月 mit `writingMode: 'vertical-rl'`, ein Zeichen über dem anderen, wie Siegel geschnitten werden.

### 6 · Jede chinesische Schrift lädt, was sie setzt

```js
// script.js, Zeilen 306–316
const grab = (re) => (markdown.match(re) ?? []).join('');
// Each poem to its closing `:::` (not the `:::space` between stanzas), then the note numbers.
const poems = grab(/:::paragraphs\{style="shi"\}[\s\S]*?\n:::(?=\n|$)/g) + '①②③④⑤⑥⑦⑧⑨⑩';
await loadFonts(FONTS, markdown);
await Promise.all([
  loadCjkFonts({ [SERIF]: ['400'] }, markdown + poems), // the notes and their ①, the heading blocks
  loadCjkFonts({ [SERIF]: ['700'] }, grab(/^# \S+(?= \{style="zh")|\*\*[^*]+\*\*/gm)),
  loadCjkFonts({ [SERIF]: ['900'] }, grab(/^# \S+(?= \{style="title")/gm)),
  loadCjkFonts({ [KAI]: ['400'] }, `${poems}明月`),
  loadCjkFonts({ [HEI]: ['400'] }, grab(/(?<=(?:poet|name)=")[^"]*/g)),
]);
```

Fontsource zerlegt jede chinesische Schrift nach Zeichenbereichen in etwa hundert Dateien, und `loadCjkFonts` holt nur die Dateien, die ihr Text braucht ([Chinesische, japanische und koreanische Schriften](https://postext.dev/de/docs/configuration-programmatic-usage.md#chinesische-japanische-und-koreanische-schriften)). Die Kai bekommt die Gedichte und das Siegel, die Hei die Dichterzeilen und Namen, die fette Song die vier Titel und die Stichwörter der Fußnoten und ihr schwarzer Schnitt die zwei Zeichen des Titelblatts.

## 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/tang-poems-facing-english

### script.js

```js
// ═══ Postext Cookbook · Nº 083 · Tang poems facing their English verse ═══════════════
// https://postext.dev/en/cookbook/tang-poems-facing-english
// Code: MIT · Text: Tang poems, zh.wikisource; H. A. Giles, 1901, Project Gutenberg (PD)
// Fonts: Noto Serif TC, LXGW WenKai TC, Noto Sans TC, Source Serif 4 (OFL) · Needs postext ≥ 1.28.0
import { buildDocument, withLoadedFonts, renderPageToCanvas } from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'tang-poems-facing-english';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// Ink and seal red on a cream paper, and a pale moon for the title page.
const palette = {
  ink: '#1f1b18', // text: a warm near-black
  seal: '#ad3526', // the accent: vermilion seal paste, for the poets, the rules and the seal
  moon: '#eadcb4', // the title page's moon
  leaf: '#f2e8cf', // the recto's tint, under the Chinese
  muted: '#6d655c', // running heads and the colophon
  paper: '#faf6ed', // a cream paper
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'seal (defaults)', value: { hex: palette.seal, model: 'hex' } },
];
const SERIF = 'Noto Serif TC'; // 明: the Chinese titles and the notes
const KAI = 'LXGW WenKai TC'; // 楷: the poems
const HEI = 'Noto Sans TC'; // 黑: the poet lines and the recto's running head
const ROMAN = 'Source Serif 4'; // the English: the Latin that Noto Serif TC is built on
const LABEL = 'Source Sans 3'; // English labels and folios
const [BODY, LEAD, SHI] = [10.5, 16, 16]; // pt: 五号 on a 16 pt pitch; the poems' size
const [CHARS, LINES] = [28, 30]; // the character grid: 28 ems by 30 lines
const TRIM = { width: 148, height: 210 }; // mm: 25開, a Taiwan trade trim
const [INNER, OUTER] = [18, 21]; // mm: minimum sides; the grid centres its measure between them
const MEASURE = (CHARS * BODY * 25.4) / 72; // 103.7 mm: 28 ems of 五号
const EDGE = OUTER + (TRIM.width - INNER - OUTER - MEASURE) / 2; // 23.6 mm: the outer text edge

// #region answer: the English opens the next verso, the Chinese the recto that faces it
const SINK = 6; // lines of LEAD under both titles, so the two poems start on the same line
const at = (y) => ({ anchor: { to: 'container', edge: 'top' }, offset: { y: mm(y) } });
// A title whose foot is 23 mm down at any size, and the poet's line under it at 24 mm.
const titled = (size, title, poet, under = []) => ({
  enabled: true, minHeight: pt(SINK * LEAD),
  slot: { elements: [...under,
    { kind: 'text', id: 'title', content: '{titleText}', fontSize: pt(size), lineHeight: 1.2,
      align: 'center', overflow: 'wrap', color: col('ink'),
      placement: at(23 - (size * 1.2 * 25.4) / 72), ...title },
    { kind: 'text', id: 'poet', align: 'center', overflow: 'wrap', color: col('seal'),
      placement: at(24), ...poet },
  ] },
});
// Each style breaks to its own side of the spread (gotcha: style-inherits-break).
const english = { id: 'en', breakBefore: { enabled: true, parity: 'even' }, // a verso
  advancedDesign: titled(19, { fontFamily: ROMAN, italic: true },
    { content: '{attr.kicker}', fontFamily: LABEL, fontSize: pt(7.5), fontWeight: 600,
      letterSpacing: pt(1.5), textTransform: 'uppercase' }) };
// The original on a tinted leaf: span 'page' lets the tint reach past the text column.
const chinese = { id: 'zh', span: 'page', breakBefore: { enabled: true, parity: 'odd' },
  advancedDesign: titled(28, { fontFamily: SERIF, fontWeight: 700, letterSpacing: pt(7) },
    { content: '{attr.poet}', fontFamily: HEI, fontSize: pt(9), letterSpacing: pt(1.5) },
    [{ kind: 'box', id: 'leaf', reserve: false, style: { backgroundColor: col('leaf') },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: 'fill' } } }]) };
// #endregion

// #region verse: a Chinese row faces two lines of English on the grid both pages share
// Each English poem is a :::verse block: a line of verse a line of Markdown, Giles's indents
// as two leading spaces (two ems: bodyText.verse.indentStep), a blank line between stanzas.
// The block is centred on its longest line in the 103.7 mm measure, so a short poem and a
// long one both sit in the middle.
const verse = { id: 'verse', fontFamily: ROMAN, fontSize: pt(BODY), lineHeight: pt(LEAD) };
const shi = { id: 'shi', fontFamily: KAI, fontSize: pt(SHI), lineHeight: pt(2 * LEAD),
  textAlign: 'left', firstLineIndent: pt(0), // two lines of verse to a row: 2 × LEAD
  // Five characters and a full-width mark, twice: 12 ems, centred in the 28 of the grid.
  // A fixed left edge, and the note numbers after each row's 。: every character keeps its column.
  indent: pt((CHARS * BODY - 12 * SHI) / 2) };
// #endregion

// #region notes: glosses at the foot of the recto, in the edition's language
const footnotes = { placement: 'column', numbering: 'page', // the page foot; ① again on each page
  numberFormat: 'circled-decimal', markerSize: em(0.6), fontSize: pt(8), lineHeight: pt(11),
  color: col('ink'), textAlign: 'left', spaceAbove: pt(LEAD), spaceBelowRule: pt(5),
  spaceBetween: pt(3), separator: { width: 0.12, lineWidth: pt(0.6), color: col('seal') },
  wordBreak: 'keep-all' }; // a reading a note quotes (山月, 看月光) is never broken
// #endregion

// #region title: 唐詩 on a pale moon, with a seal
const onPage = (y, size) => ({ anchor: { to: 'page', edge: 'top' }, offset: { y: mm(y) },
  ...(size ? { size: { width: mm(size), height: mm(size) } } : {}) });
const [MOON_Y, MOON, HAN] = [36, 78, 60]; // mm: the moon's top and width; pt: the title
const UNDER = MOON_Y + MOON; // mm: the English lines start under the moon
const line = (id, content, y, style) => ({ kind: 'text', id, content, align: 'center',
  overflow: 'wrap', color: col('ink'), placement: onPage(y), ...style });
const titlePage = { id: 'title', span: 'page', runningChapter: false,
  header: { elements: [] }, footer: { elements: [] },
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'box', id: 'moon', placement: onPage(MOON_Y, MOON),
      style: { backgroundColor: col('moon'), borderRadius: mm(MOON / 2) } },
    line('han', '{titleText}', MOON_Y + MOON / 2 - (HAN * 25.4) / 144, { fontFamily: SERIF,
      fontWeight: 900, fontSize: pt(HAN), letterSpacing: pt(HAN / 4), lineHeight: 1 }),
    line('en', '{attr.en}', UNDER + 10, { fontFamily: LABEL, fontSize: pt(10), fontWeight: 600,
      letterSpacing: pt(3), textTransform: 'uppercase', color: col('seal') }),
    line('poets', '{attr.poets}', UNDER + 19, { fontFamily: ROMAN, fontSize: pt(12) }),
    line('giles', '{attr.english}', UNDER + 26, { fontFamily: ROMAN, fontSize: pt(11),
      italic: true }),
    // A seal reading 明月, the bright moon of three of the four poems, set down its square.
    { kind: 'box', id: 'seal', placement: onPage(170, 13),
      style: { backgroundColor: col('seal'), borderRadius: mm(1) } },
    line('seal-text', '明月', 170, { placement: onPage(170, 13), writingMode: 'vertical-rl',
      fontFamily: KAI, fontSize: pt(14), lineHeight: 1, color: col('paper'),
      verticalAlign: 'middle' }),
  ] } },
};
// #endregion

// #region heads: the book in English over the verso, the poet in Chinese over the recto
const head = (id, content, parity, edge, x, style) => ({ kind: 'text', id, content, parity,
  pages: 'opener', // every poem page opens with its title; a blank page stays bare
  fontFamily: LABEL, fontSize: pt(7.5), fontWeight: 600, color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(12) } }, ...style });
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', EDGE, { color: col('ink') }),
  head('verso-book', '{subtitle}', 'even', 'top-left', EDGE + 8,
    { letterSpacing: pt(1.2), textTransform: 'uppercase' }),
  head('recto-poet', '{attr.name}', 'odd', 'top-right', -(EDGE + 8),
    { fontFamily: HEI, fontWeight: 400, fontSize: pt(8), letterSpacing: pt(3) }),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -EDGE, { color: col('ink') }),
] };
// #endregion

const paragraphStyles = [shi, verse,
  // Justified like the body, and divided by the patterns of the edition's language (bodyText).
  { id: 'prose', fontFamily: ROMAN, fontSize: pt(10), lineHeight: pt(LEAD),
    firstLineIndent: mm(4) },
  { id: 'colophon', fontFamily: LABEL, fontSize: pt(7), lineHeight: pt(10), color: col('muted'),
    italicColor: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
];

const config = () => ({
  locale: 'zh-Hant', // the Chinese sets the rules; written out, never LANG (gotcha: cjk-locale-tag)
  colorPalette,
  page: {
    width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'),
    // 23 + 17.5 mm leave 169.5 mm for 30 lines of 16 pt (169.3 mm): a head deeper than the foot.
    margins: { top: mm(23), bottom: mm(17.5), left: mm(INNER), right: mm(OUTER), mirror: true },
  },
  layout: { layoutType: 'single' },
  cjk: { grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES } },
  bodyText: { fontFamily: SERIF, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: em(2), indentAfterHeading: false,
    hyphenation: { enabled: true, locale: LANG }, // no Chinese patterns: the Latin's language
    verse: { indentStep: em(1) } }, // Giles's two-space indents: two ems
  // The designs paint the titles; weight 400 keeps the heading blocks in a loaded face.
  // parity 'any': a level-1 heading takes the next page; the styles below choose the side.
  headings: { fontFamily: SERIF, fontWeight: 400, levels: [{ level: 1, marginTop: pt(0),
    marginBottom: pt(0), breakBefore: { enabled: true, parity: 'any' } }] },
  headingStyles: [titlePage, english, chinese],
  paragraphStyles, footnotes, header, footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "唐詩"
subtitle: "Tang Poems"
---

# 唐詩 {style="title" en="Tang Poems" poets="Li Bai · Du Fu · Wang Wei" english="with the English verse of Herbert A. Giles"}

# Night Thoughts {style="en" kicker="Li Bai · 701–762"}

:::verse{style="verse"}
I wake, and moonbeams play around my bed,
  Glittering like hoar-frost to my wondering eyes;
Up towards the glorious moon I raise my head,
  Then lay me down,—and thoughts of home arise.
:::

# 靜夜思 {style="zh" poet="〔唐〕李白" name="李白"}

:::paragraphs{style="shi"}
牀前明月光，疑是地上霜。[^bed]

舉頭望明月，低頭思故鄉。[^moon]
:::

[^bed]: **牀** chuáng, now written 床: a bed. Some commentators read it as the railing round a well, others as a folding seat set out in the courtyard; Giles keeps the bed.

[^moon]: **明月** míngyuè, the bright moon. The collected works of Li Bai read 山月 here, the moon over the hills, and 看月光 in the first line; the 1705 edition of all Tang poetry reads 山月 but keeps 明月光. This page follows the anthology of 1763, the text Chinese children learn by heart. **鄉** xiāng rhymes with 光 guāng and 霜 shuāng at the ends of the first two lines; Giles rhymes his quatrain on alternate lines instead.

# Drinking Alone under the Moon {style="en" kicker="Li Bai · 701–762"}

:::verse{style="verse"}
An arbour of flowers and a kettle of wine:
Alas! in the bowers no companion is mine.
Then the moon sheds her rays on my goblet and me,
And my shadow betrays we’re a party of three.

Though the moon cannot swallow her share of the grog,
And my shadow must follow wherever I jog,—
Yet their friendship I’ll borrow and gaily carouse,
And laugh away sorrow while spring-time allows.

See the moon,—how she glances response to my song;
See my shadow,—it dances so lightly along!
While sober I feel you are both my good friends;
When drunken I reel, our companionship ends.
But we’ll soon have a greeting without a good-bye,
At our next merry meeting away in the sky.
:::

# 月下獨酌 {style="zh" poet="〔唐〕李白" name="李白"}

:::paragraphs{style="shi"}
花間一壺酒，獨酌無相親。

舉杯邀明月，對影成三人。[^three]

:::space{lines=1}

月既不解飲，影徒隨我身。

暫伴月將影，行樂須及春。[^jiang]

:::space{lines=1}

我歌月徘徊，我舞影零亂。

醒時同交歡，醉後各分散。

永結無情遊，相期邈雲漢。[^wuqing]
:::

[^three]: **三人** sān rén, three people: the poet, the moon and his shadow.

[^jiang]: **將** jiāng: here it joins 月 to 影, the moon to the shadow.

[^wuqing]: **無情遊** wúqíng yóu: a friendship with companions who feel nothing, the moon and the shadow; the poet binds himself to it for good. **雲漢** yúnhàn: the Milky Way, where the three will meet again, away in the sky. The rhyme changes at the ninth line, where the third stanza of Giles begins.

# Stop-short {style="en" kicker="Du Fu · 712–770"}

:::verse{style="verse"}
White gleam the gulls across the darkling tide,
  On the green hills the red flowers seem to burn;
Alas! I see another spring has died….
  When will it come—the day of my return?
:::

# 絕句 {style="zh" poet="〔唐〕杜甫" name="杜甫"}

:::paragraphs{style="shi"}
江碧鳥逾白，山青花欲燃。[^yu]

今春看又過，何日是歸年。[^guinian]
:::

[^yu]: **逾** yú, all the more: against the green of the river the birds look whiter still. **欲燃** yù rán, about to catch fire: the red of the flowers, which in Giles seem to burn.

[^guinian]: **歸年** guī nián, the year of going home. Du Fu wrote this quatrain, the second of a pair, in the spring of 764 at Chengdu, where the war in the north had driven him.

# The Bamboo Lodge {style="en" kicker="Wang Wei · 701–761"}

:::verse{style="verse"}
Beneath the bamboo grove, alone,
  I seize my lute and sit and croon;
No ear to hear me, save mine own:
  No eye to see me—save the moon.
:::

# 竹里館 {style="zh" poet="〔唐〕王維" name="王維"}

:::paragraphs{style="shi"}
獨坐幽篁裏，彈琴復長嘯。[^huang]

深林人不知，明月來相照。
:::

[^huang]: **幽篁** yōuhuáng: a deep, dark grove of bamboo. **長嘯** cháng xiào: a long whistle through pursed lips, the art of recluses; in Giles the poet croons. The Bamboo Lodge was one of twenty places on the estate of Wang Wei at Wangchuan, south-east of the Tang capital, and he gave each of them a quatrain in 輞川集, the Wangchuan collection.

# A note on the text {style="en" kicker="About this edition"}

:::paragraphs{style="prose"}
The Chinese poems follow *Three Hundred Tang Poems*, the anthology Sun Zhu compiled in 1763 under the name Hengtang Tuishi, except Du Fu’s quatrain, which is not in it and is printed as the *Complete Tang Poems* of 1705 gives it. All four are taken from Wikisource in traditional characters, with the old forms of some characters as found there.

The English verse is Herbert A. Giles’s, as he printed it in *A History of Chinese Literature* (1901), where the poems have no titles; the English titles are this edition’s. Giles wrote Li Po and Tu Fu for Li Bai and Du Fu, and called the four-line *jueju* a “stop-short”, which gives Du Fu’s poem its English name. His rhymes, indents and punctuation are kept; the moonlight poem, which his book printed in half-lines, is set here in whole lines.

Each Chinese poem is set two lines of verse to a row, so that every row faces the two lines of English that render it. The space between the stanzas of the moonlight poem is the editor’s, added on both pages.
:::

:::paragraphs{style="colophon"}
Set in Noto Serif TC, LXGW WenKai TC and Noto Sans TC for the Chinese, Source Serif 4 and Source Sans 3 for the English, all under the SIL Open Font License · Chinese text: Wikisource · English verse: H. A. Giles, *A History of Chinese Literature*, 1901, Project Gutenberg eBook 43711
:::
`; // content.<lang>.md, inlined by the Cookbook

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // every face the pages use, loaded before the build
  'Noto Serif TC': ['400', '700', '900'], // SERIF: the notes; the titles; 唐詩
  'LXGW WenKai TC': ['400'], // KAI: the poems and the seal
  'Noto Sans TC': ['400'], // HEI: the poet lines, the recto's head
  'Source Serif 4': ['400', '400i'], // ROMAN: the verse, the titles, the note on the text
  'Source Sans 3': ['400', '400i', '600'], // LABEL: kickers, the verso's head, folios, colophon
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// #region voices: each Chinese face loads the files of the characters it sets
const grab = (re) => (markdown.match(re) ?? []).join('');
// Each poem to its closing `:::` (not the `:::space` between stanzas), then the note numbers.
const poems = grab(/:::paragraphs\{style="shi"\}[\s\S]*?\n:::(?=\n|$)/g) + '①②③④⑤⑥⑦⑧⑨⑩';
await loadFonts(FONTS, markdown);
await Promise.all([
  loadCjkFonts({ [SERIF]: ['400'] }, markdown + poems), // the notes and their ①, the heading blocks
  loadCjkFonts({ [SERIF]: ['700'] }, grab(/^# \S+(?= \{style="zh")|\*\*[^*]+\*\*/gm)),
  loadCjkFonts({ [SERIF]: ['900'] }, grab(/^# \S+(?= \{style="title")/gm)),
  loadCjkFonts({ [KAI]: ['400'] }, `${poems}明月`),
  loadCjkFonts({ [HEI]: ['400'] }, grab(/(?<=(?:poet|name)=")[^"]*/g)),
]);
// #endregion
const doc = await withLoadedFonts(
  () => buildDocument({ markdown }, config()),
  { ...kitFonts(FONTS), text: markdown });
showPages(doc, { title: t({ en: 'Tang poems facing their English verse',
  es: 'Poemas Tang frente a su versión inglesa' }) });
offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);

// ─── 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 · pdf v3 ── the same in every recipe that exports a PDF
/** The Fontsource files the screen used, as TrueType, with their range and
 *  place: the nearest weight the family ships, upright with no italic;
 *  latin, then what its letters need (kitSubsetsFor). */
async function fontsourceProvider(family, weight, style, request) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  const weights = meta?.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style;
  const text = String.fromCodePoint(...(request?.codePoints ?? []));
  const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => {
    const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`);
    if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`);
    return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())),
      { unicodeRange: kitRanges()[subset], order });
  }));
  return files.length === 1 ? files[0] : files;
}

/** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame
 *  shows no PDFs) and a download link. */
function offerPdf(makePdf, filename) {
  viewer();
  const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' });
  button.dataset.postextPdf = filename;
  button.addEventListener('click', async () => {
    button.disabled = true;
    button.textContent = 'Building the PDF…';
    try {
      const bytes = await makePdf();
      const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }));
      const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`;
      button.replaceWith(
        Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }),
        Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` }));
    } catch (error) {
      button.disabled = false;
      button.textContent = 'Build the PDF';
      kitFail(error);
    }
  });
  document.getElementById('pt-actions').append(button);
}

// ─── Kit · cjk v2 ── Chinese, Japanese and Korean books · postext.dev/cookbook
// Fontsource ships a CJK family as about a hundred files per weight, each
// declared in its stylesheet with the unicode-range it covers. The screen
// loads the files the sample touches; the PDF gets the same files for the
// characters its pages set in each face, and embeds each as a subset.
// A book bound on the right (vertical text) is shown with its spreads
// mirrored: page 1 alone on the left of the spine, then [3 | 2].

/** The files of a Fontsource face, read from its stylesheet: { url, range,
 *  ranges }, the last declared first (the order the browser tries them in). */
function cjkSlices(family, weight, style) {
  cjkSlices.cache ??= new Map();
  const id = fontsourceId(family);
  const css = `${id}/${weight}${style === 'italic' ? '-italic' : ''}.css`;
  if (!cjkSlices.cache.has(css)) {
    let at;
    cjkSlices.cache.set(css, fontsourceAt(id).then((a) => fetch(at = a + css.slice(id.length + 1)))
      .then((res) => {
        if (!res.ok) throw new Error(`Fontsource has no ${family} ${weight} ${style} (${res.status})`);
        return res.text();
      })
      .then((text) => [...text.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => {
        const range = /unicode-range:\s*([^;]+);/.exec(rule)?.[1].trim() ?? 'U+0-10FFFF';
        const ranges = range.split(',').map((part) => {
          const [lo, hi = lo] = part.trim().slice(2).split('-');
          return [parseInt(lo, 16), parseInt(hi, 16)];
        });
        return { url: new URL(/url\(([^)]+?\.woff2)\)/.exec(rule)[1], at).href, range, ranges };
      }).reverse()));
  }
  return cjkSlices.cache.get(css);
}

/** The file of `slices` the screen sets code point `cp` in, if any: the
 *  last one loadCjkFonts adds whose range holds it. A numbered file that
 *  repeats a letter of latin is added after latin, and wins it. */
function cjkSliceFor(slices, cp) {
  const holds = (slice) => slice.ranges.some(([lo, hi]) => cp >= lo && cp <= hi);
  return slices.reduce((last, slice) => (holds(slice) ? slice : last), undefined);
}

/** Whether Fontsource serves `family` as a Chinese, Japanese or Korean
 *  family (its subsets name the script). Fails when the API does not
 *  answer: a CJK face taken for a Latin one would paint in a system face. */
async function isCjkFamily(family) {
  const meta = await fontsourceMeta(family);
  if (!meta) throw new Error(`api.fontsource.org did not describe ${family}: reload to try again`);
  return !!meta.subsets?.some((subset) => /^(chinese|japanese|korean)/.test(subset));
}

/** faces = { 'Noto Serif TC': ['400', '700'] }, as for loadFonts: the
 *  whole FONTS object may be passed, its other families are left to
 *  loadFonts. Adds one FontFace per file of each CJK face with its
 *  unicodeRange, then loads the files `text` touches. `text` is what the
 *  faces set: the sample for the text face; a book in several voices calls
 *  it once per voice (loadCjkFonts({ 'LXGW WenKai TC': ['400'] }, quotes)),
 *  so the heading and quotation faces fetch and check only their own
 *  characters. Fails when a character of `text` is in no file of a face.
 *  List every weight the pages use: a weight left to the engine's later round
 *  gets the latin file only. With { vertical: true } it also loads each
 *  family's vertical forms (brackets, quotes, pause marks) for the canvas,
 *  which needs loadVerticalAlternates imported from postext. Resolves to
 *  the number of files loaded. */
async function loadCjkFonts(faces, text, { vertical = false } = {}) {
  kitStatus('Loading fonts…');
  let loaded = 0;
  try {
    if (vertical && typeof loadVerticalAlternates !== 'function') {
      throw new Error('loadCjkFonts(…, { vertical: true }) needs loadVerticalAlternates imported from postext');
    }
    for (const [family, specs] of Object.entries(faces)) {
      if (!(await isCjkFamily(family))) continue;
      const twin = [];
      for (const spec of new Set(specs)) {
        const weight = parseInt(spec, 10);
        const style = spec.endsWith('i') ? 'italic' : 'normal';
        const slices = await cjkSlices(family, weight, style);
        const missing = [...new Set(text)].filter((ch) => /\S/.test(ch) && !cjkSliceFor(slices, ch.codePointAt(0)));
        if (missing.length) {
          throw new Error(`${family} ${spec} has no file for ${missing.slice(0, 12).join(' ')}: `
            + `give each face the text it sets (loadCjkFonts({ '${family}': ['${spec}'] }, text))`);
        }
        for (const slice of slices) {
          document.fonts.add(new FontFace(family, `url(${slice.url}) format('woff2')`,
            { weight: String(weight), style, unicodeRange: slice.range }));
          twin.push({ source: slice.url, weight: String(weight), style, unicodeRange: slice.range });
        }
        const font = `${style === 'italic' ? 'italic ' : ''}${weight} 16px "${family}"`;
        loaded += (await document.fonts.load(font, text)).length;
        if (!document.fonts.check(font, text)) throw new Error(`${family} ${spec} did not load for the sample`);
      }
      // The same files under a twin name with the `vert` feature on: the
      // canvas paints the punctuation of vertical lines with it.
      if (vertical && twin.length) await loadVerticalAlternates(family, twin);
    }
  } catch (error) {
    kitFail(error);
    throw error;
  }
  return loaded;
}

/** The PDF font provider for recipes with CJK faces: a family whose
 *  Fontsource subsets are Chinese, Japanese or Korean gets the files the
 *  screen set its pages' characters in (`request.codePoints`), each marked
 *  with its unicode-range and its place in the order loadCjkFonts added
 *  them: postext-pdf sets a character two files hold in the one added
 *  last, as the browser does. Any other family gets the latin file
 *  fontsourceProvider fetches (the "pdf" block) and, when the face sets
 *  letters only latin-ext has, that file too. */
async function cjkPdfProvider(family, weight, style, request) {
  if (!(await isCjkFamily(family))) return cjkLatinPdfFiles(family, weight, style, request);
  const meta = await fontsourceMeta(family);
  const weights = meta.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style;
  const slices = await cjkSlices(family, w, s);
  const picked = new Set();
  for (const cp of request?.codePoints ?? []) {
    const slice = cjkSliceFor(slices, cp);
    if (slice) picked.add(slice);
  }
  if (!picked.size) picked.add(slices[0]);
  return Promise.all(slices.map(async (slice, order) => {
    if (!picked.has(slice)) return null;
    const res = await fetch(slice.url);
    if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`);
    const bytes = await decompressWoff2(new Uint8Array(await res.arrayBuffer()));
    return Object.assign(bytes, { unicodeRange: slice.range, order });
  })).then((files) => files.filter(Boolean));
}

/** A Latin family set next to the CJK faces: its latin file, then its
 *  latin-ext file when the face sets letters only latin-ext has (ō ū in
 *  Hepburn rōmaji, ǎ in pinyin), the file loadFonts adds on screen for
 *  them, each marked with its unicode-range and its place in that order:
 *  the PDF sets a character in the file the browser took it from. A face
 *  Fontsource ships without latin-ext, or whose file does not come, gets
 *  latin alone, and the PDF names the letters it lacks. */
async function cjkLatinPdfFiles(family, weight, style, request) {
  const meta = await fontsourceMeta(family);
  const beyond = [...(request?.codePoints ?? [])].some(cjkLatinExtOnly);
  if (!beyond || !meta?.subsets?.includes('latin-ext')) return fontsourceProvider(family, weight, style);
  const weights = meta.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style;
  const id = fontsourceId(family);
  const url = `${await fontsourceAt(id)}files/${id}-latin-ext-${w}-${s}.woff2`;
  const [latin, ext] = await Promise.all([fontsourceProvider(family, weight, style), fetch(url)
    .then(async (res) => (res.ok ? decompressWoff2(new Uint8Array(await res.arrayBuffer())) : null), () => null)]);
  const ranges = kitRanges();
  return ext ? [Object.assign(latin, { unicodeRange: ranges.latin, order: 0 }),
    Object.assign(ext, { unicodeRange: ranges['latin-ext'], order: 1 })] : latin;
}

/** Whether code point `cp` is in Fontsource's latin-ext file and not in
 *  its latin file: Latin Extended-A and -B, IPA, the spacing modifiers and
 *  Latin Extended Additional (loadFonts's test for latin-ext), less the
 *  few latin holds too (ı Œ œ ʻ ʼ ˆ ˚ ˜). */
function cjkLatinExtOnly(cp) {
  if (!((cp >= 0x100 && cp <= 0x2ff) || (cp >= 0x1e00 && cp <= 0x1eff))) return false;
  return ![0x131, 0x152, 0x153, 0x2bb, 0x2bc, 0x2c6, 0x2da, 0x2dc].includes(cp);
}

/** showPages for a book bound on either edge. A right-bound book (the
 *  document says so: doc.binding is 'right' for page.binding 'right' and
 *  for vertical text) lies on the desk as it opens: page 1 alone on the
 *  left of the spine, then [3 | 2], the spine shade on each page's inner
 *  edge. `binding` ('left' | 'right') overrides the document's. */
function showBook(docs, { binding, ...options } = {}) {
  const count = showPages(docs, options);
  const right = (binding ?? [docs].flat()[0]?.binding) === 'right';
  if (!document.getElementById('pt-kit-cjk')) {
    // The pages keep direction ltr: a canvas draws text in the direction its
    // element inherits, and under rtl each run would end where the engine
    // starts it, its brackets mirrored.
    document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit-cjk">
      .pt-spread[dir="rtl"] canvas { direction: ltr; }
      .pt-spread[dir="rtl"] 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); }
    </style>`);
  }
  // Each pair stays [verso, recto] in the page; right to left, the verso
  // sits on the right. Phones stack the pages in reading order either way.
  for (const spread of document.querySelectorAll('#pages > .pt-spread')) spread.dir = right ? 'rtl' : 'ltr';
  document.getElementById('pages').dataset.binding = right ? 'right' : 'left';
  return count;
}

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

## Varianten

### Das Original auf die linke Seite setzen

Loeb-Ausgaben und viele chinesisch-englische Lesebücher setzen das Original links: Tauschen Sie die Paritäten der beiden Überschriftenformate und der beiden Kolumnentitel, die Buch und Dichter nennen, und schreiben Sie im Markdown jede chinesische Überschrift und jedes Gedicht vor sein Englisch.

```diff
-const english = { id: 'en', breakBefore: { enabled: true, parity: 'even' }, // a verso
+const english = { id: 'en', breakBefore: { enabled: true, parity: 'odd' }, // a recto
-const chinese = { id: 'zh', span: 'page', breakBefore: { enabled: true, parity: 'odd' },
+const chinese = { id: 'zh', span: 'page', breakBefore: { enabled: true, parity: 'even' },
-  head('verso-book', '{subtitle}', 'even', 'top-left', EDGE + 8,
+  head('verso-book', '{subtitle}', 'odd', 'top-right', -(EDGE + 8),
-  head('recto-poet', '{attr.name}', 'odd', 'top-right', -(EDGE + 8),
+  head('recto-poet', '{attr.name}', 'even', 'top-left', EDGE + 8,
```

### Die Glossen direkt unter das Gedicht setzen

Chinesische kommentierte Anthologien (注釋本) drucken die Anmerkungen nach jedem Gedicht. Kapitelendnoten erledigen das hier, da jedes Gedicht ein eigenes Kapitel ist.

```diff
-const footnotes = { placement: 'column', numbering: 'page', // the page foot; ① again on each page
+const footnotes = { placement: 'chapterEnd', chapterEndAlign: 'text',
```

## Häufige Fehler

- **Ein Überschriftenformat erbt den Seitenumbruch seiner Ebene.** Ein Eintrag in headingStyles übernimmt jedes Feld, das er auslässt, von seiner Überschriftenebene, breakBefore eingeschlossen. Ein Inhaltsverzeichnis oder Impressum, das nach einem :::pagebreak über eine H1 gestaltet wird, erbt die Parität 'odd' und landet hinter einer Vakatseite. Geben Sie einem solchen Format breakBefore: { enabled: false }.
- **Seite 1 ist eine rechte Seite: Seiten mit physischen Nummern planen.** Seite 1 ist eine rechte Seite und Seite 2 die erste linke, planen Sie Doppelseiten also mit physischen Seitenzahlen: ein Auftakt auf einer geraden Seite steht der ungeraden Seite danach gegenüber.
- **Chinesische Schnitte werden in Teilstücken geladen, über den Block cjk.** Fontsource liefert eine chinesische, japanische oder koreanische Familie als etwa hundert Dateien pro Schriftstärke aus, jede für einen Zeichenbereich. loadFonts holt nur die Datei latin, am Bildschirm stammen die Han-Zeichen also aus einer Systemschrift und werden falsch gemessen, und fontsourceProvider gibt dem PDF diese latin-Datei, die sie als leere Kästchen druckt. Führen Sie den Kit-Block cjk auf, rufen Sie loadCjkFonts(FONTS, markdown) nach loadFonts auf (einmal pro Stimme, mit dem Text, den sie setzt, wenn das Buch mehrere CJK-Schriften verwendet), und geben Sie renderToPdf fontProvider: cjkPdfProvider: Beide nehmen die Dateien, die die Zeichen des Textes enthalten.
- **Zeichnen Sie das Dokument mit zh-Hans oder zh-Hant aus, nicht mit LANG.** Die Ausgaben eines Rezepts sind en und es, ein chinesisches Beispiel ist aber in beiden Chinesisch: `locale: LANG` würde es als Englisch oder Spanisch auszeichnen, seine lateinischen Wörter trennen, seine Abbildungen mit Figure oder Figura beschriften und dem PDF die falsche Sprache geben. Seit postext 1.30 ist die Auszeichnung auch die Sprache, in der der Text geformt wird (gemessen, gezeichnet und eingebettet). Unter einer englischen oder spanischen Auszeichnung setzt eine chinesische Schrift daher ihre Ziffern und die mit lateinischem Text geteilte Interpunktion in anderen Formen, und die Zeilen brechen an anderer Stelle um. Schreiben Sie die Auszeichnung selbst: 'zh-Hans' (Festlandkonventionen: GB-Zeilenumbruch, Kaiming-Interpunktion) oder 'zh-Hant' (Taiwan: zentrierte Interpunktion voller Breite); 'zh-HK' für Hongkong. Ein bloßes 'zh' gilt als vereinfacht, Festland. Ein japanisches Beispiel erhält 'ja' (Häufiger Fehler ja-locale-tag).
- **Eine chinesische Schrift setzt Anführungszeichen und Apostrophe in voller Breite, auch in lateinischem Text.** Noto Serif TC und die anderen traditionell-chinesischen Schriften zeichnen ‘ ’ “ ” ein Geviert breit, so wie chinesischer Text sie verwendet, und diese Glyphe erhält auch ein lateinisches Wort in dieser Schrift: Li Bai’s druckt mit einer breiten Lücke nach dem Apostroph, und ein englisches Zitat öffnet und schließt mit einem vollen Geviert Leerraum. Eine CJK-Schrift hat auch keine Kursive, und die am Bildschirm nachgeahmte Neigung druckt im PDF aufrecht. Text, der Chinesisch mit einer lateinischen Sprache in einer Schrift mischt (Fußnoten, die die Grundschrift übernehmen, oder Glossen mit Pinyin), liest sich am besten ohne Apostrophe, typografische Anführungszeichen und Kursive; « » und die Latin-1-Zeichen behalten ihre Breite. Wo der lateinische Text für sich steht, geben Sie ihm über ein Absatzformat eine lateinische Schrift.

## Nachweise

- Rezept: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Text: Three Tang poems in traditional characters, Li Bai’s 靜夜思 and 月下獨酌 and Wang Wei’s 竹里館, as Three Hundred Tang Poems (1763) gives them: Li Bai, Wang Wei ([Quelle](https://zh.wikisource.org/wiki/唐詩三百首)), gemeinfrei
- Text: Du Fu’s 絕句, the second of his 絕句二首, as the Complete Tang Poems (1705, juan 228) prints it: Du Fu ([Quelle](https://zh.wikisource.org/wiki/全唐詩/卷228)), gemeinfrei
- Text: The English verse translations, from A History of Chinese Literature (1901), Project Gutenberg eBook 43711: Herbert A. Giles ([Quelle](https://www.gutenberg.org/ebooks/43711)), gemeinfrei
- Text: The notes, the English titles, the note on the text and the colophon: Postext Cookbook, Original
- Schrift: Noto Serif TC (OFL-1.1), LXGW WenKai TC (OFL-1.1), Noto Sans TC (OFL-1.1), Source Serif 4 (OFL-1.1), Source Sans 3 (OFL-1.1)
- Code: MIT · Beispielinhalt: MIT

## Verwandte Rezepte

- [Nº 043 · Übersetzung gegenüber, Strophe für Strophe](https://postext.dev/de/cookbook/bilingual-facing-verse.md): Jedes Gedicht ist ein randloser Kasten mit zwei Spalten: Ein fester Spaltenumbruch beginnt das Englische beim Titel, :::space hält die Strophen auf einer Höhe. · Stufe 2 (Mittel) · Lyrik
- [Nº 034 · Katalogeinträge gegenüber ihren Tafeln](https://postext.dev/de/cookbook/catalogue-facing-plates.md): Jeder Eintrag beginnt links und nennt seine Tafel im ersten Satz; die Tafel gleitet auf die rechte Seite gegenüber, skaliert auf den Satzspiegel. · Stufe 3 (Fortgeschritten) · Kataloge
- [Nº 015 · Gedichte, Zeile für Zeile gesetzt](https://postext.dev/de/cookbook/poetry-collection.md): Jedes Gedicht ist ein Block :::verse, geschrieben wie gedruckt: Die Einzüge von 1918 sind Leerzeichen, Folgezeilen hängen 2 em ein, Leerzeilen trennen Strophen. · Stufe 2 (Mittel) · Lyrik
