# Chinois vertical, citations en notes dans la ligne

> Un chapitre vertical en caractères traditionnels, relié à droite, dont les citations en notes GB/T 7714 sont composées en 夾注 dans la colonne.

- Version HTML: https://postext.dev/fr/cookbook/vertical-book-with-jiazhu-citations
- Recette Nº 091 · Structure du livre · Niveau 3 (Avancé) · Sorties: Canvas, PDF
- Genres: Tous genres
- Nécessite postext ≥ 1.25.0, postext-pdf ≥ 1.25.0 · testée avec 1.31.0, postext-pdf 1.31.0 le 2026-10-13
- Pages: [四十五](https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/en/p01.webp?v=33364552), [四十六](https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/en/p02.webp?v=33364552), [四十七](https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/en/p03.webp?v=33364552)
- PDF: https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/en/vertical-book-with-jiazhu-citations.pdf?v=33364552
- Ouvrir dans le Sandbox: https://postext.dev/fr/sandbox#recipe=vertical-book-with-jiazhu-citations&lang=en (.postext: https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/en/vertical-book-with-jiazhu-citations.postext)
- Dernière mise à jour: 2026-10-11
- Autres langues: [en](https://postext.dev/en/cookbook/vertical-book-with-jiazhu-citations.md), [es](https://postext.dev/es/cookbook/vertical-book-with-jiazhu-citations.md), [ca](https://postext.dev/ca/cookbook/vertical-book-with-jiazhu-citations.md), [pt](https://postext.dev/pt/cookbook/vertical-book-with-jiazhu-citations.md), [zh](https://postext.dev/zh/cookbook/vertical-book-with-jiazhu-citations.md), [ja](https://postext.dev/ja/cookbook/vertical-book-with-jiazhu-citations.md), [ar](https://postext.dev/ar/cookbook/vertical-book-with-jiazhu-citations.md)

## En bref

Un chapitre de livre chinois composé en colonnes de haut en bas. Chaque source est citée dans une petite note sur deux lignes à l’intérieur de la colonne, et les ouvrages cités sont listés à la fin.

## Ce que vous allez composer

La fin d’un chapitre d’un livre inventé sur l’imprimerie chinoise, 版刻叢談 (« Propos sur l’impression xylographique »), composé en caractères traditionnels, de haut en bas, en colonnes lues à partir de la droite, sur une page 大32開 de 140 × 203 mm reliée à droite. Le chapitre traite de la mise en page des éditions Song et cite cinq ouvrages réels. Chaque citation est composée comme les livres chinois composent le commentaire depuis des siècles : en 夾注, une note sur deux lignes à demi-corps, dans la colonne même où se trouve la citation. Une note qui ne tient pas dans ce qui reste d’une colonne se poursuit en tête de la suivante, et l’une d’elles continue après la tourne. Les notes suivent la forme de note de GB/T 7714—2015, la norme continentale pour les références, et le chapitre se termine par la liste numérotée des ouvrages qu’il cite, les titres anglais couchés. Le même chapitre avec ses notes en bas de page se composerait comme [Nº 071](https://postext.dev/fr/cookbook/novel-footnotes-column-foot.md).

**Cette recette répond à:**

- Comment composer les citations en notes sur deux lignes dans la ligne (夾注) d’un livre chinois vertical ?
- Comment composer les citations en notes de bas de page, style Chicago, avec des formes abrégées pour les citations suivantes ?

## La réponse courte

```js
// script.js, lignes 29–43
// The GB/T 7714 note style turns each citation into a note. notes: 'warichu' sets the note
// in the column where the citation stands, in two rows at half the text size, the right
// row read first; a note longer than what is left of the column goes on in the next one.
// The list at the chapter's end comes from :::bibliography{scope=chapter}, numbered in
// the order the works are cited. Latin words turn sideways in the notes and in the list.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'china-national-standard-gb-t-7714-2015-note',
  notes: 'warichu', // 夾注; 'footnote' sets the same notes at the foot of the page
  bibliography: { fontSize: em(0.9), entrySpacing: pt(0) },
};
const cjk = {
  grid: { enabled: true, charsPerLine: 36, linesPerPage: 15 },
  warichu: { fontSize: em(0.5), color: col('ink') }, // no brackets: the half size sets it off
};
```

## Ingrédients

**Enseigne**

- [Citations en note](https://postext.dev/fr/docs/document-format.md#citations-et-bibliographie): Un style de notes (notes Chicago, OSCOLA, notes GB/T 7714) compose chaque citation en note de bas de page, avec des formes abrégées pour les citations suivantes, ou en note sur deux rangs à l’intérieur d’une ligne chinoise.
- [Citations dans un style bibliographique](https://postext.dev/fr/docs/document-format.md#citations-et-bibliographie): Des ouvrages cités sous la forme [@key, p. 33] et mis en forme dans un style CSL (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) choisi dans les réglages, liés à leurs entrées.

**Utilise aussi**

- [Warichu : notes sur deux lignes dans la ligne](https://postext.dev/fr/docs/document-format.md#marques-chinoises-ruby-et-warichu)
- [Bibliographie tirée des références](https://postext.dev/fr/docs/document-format.md#citations-et-bibliographie)
- [Texte vertical](https://postext.dev/fr/docs/configuration-page-layout.md#écriture-verticale)
- [Livres reliés à droite](https://postext.dev/fr/docs/configuration-page-layout.md#reliure)
- [Grille de caractères](https://postext.dev/fr/docs/configuration-east-asian.md#grille-de-caractères)
- [Numération chinoise](https://postext.dev/fr/docs/configuration-page-layout.md#numérotation)
- [Largeur de la ponctuation chinoise](https://postext.dev/fr/docs/configuration-east-asian.md#chasse-de-la-ponctuation)
- [Polices chinoises, japonaises et coréennes](https://postext.dev/fr/docs/configuration-programmatic-usage.md#polices-chinoises-japonaises-et-coréennes)
- [Ouvertures composées](https://postext.dev/fr/docs/configuration-text.md#étendue-et-maquette-avancée)
- [Titres numérotés](https://postext.dev/fr/docs/configuration-text.md#réglages-par-niveau)
- [Titres courants et folios](https://postext.dev/fr/docs/configuration-page-layout.md#en-têtes-et-pieds-de-page)
- [Titres courants selon le rôle de la page](https://postext.dev/fr/docs/configuration-page-layout.md#éléments-de-texte)
- [Marges en miroir](https://postext.dev/fr/docs/configuration-page-layout.md#marges-en-miroir)
- [Palette de couleurs sémantique](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#palette-de-couleurs)
- [Export PDF](https://postext.dev/fr/docs/configuration-programmatic-usage.md#générer-des-pdf)
- [Coupure des lignes en chinois](https://postext.dev/fr/docs/configuration-east-asian.md#typographie-dasie-de-lest)
- [Attributs de titre](https://postext.dev/fr/docs/document-format.md#attributs-de-titre)
- [Couleur du papier](https://postext.dev/fr/docs/configuration-page-layout.md#page)
- [Polices intégrées au PDF](https://postext.dev/fr/docs/configuration-programmatic-usage.md#pourquoi-un-fournisseur-de-polices)
- [Pages liminaires en chiffres romains](https://postext.dev/fr/docs/document-format.md#numbering)

**La configuration en un coup d’œil**

- [`bodyText`](https://postext.dev/fr/docs/configuration-text.md#texte-courant), [`citations`](https://postext.dev/fr/docs/configuration-notes-references.md#citations), [`cjk`](https://postext.dev/fr/docs/configuration-east-asian.md#typographie-dasie-de-lest), [`colorPalette`](https://postext.dev/fr/docs/configuration-fonts-colors-viewers.md#palette-de-couleurs), [`footer`](https://postext.dev/fr/docs/configuration-page-layout.md#en-têtes-et-pieds-de-page), [`header`](https://postext.dev/fr/docs/configuration-page-layout.md#en-têtes-et-pieds-de-page), [`headings`](https://postext.dev/fr/docs/configuration-text.md#titres), [`layout`](https://postext.dev/fr/docs/configuration-page-layout.md#mise-en-page), [`locale`](https://postext.dev/fr/docs/configuration-text.md#césure), [`page`](https://postext.dev/fr/docs/configuration-page-layout.md#page)

**API**

- [`LOCALES`](https://postext.dev/fr/docs/document-format.md#citations-et-bibliographie), [`STYLES`](https://postext.dev/fr/docs/document-format.md#citations-et-bibliographie), [`buildDocument`](https://postext.dev/fr/docs/configuration-programmatic-usage.md#construire-un-document), [`createCiteprocEngine`](https://postext.dev/fr/docs/document-format.md#citations-et-bibliographie), [`decompressWoff2`](https://postext.dev/fr/docs/configuration-programmatic-usage.md#fournisseur-de-polices-dans-le-navigateur-fontsource--woff2), `loadVerticalAlternates`, [`registerCitationEngine`](https://postext.dev/fr/docs/document-format.md#citations-et-bibliographie), [`renderPageToCanvas`](https://postext.dev/fr/docs/configuration-programmatic-usage.md#rendre-une-page-en-bitmap), [`renderToPdf`](https://postext.dev/fr/docs/configuration-programmatic-usage.md#générer-des-pdf), [`withLoadedFonts`](https://postext.dev/fr/docs/configuration-programmatic-usage.md#charger-les-polices-avant-la-mise-en-page)

**Caractères**

- Noto Serif SC (OFL-1.1), Noto Sans SC (OFL-1.1), LXGW WenKai TC (OFL-1.1)

## Préparation

### 1 · Un style de notes, composé dans la ligne

Le code est dans [la réponse courte](https://postext.dev/fr/cookbook/vertical-book-with-jiazhu-citations.md#la-réponse-courte) plus haut. Le style de notes GB/T 7714 (`china-national-standard-gb-t-7714-2015-note`) est un style à notes : chaque citation devient une note qui nomme l’ouvrage en entier ([Citations](https://postext.dev/fr/docs/configuration-notes-references.md#citations)). Par défaut, un style à notes envoie ses notes en bas de page. `notes: 'warichu'` garde au contraire chaque note dans la colonne, composée en warichu : deux lignes à `cjk.warichu.fontSize`, la moitié du corps du texte, centrées sur la colonne, la ligne de droite lue en premier ([Signes, ruby et warichu](https://postext.dev/fr/docs/configuration-east-asian.md#signes-ruby-et-warichu)). Le Markdown est le même que pour n’importe quel style, `[@su1999]` après les mots qu’il appuie, et la note prend la place de la citation, avant le 。 qui termine la phrase.

![Opening page 四十五: 第三章 宋刻本的版式.](https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/en/p01.webp?v=33364552)

*Page 1 : la note sur le livre de 宿白 commence au pied de la troisième colonne et se poursuit en tête de la quatrième, la ligne de droite en premier à chaque fois.*

Les lignes se remplissent morceau par morceau. Au pied de la troisième colonne, il y a la place pour 宿白. 唐宋時期 et rien de plus : ce fragment est donc réparti sur deux lignes de trois et quatre cases ; le reste de la note remplit la tête de la colonne suivante, de nouveau sur deux lignes. Page 2, la note sur Twitchett commence au pied de la dernière colonne, et le lecteur tourne la page pour la terminer.

### 2 · Les ouvrages cités, à la fin du chapitre

`:::bibliography{title="本章引用書目" scope=chapter}` compose la liste là où elle se trouve et ne retient que les ouvrages cités dans ce chapitre ([La bibliographie](https://postext.dev/fr/docs/document-format.md#la-bibliographie)). Les références viennent d’un bloc BibTeX. Chaque entrée porte un `langid`, `chinese` ou `english`, que le style lit comme la langue de l’ouvrage, et les noms écrits en caractères restent entiers. La liste est numérotée dans l’ordre de première citation, et les colonnes de suite d’une entrée commencent sous son étiquette.

### 3 · Des caractères traditionnels aux règles continentales

`locale: 'zh-Hant-CN'` demande des caractères traditionnels composés selon les règles du continent, comme les éditeurs de Pékin et de Shanghai composent l’érudition classique : la virgule et le point se placent dans le coin supérieur droit de leur case, et un livre vertical est relié à droite. Les citations suivent l’étiquette : l’écriture traditionnelle choisit la locale CSL zh-TW, si bien que les mots ajoutés par le style sont eux aussi en caractères traditionnels. Noto Serif SC dessine les caractères traditionnels sous leurs formes continentales, ce qui convient à cette édition ; LXGW WenKai TC, un caractère taïwanais, ne compose que le titre du chapitre, qui n’a pas de ponctuation ([Les nombres en texte vertical](https://postext.dev/fr/docs/configuration-east-asian.md#nombres-en-texte-vertical) explique ce qu’il advient des chiffres).

Dans les notes et la liste, un titre latin et une année à quatre chiffres se couchent et descendent la colonne. Un nombre court à côté du texte chinois, comme le 1 d’une étiquette, reste debout dans sa case.

### 4 · L’ouverture entre des filets

```js
// script.js, lignes 47–64
// In the flow frame x runs down the column and y across the page, right to left, so a
// 'horizontal' rule is a vertical line on the sheet: three of them rule the two columns.
const [TITLE, PITCH] = [15, 1.6 * LEAD]; // pt: the title's size and its column pitch
const rule = (n) => ({ kind: 'rule', id: `rule-${n}`, direction: 'horizontal',
  thickness: pt(n === 1 ? 0.4 : 0.8), color: col('vermilion'),
  placement: { anchor: { to: 'container', edge: 'top-left' },
    offset: { y: pt(LEAD + n * PITCH) }, size: { width: 'fill' } } });
// A line of PITCH in column n (0, 1), its characters centred between two rules.
const title = (id, content, n, family, size, weight) => ({ kind: 'text', id, content,
  fontFamily: family, fontWeight: weight, fontSize: pt(size), lineHeight: PITCH / size,
  color: col('ink'), letterSpacing: pt(size * 0.25), align: 'left',
  placement: { anchor: { to: 'container', edge: 'top-left' },
    offset: { x: pt(2 * BODY), y: pt(LEAD + n * PITCH) } } }); // two characters down
const opener = { enabled: true, minHeight: cols(5), slot: { elements: [
  rule(0), rule(1), rule(2),
  title('number', '{number}', 0, HEI, 12, 700), // 第三章 in the first column, in 黑
  title('title', '{titleText}', 1, KAI, TITLE, 400), // the title in the second, in 楷
] } };
```

Sur une page verticale, une maquette de titre se dessine dans le cadre du flux : `x` descend la colonne et `y` traverse la page à partir de son bord droit, si bien qu’un filet `'horizontal'` est une ligne verticale sur la feuille. Trois filets encadrent deux colonnes espacées de 30,4 pt. 第三章 se place dans la première en Hei gras, et le titre dans la seconde en Kai ([Emplacements et maquette avancée](https://postext.dev/fr/docs/configuration-text.md#étendue-et-maquette-avancée)). Le numéro de chapitre vient de `startAt=3` sur le titre et du modèle `第{1:一}章`, et les folios de `pageNumbering` : `trad-chinese-informal` à partir de 45. Les titres courants et les folios descendent le long de la marge extérieure comme dans [Nº 075](https://postext.dev/fr/cookbook/vertical-novel-right-bound.md) ([Éléments en texte vertical](https://postext.dev/fr/docs/configuration-page-layout.md#éléments-de-texte-vertical)).

```js
// script.js, lignes 17–22
const palette = {
  ink: '#22201c', vermilion: '#a8352a', muted: '#6d665d', paper: '#fbf8f1',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.vermilion })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
```

## La recette complète

Un seul fichier, composé à partir du dossier de la recette avec le texte d’exemple et le kit commun des Recettes déjà inclus ; il construit sa propre page. Pour l’exécuter, placez-le dans un `<script type="module">` d’une page vide ou collez-le dans le panneau JS d’un nouveau pen CodePen (en tant que module). Il importe postext depuis esm.sh : il n’y a rien à installer ni à compiler.

- Dossier de la recette: https://github.com/drnachio/postext/tree/main/cookbook/vertical-book-with-jiazhu-citations

### script.js

```js
// ═══ Postext Cookbook · Nº 091 · Vertical Chinese with citations in inline notes ══════
// https://postext.dev/en/cookbook/vertical-book-with-jiazhu-citations
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Noto Serif SC, Noto Sans SC, LXGW WenKai TC (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, registerCitationEngine,
  loadVerticalAlternates,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'en'; // @lang: the language of the frame; the chapter is Chinese in both editions
const RECIPE = 'vertical-book-with-jiazhu-citations';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: black ink, one vermilion for the opener's rules, the paper of the page
const palette = {
  ink: '#22201c', vermilion: '#a8352a', muted: '#6d665d', paper: '#fbf8f1',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.vermilion })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const [SONG, HEI, KAI] = ['Noto Serif SC', 'Noto Sans SC', 'LXGW WenKai TC']; // 宋, 黑, 楷
const [BODY, LEAD] = [10.5, 19]; // pt: 五號 on a column pitch of 1.8 em
const cols = (n) => pt(n * LEAD); // n columns across the page

// #region answer: a note style set as 夾注, two half-size rows inside the column
// The GB/T 7714 note style turns each citation into a note. notes: 'warichu' sets the note
// in the column where the citation stands, in two rows at half the text size, the right
// row read first; a note longer than what is left of the column goes on in the next one.
// The list at the chapter's end comes from :::bibliography{scope=chapter}, numbered in
// the order the works are cited. Latin words turn sideways in the notes and in the list.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'china-national-standard-gb-t-7714-2015-note',
  notes: 'warichu', // 夾注; 'footnote' sets the same notes at the foot of the page
  bibliography: { fontSize: em(0.9), entrySpacing: pt(0) },
};
const cjk = {
  grid: { enabled: true, charsPerLine: 36, linesPerPage: 15 },
  warichu: { fontSize: em(0.5), color: col('ink') }, // no brackets: the half size sets it off
};
// #endregion

// #region opener: 第三章 and the title in two ruled columns, the text after a blank one
// In the flow frame x runs down the column and y across the page, right to left, so a
// 'horizontal' rule is a vertical line on the sheet: three of them rule the two columns.
const [TITLE, PITCH] = [15, 1.6 * LEAD]; // pt: the title's size and its column pitch
const rule = (n) => ({ kind: 'rule', id: `rule-${n}`, direction: 'horizontal',
  thickness: pt(n === 1 ? 0.4 : 0.8), color: col('vermilion'),
  placement: { anchor: { to: 'container', edge: 'top-left' },
    offset: { y: pt(LEAD + n * PITCH) }, size: { width: 'fill' } } });
// A line of PITCH in column n (0, 1), its characters centred between two rules.
const title = (id, content, n, family, size, weight) => ({ kind: 'text', id, content,
  fontFamily: family, fontWeight: weight, fontSize: pt(size), lineHeight: PITCH / size,
  color: col('ink'), letterSpacing: pt(size * 0.25), align: 'left',
  placement: { anchor: { to: 'container', edge: 'top-left' },
    offset: { x: pt(2 * BODY), y: pt(LEAD + n * PITCH) } } }); // two characters down
const opener = { enabled: true, minHeight: cols(5), slot: { elements: [
  rule(0), rule(1), rule(2),
  title('number', '{number}', 0, HEI, 12, 700), // 第三章 in the first column, in 黑
  title('title', '{titleText}', 1, KAI, TITLE, 400), // the title in the second, in 楷
] } };
// #endregion

// The fore-edge: the book's title or the chapter's down the outer margin, the folio low.
const foreEdge = (id, content, parity, edge, y, pages) => ({
  kind: 'text', id, content, parity, pages, writingMode: 'vertical-rl',
  fontFamily: HEI, fontSize: pt(8), color: col('muted'), overflow: 'clip',
  placement: { anchor: { to: 'outer', edge }, offset: { y: em(y) } },
});
const header = { elements: [
  foreEdge('book', '{title}', 'even', 'top', 4, 'body'),
  foreEdge('chapter', '{chapterNumber}　{chapterTitle}', 'odd', 'top', 4, 'body'),
  foreEdge('folio', '{pageNumber}', 'all', 'bottom', -5, 'all'),
  // Across the foot of the opening page, in the edition's language: what is invented.
  { kind: 'text', id: 'imprint', content: '{subtitle}', pages: 'opener', fontFamily: SONG,
    fontSize: pt(6.5), color: col('muted'), align: 'center', overflow: 'wrap',
    placement: { anchor: { to: 'page', edge: 'bottom' }, offset: { y: mm(-9) },
      size: { width: mm(100) } } },
] };

const config = () => ({
  // Traditional characters to the mainland's rules, as Beijing and Shanghai publishers set
  // classical scholarship (gotcha: cjk-locale-tag). The citations follow it: zh-TW terms.
  locale: 'zh-Hant-CN',
  colorPalette,
  citations,
  cjk,
  page: { sizePreset: 'custom', width: mm(140), height: mm(203), dpi: 150, // 大32開
    backgroundColor: col('paper'),
    margins: { top: mm(34), bottom: mm(26), left: mm(15), right: mm(19), mirror: true },
    pageNumbering: { format: 'trad-chinese-informal', startAt: 45 } }, // 四十五
  layout: { layoutType: 'single', writingMode: 'vertical-rl' }, // bound on the right
  bodyText: {
    fontFamily: SONG, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: em(2), indentAfterHeading: true,
  },
  headings: { fontFamily: SONG, fontWeight: 700, color: col('ink'),
    levels: [{ level: 1, numberingTemplate: '第{1:一}章', numberSeparator: '　',
      breakBefore: { enabled: true, parity: 'odd' }, // a chapter opens on an odd page
      marginBottom: pt(0), advancedDesign: opener }] },
  header,
  footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "版刻叢談"
author: "沈硯秋"
subtitle: "A specimen chapter written for the Postext Cookbook. The book and its author are invented; the five works cited are real."
---

# 宋刻本的版式 {startAt=3}

雕版印書，始於唐而盛於宋。唐代的印本流傳至今的極少，所見多是佛經、曆書一類的零星之物，敦煌所出的咸通九年《金剛經》卷子，是其中有確切紀年的一件[@su1999]。到了宋代，經史子集都有了刻本，官府、書院、私家和書坊競相開雕，書的樣子也大體定了下來[@zhang1989]。後世講版本的人談到書葉的格局，總是從宋本說起。

一塊書版刻成一葉，四周用欄線圍住，叫作版框。欄線只有一道的稱「四周單邊」，外粗內細兩道的稱「四周雙邊」，只在左右兩邊用雙線的稱「左右雙邊」。框內用直線分行，每半葉幾行、每行幾字，叫作行款。書目裏常見的「半葉十行，行二十字」，說的就是這個。同一地方的書坊往往沿用一種行款，所以著錄一部書，行款總是要記的，鑑定版本時也常用它來推斷刻書的地點和年代。

版框正中留出窄窄的一行，不刻正文，是為版心。印好的紙沿這一行對折，版心便落在書口上，翻檢時一眼可見。版心裏刻書名、卷次和葉碼，有時還刻上刻工的姓名和這一版的字數，那是當年計算工錢用的。版心上下各有一個形如魚尾的標記，叫作魚尾；魚尾到欄線之間若刻一道黑線，叫作黑口，空著不刻的叫作白口。

大體說來，浙江一帶的官刻和家刻，字體方整，多作白口、左右雙邊；福建建陽的坊刻，字體瘦勁，版式緊湊，常在框外左上角加一個小方格，刻上篇名，稱為書耳，便於讀者翻檢[@huang2005]。坊刻的書要賣得出去，處處替讀者打算，這一點從版式上也看得出來。

書末或目錄之後，常有一塊刻記，寫明何人於何年在何處刊行，有時兼作書坊的廣告，叫作牌記。牌記的形制有長方的，也有鐘形、亞字形的。後人鑑定版本，牌記是最直接的依據，可也最容易被書賈挖改作偽，所以不能單憑牌記下斷語，還要和行款、字體、紙墨、避諱字一起看。

西方學者系統地研究中國的雕版書，是二十世紀的事。Carter的專著第一次把中國印刷術的發明及其西傳完整地介紹給英語讀者[@carter1925]；Twitchett則從書業和讀者的角度，討論中古時期的印刷與出版[@twitchett1983]。兩書所談雖不限於版式，卻都把宋代看作雕版書定型的時代，和中國學者的看法相合。

:::bibliography{title="本章引用書目" scope=chapter}

:::references{format=bibtex}
@book{su1999, author = {宿白}, title = {唐宋時期的雕版印刷}, publisher = {文物出版社}, address = {北京}, year = 1999, langid = {chinese}}
@book{zhang1989, author = {張秀民}, title = {中國印刷史}, publisher = {上海人民出版社}, address = {上海}, year = 1989, langid = {chinese}}
@book{huang2005, author = {黃永年}, title = {古籍版本學}, publisher = {江蘇教育出版社}, address = {南京}, year = 2005, langid = {chinese}}
@book{carter1925, author = {Carter, Thomas Francis}, title = {The invention of printing in China and its spread westward}, publisher = {Columbia University Press}, address = {New York}, year = 1925, langid = {english}}
@book{twitchett1983, author = {Twitchett, Denis}, title = {Printing and publishing in medieval China}, publisher = {Frederic C. Beil}, address = {New York}, year = 1983, langid = {english}}
:::
`; // content.<lang>.md: the same Chinese text in both

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // every face the pages use, loaded before the build
  'Noto Serif SC': ['400', '700'], // SONG: the text, notes, list, imprint; the list's title
  'Noto Sans SC': ['400', '700'], // HEI: the fore-edge heads and folios; 第三章
  'LXGW WenKai TC': ['400'], // KAI: the chapter's title
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// Each voice loads the files of what it sets; the Song also what the style writes.
const heading = markdown.match(/^# (.*?)(?: \{.*\})?$/m)[1]; // the title, no attributes
const book = markdown.match(/^title: "(.*)"/m)[1]; // 版刻叢談, the head of the even pages
const numerals = '第章一二三四五六七八九十';
await loadFonts(FONTS, markdown);
await loadCjkFonts({ [SONG]: ['400'] }, `${markdown}卷等頁`, { vertical: true });
await loadCjkFonts({ [HEI]: ['400'] }, `${heading}${book}${numerals}`, { vertical: true });
await loadCjkFonts({ [SONG]: ['700'] }, markdown.match(/bibliography\{title="([^"]*)"/)[1],
  { vertical: true });
await loadCjkFonts({ [HEI]: ['700'] }, numerals);
await loadCjkFonts({ [KAI]: ['400'] }, heading);
const doc = await withLoadedFonts(() => buildDocument({ markdown }, config()),
  { ...kitFonts(FONTS), text: markdown });
showBook(doc, { title: t({ en: 'Vertical Chinese with citations in inline notes',
  es: 'Chino vertical con citas en notas dentro del renglón' }) });
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 ───────────────────────────────────────────────────────────────────────
```

## Variantes

### Des crochets autour de chaque note

Certaines éditions ferment un 夾注 entre crochets au corps du texte. `cjk.warichu` les pose pour toutes les notes, citations comprises.

```diff
-  warichu: { fontSize: em(0.5), color: col('ink') }, // no brackets: the half size sets it off
+  warichu: { fontSize: em(0.5), color: col('ink'), open: '〔', close: '〕' },
```

### Des numéros dans la colonne au lieu des notes

Le style numérique GB/T 7714 avec crochets d’angle imprime 〔1〕 debout dans la colonne, et la liste reste telle quelle.

```diff
-  style: 'china-national-standard-gb-t-7714-2015-note',
-  notes: 'warichu', // 夾注; 'footnote' sets the same notes at the foot of the page
+  style: 'china-national-standard-gb-t-7714-2015-numeric',
+  marker: 'corner', // 〔1〕, upright in vertical text
```

## Erreurs fréquentes

- **Balisez le document zh-Hans ou zh-Hant, pas avec LANG.** Les éditions d’une recette sont en et es, mais un exemple chinois est en chinois dans les deux : `locale: LANG` le baliserait en anglais ou en espagnol, couperait ses mots latins, nommerait ses figures Figure ou Figura et donnerait au PDF la mauvaise langue. Depuis postext 1.30, la balise est aussi la langue dans laquelle le texte est mis en forme (mesuré, peint et incorporé) : sous une balise anglaise ou espagnole, une police chinoise compose ses chiffres et la ponctuation qu’elle partage avec le texte latin sous d’autres formes, et les lignes se coupent ailleurs. Écrivez la balise vous-même : 'zh-Hans' (conventions de Chine continentale : coupure des lignes GB, ponctuation Kaiming) ou 'zh-Hant' (Taïwan : ponctuation pleine chasse centrée) ; 'zh-HK' pour Hong Kong. Un 'zh' seul se lit comme du chinois simplifié de Chine continentale. Un exemple japonais prend 'ja' (piège ja-locale-tag).
- **Les polices chinoises se chargent par tranches, avec le bloc cjk.** Fontsource sert une famille chinoise, japonaise ou coréenne sous la forme d’une centaine de fichiers par graisse, chacun couvrant une plage de caractères. loadFonts ne récupère que le fichier latin : à l’écran, les caractères han viennent d’une police système et sont mal mesurés, et fontsourceProvider remet ce fichier latin au PDF, qui les imprime en cases vides. Listez le bloc cjk du kit, appelez loadCjkFonts(FONTS, markdown) après loadFonts (une fois par voix, avec le texte qu’elle compose, quand le livre utilise plusieurs polices CJK) et donnez à renderToPdf fontProvider: cjkPdfProvider : les deux prennent les fichiers qui contiennent les caractères du texte.
- **Composez la ponctuation de chaque région dans un caractère de cette région.** Les chasses de ponctuation déplacent le blanc à côté de chaque signe du côté où la région du document le place, et non du côté où la police le dessine : en zh-Hans, une virgule Kaiming garde la moitié gauche de son cadre, où un caractère simplifié dessine le glyphe, et abandonne la moitié droite. Un caractère traditionnel centre ，。 dans le cadre : la moitié retirée contient une partie du glyphe, et la virgule serre le caractère suivant. LXGW WenKai TC, le seul caractère de texte Kai sur Fontsource, produit cet effet dans un texte simplifié. Composez le texte, les notes et les citations de Chine continentale en Noto Serif SC ou Noto Sans SC, gardez les polices TC pour zh-Hant, et n’employez un caractère pinceau simplifié (Ma Shan Zheng) que pour les lignes de titrage, où le texte de maquette n’applique aucune chasse de ponctuation. Ce caractère dessine aussi des formes héritées que ni la norme continentale ni celle de Taïwan n’impriment, 為 avec le haut 爫 de 爲 et 令 (dans 冷, 領) avec un pied 卩 : vérifiez les caractères qu’une page y compose.
- **Un livre relié à droite montre ses doubles pages avec showBook.** Dans un livre relié à droite (texte arabe, hébreu ou persan, chinois vertical, ou page.binding 'right'), la page 1 reste le recto, mais elle se trouve à gauche du dos, et les paires se lisent [3 | 2]. showPages présente tout livre relié à gauche ; showBook, du bloc book (le bloc cjk contient la même fonction), lit doc.binding et inverse les paires. capture.hero désigne toujours une double page dans l’ordre de lecture, [verso, recto] : [2, 3].

- Un style à notes écrit en forme abrégée un ouvrage cité une seconde fois, et le style de notes GB/T 7714 l’abrège en 同1, « comme à la note 1 ». Un warichu n’a pas de numéro : le lecteur ne peut donc pas retrouver la note 1. Ce chapitre cite chaque ouvrage une seule fois ; un texte qui cite de nouveau un ouvrage doit écrire la seconde citation à la main, ou employer le style numérique de la seconde variante.
- Le style écrit une ponctuation occidentale dans les entrées chinoises, `宿白. 唐宋時期的雕版印刷[M]. 北京: 文物出版社, 1999.`, comme la norme les imprime. Dans une colonne verticale, ces signes gardent leur forme occidentale : ils se couchent et occupent moins d’une case, à la différence du 。 et du ， du texte.
- Une ligne de warichu fait la moitié du corps du texte : à 10,5 pt, chaque ligne fait 5,25 pt, le plus petit caractère de la page. Composez le texte en 五號 ou plus grand, ou augmentez `cjk.warichu.fontSize` ; les deux lignes partagent toujours la largeur d’une seule colonne.

## Crédits

- Recette: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Caractères: Noto Serif SC (OFL-1.1), Noto Sans SC (OFL-1.1), LXGW WenKai TC (OFL-1.1)
- Code: MIT · Contenu d’exemple: CC-BY-4.0

## Recettes liées

- [Nº 075 · Un roman chinois vertical, relié à droite](https://postext.dev/fr/cookbook/vertical-novel-right-bound.md): Le chapitre 1 du 三國演義 en 25開 taïwanais : 40 caractères sur 16 colonnes, ouverture 回目 filetée, titres en marge, folios chinois, lecture de droite à gauche. · Niveau 3 (Avancé) · Fiction, théâtre et prose littéraire
- [Nº 071 · Des notes au pied de la colonne qui les appelle](https://postext.dev/fr/cookbook/novel-footnotes-column-foot.md): Les appels Markdown [^id] deviennent des notes numérotées au pied de la colonne qui les cite ; options : une colonne, fin de chapitre, numéros continus. · Niveau 2 (Intermédiaire) · Fiction, théâtre et prose littéraire
- [Nº 089 · Un essai d’histoire avec des notes Chicago](https://postext.dev/fr/cookbook/history-essay-chicago-notes.md): Un essai d’histoire de quatre pages dont les citations [@key, 87] deviennent des notes Chicago en bas de page : complètes la première fois, abrégées ensuite. · Niveau 2 (Intermédiaire) · Articles et publications universitaires
