# Xinès vertical amb citacions en notes dins de la línia

> Un capítol vertical en caràcters tradicionals, enquadernat per la dreta, amb les citacions de nota GB/T 7714 compostes com a 夾注 dins de la columna.

- Versió HTML: https://postext.dev/ca/cookbook/vertical-book-with-jiazhu-citations
- Recepta Núm. 091 · Estructura del llibre · Nivell 3 (Avançat) · Sortides: Canvas, PDF
- Gèneres: Qualsevol gènere
- Requereix postext ≥ 1.12.0, postext-pdf ≥ 1.12.0 · provada amb 1.12.0, postext-pdf 1.12.0 el 2026-10-01
- Pàgines: [四十五](https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/es/p01.webp?v=07f8ae4e), [四十六](https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/es/p02.webp?v=07f8ae4e), [四十七](https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/es/p03.webp?v=07f8ae4e)
- PDF: https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/es/vertical-book-with-jiazhu-citations.pdf?v=07f8ae4e
- Obre al Sandbox: https://postext.dev/ca/sandbox#recipe=vertical-book-with-jiazhu-citations&lang=es (.postext: https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/es/vertical-book-with-jiazhu-citations.postext)
- Última actualització: 2026-10-01
- Altres idiomes: [en](https://postext.dev/en/cookbook/vertical-book-with-jiazhu-citations.md), [es](https://postext.dev/es/cookbook/vertical-book-with-jiazhu-citations.md), [zh](https://postext.dev/zh/cookbook/vertical-book-with-jiazhu-citations.md), [ar](https://postext.dev/ar/cookbook/vertical-book-with-jiazhu-citations.md)

## En poques paraules

Un capítol d'un llibre xinès compost en columnes de dalt a baix. Cada font es cita en una nota petita de dues línies dins de la columna, i al final es llisten les obres citades.

## Què compondràs

El final d'un capítol d'un llibre inventat sobre la impremta xinesa, 版刻叢談 («Converses sobre la impressió xilogràfica»), compost en caràcters tradicionals, de dalt a baix en columnes que es llegeixen des de la dreta, en una pàgina 大32開 de 140 × 203 mm enquadernada per la dreta. El capítol tracta de la disposició de les edicions Song i cita cinc obres reals. Cada citació es compon com els llibres xinesos han compost el comentari durant segles: com un 夾注, una nota en dues files a mig cos, dins de la columna on és la citació. Una nota que no cap en el que queda de columna continua al capdamunt de la següent, i una d'elles continua després del pas de pàgina. Les notes segueixen la forma de nota de la GB/T 7714—2015, la norma de referències de la Xina continental, i el capítol acaba amb una llista numerada de les obres que cita, les angleses girades. El mateix capítol amb les notes a peu de pàgina es compondria com [Notes al peu de la columna que les cita](https://postext.dev/ca/cookbook/novel-footnotes-column-foot.md).

**Aquesta recepta respon a:**

- Com componc les citacions com a notes de dues línies dins de la línia (夾注) en un llibre xinès vertical?
- Com poso les cites en notes a peu de pàgina, a l'estil de Chicago, amb forma breu en les següents?

## La resposta curta

```js
// script.js, línies 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
};
```

## Ingredients

**Ensenya**

- [Cites en notes](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia): Un estil de notes (Chicago notes, OSCOLA, GB/T 7714 notes) posa cada cita en una nota a peu de pàgina, amb forma breu a les cites següents, o en una nota de dues línies dins de la línia xinesa.
- [Cites en un estil de cita](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia): Obres citades com a [@clau, pàg. 33] i formatades en un estil CSL (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) triat a la configuració, enllaçades amb les seves entrades.

**També fa servir**

- [Warichu: notes a dues línies dins de la línia](https://postext.dev/ca/docs/document-format.md#marques-xineses-ruby-i-warichu)
- [Bibliografia a partir de les referències](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia)
- [Text vertical](https://postext.dev/ca/docs/configuration.md#escriptura-vertical)
- [Llibres enquadernats per la dreta](https://postext.dev/ca/docs/configuration.md#enquadernació)
- [Retícula de caràcters](https://postext.dev/ca/docs/configuration.md#retícula-de-caràcters)
- [Numeració xinesa](https://postext.dev/ca/docs/configuration.md#numeració)
- [Amplada de la puntuació xinesa](https://postext.dev/ca/docs/configuration.md#amplades-de-la-puntuació)
- [Fonts xineses, japoneses i coreanes](https://postext.dev/ca/docs/configuration.md#fonts-xineses-japoneses-i-coreanes)
- [Obertures dissenyades](https://postext.dev/ca/docs/configuration.md#span-i-disseny-avançat)
- [Títols numerats](https://postext.dev/ca/docs/configuration.md#configuració-per-nivell)
- [Capçaleres i folis](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus)
- [Capçaleres segons el tipus de pàgina](https://postext.dev/ca/docs/configuration.md#elements-de-text)
- [Marges simètrics](https://postext.dev/ca/docs/configuration.md#marges-simètrics-mirall)
- [Paleta de color semàntica](https://postext.dev/ca/docs/configuration.md#paleta-de-colors)
- [Exportació a PDF](https://postext.dev/ca/docs/configuration.md#generació-de-pdf)
- [Tall de línies en xinès](https://postext.dev/ca/docs/configuration.md#tipografia-de-làsia-oriental)
- [Equilibri de columnes](https://postext.dev/ca/docs/configuration.md#equilibratge-de-columnes)
- [Atributs de títol](https://postext.dev/ca/docs/document-format.md#atributs-dencapçalament)
- [Color del paper](https://postext.dev/ca/docs/configuration.md#pàgina)
- [Fonts incrustades al PDF](https://postext.dev/ca/docs/configuration.md#per-què-un-proveïdor-de-fonts)
- [Preliminars en romans](https://postext.dev/ca/docs/document-format.md#numbering)

**La configuració d'un cop d'ull**

- [`bodyText`](https://postext.dev/ca/docs/configuration.md#text-de-cos), [`citations`](https://postext.dev/ca/docs/configuration.md#cites), [`cjk`](https://postext.dev/ca/docs/configuration.md#tipografia-de-làsia-oriental), [`colorPalette`](https://postext.dev/ca/docs/configuration.md#paleta-de-colors), [`footer`](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus), [`header`](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus), [`headings`](https://postext.dev/ca/docs/configuration.md#encapçalaments), [`layout`](https://postext.dev/ca/docs/configuration.md#disposició), [`locale`](https://postext.dev/ca/docs/configuration.md#partició-de-mots), [`page`](https://postext.dev/ca/docs/configuration.md#pàgina)

**API**

- [`LOCALES`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`STYLES`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`buildDocument`](https://postext.dev/ca/docs/configuration.md#construir-un-document), [`clearMeasurementCache`](https://postext.dev/ca/docs/configuration.md#memòria-cau-de-mesures), [`createCiteprocEngine`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`decompressWoff2`](https://postext.dev/ca/docs/configuration.md#proveïdor-de-fonts-al-navegador-fontsource--woff2), `loadVerticalAlternates`, [`registerCitationEngine`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`renderPageToCanvas`](https://postext.dev/ca/docs/configuration.md#renderitzar-una-pàgina-a-un-bitmap), [`renderToPdf`](https://postext.dev/ca/docs/configuration.md#generació-de-pdf)

**Tipus de lletra**

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

## Elaboració

### 1 · Un estil de notes, compost dins de la línia

El codi és [la resposta curta](#la-resposta-curta) de més amunt. L'estil de notes de la GB/T 7714 (`china-national-standard-gb-t-7714-2015-note`) converteix cada citació en una nota que anomena l'obra sencera ([Citacions](/ca/docs/configuration#cites)). Per defecte, un estil de notes envia les notes al peu de la pàgina. `notes: 'warichu'` deixa cada nota a la columna, composta com a warichu: dues files a `cjk.warichu.fontSize`, mig cos del text, centrades a la columna, i es llegeix primer la fila dreta ([Marques, ruby i warichu](/ca/docs/configuration#marques-ruby-i-warichu)). El Markdown és el de qualsevol estil, `[@su1999]` després de les paraules que avala, i la nota ocupa el lloc de la citació, abans del 。 que tanca la frase.

![Página de apertura 四十五: 第三章 宋刻本的版式.](https://postext.dev/cookbook/vertical-book-with-jiazhu-citations/es/p01.webp?v=07f8ae4e)

*Pàgina 1: la nota del llibre de 宿白 comença al peu de la tercera columna i continua al capdamunt de la quarta, cada vegada amb la fila dreta primer.*

Les files s'omplen per trams. Al peu de la tercera columna només hi caben 宿白. 唐宋時期, que es reparteixen en dues files de tres i quatre caselles; la resta de la nota omple el capdamunt de la columna següent, altra vegada en dues files. A la pàgina 2 la nota de Twitchett comença al peu de l'última columna, i el lector passa la pàgina per acabar-la.

### 2 · Les obres citades, al final del capítol

`:::bibliography{title="本章引用書目" scope=chapter}` col·loca la llista on és i només inclou les obres que cita aquest capítol ([La bibliografia](/ca/docs/document-format#la-bibliografia)). Les referències surten d'un bloc BibTeX. Cada entrada porta un `langid`, `chinese` o `english`, que l'estil llegeix com a llengua de l'obra, i els noms escrits en caràcters queden sencers. La llista es numera per ordre de primera citació, i les columnes següents de cada entrada comencen sota la seva etiqueta.

### 3 · Caràcters tradicionals amb les regles del continent

`locale: 'zh-Hant-CN'` demana caràcters tradicionals amb les regles de la Xina continental, com componen els editors de Pequín i Xangai els estudis clàssics: la coma i el punt van a la cantonada superior dreta de la seva casella, i un llibre vertical s'enquaderna per la dreta. Les citacions segueixen l'etiqueta: l'escriptura tradicional tria el locale CSL zh-TW, així que les paraules que afegeix l'estil també van en caràcters tradicionals. Noto Serif SC dibuixa els caràcters tradicionals amb les formes del continent, que és el que demana aquesta edició; LXGW WenKai TC, una font de Taiwan, només compon el títol del capítol, que no porta puntuació ([Números en text vertical](/ca/docs/configuration#nombres-en-text-vertical) explica què passa amb les xifres).

A les notes i a la llista, un títol llatí i un any de quatre xifres es giren i baixen per la columna. Un número curt al costat de text xinès, com l'1 d'una etiqueta, queda dret a la seva casella.

### 4 · L'obertura entre filets

```js
// script.js, línies 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 楷
] } };
```

En una pàgina vertical, un disseny de títol es col·loca en el marc del flux: `x` baixa per la columna i `y` travessa la pàgina des de la vora dreta, així que un filet `'horizontal'` és una línia vertical al plec. Tres filets emmarquen dues columnes a 30,4 pt. 第三章 va a la primera en Hei negreta, i el títol a la segona en Kai ([Span i disseny avançat](/ca/docs/configuration#span-i-disseny-avançat)). El número del capítol surt de `startAt=3` al títol i de la plantilla `第{1:一}章`, i els folis de `pageNumbering`: `trad-chinese-informal` des de 45. Les capçaleres i els folis baixen pel marge exterior com a [Una novel·la xinesa vertical, enquadernada per la dreta](https://postext.dev/ca/cookbook/vertical-novel-right-bound.md) ([Elements de text verticals](/ca/docs/configuration#elements-de-text-verticals)).

```js
// script.js, línies 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 recepta completa

Un sol fitxer, compost a partir de la carpeta de la recepta amb el text d'exemple i el kit comú del Receptari ja inclosos; construeix la seva pròpia pàgina. Per executar-lo, posa'l en un `<script type="module">` d'una pàgina buida o enganxa'l al tauler JS d'un pen nou de CodePen (com a mòdul). Importa postext des d'esm.sh, així que no cal instal·lar ni compilar res.

- Carpeta de la recepta: 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.12.0
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, 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 = 'es'; // @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 = () => ({ // a factory: the engine caches resolved configs per object
  // 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'),
    balancing: { enabled: false }, // the columns stay on the grid
    levels: [{ level: 1, numberingTemplate: '第{1:一}章', numberSeparator: '　',
      breakBefore: { enabled: true, parity: 'odd' }, // gotcha: headings-drop-h1-break
      marginBottom: pt(0), advancedDesign: opener }] },
  header,
  footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "版刻叢談"
author: "沈硯秋"
subtitle: "Capítulo de muestra escrito para el Recetario de Postext. El libro y su autor son inventados; las cinco obras citadas son reales."
---

# 宋刻本的版式 {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 (gotcha: fonts-first)
  '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 buildWithFonts(() => buildDocument({ markdown }, config()), 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 v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    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',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

/** Runs `build` (a buildDocument or buildBundle call) and checks the faces
 *  the pages use. A regular face missing from FONTS is loaded with a warning;
 *  bold and italic variants are loaded when the family ships them. Then the
 *  measurement caches are cleared and the build runs again. */
async function buildWithFonts(build, text = '') {
  const tried = new Set();
  for (let round = 0; round < 3; round++) {
    kitStatus('Laying out…');
    await new Promise(requestAnimationFrame);          // let the status paint first
    const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; });
    const wanted = { base: {}, variants: {} };
    for (const { font, base } of [result].flat().flatMap(fontStringsOf)) {
      const { family, weight, style } = parseFont(font);
      const key = `${family}|${weight}|${style}`;
      if (tried.has(key) || hasFace(family, weight, style)) continue;
      tried.add(key);
      (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`);
    }
    if (Object.keys(wanted.base).length) {
      console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`);
    }
    const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true });
    if (added === 0) return result;
    clearMeasurementCache();
  }
  throw new Error('The fonts did not settle after three builds.');
}

/** Every font string of the layout. `base` marks a block's own face; its
 *  bold, italic and bold-italic variants are listed whether or not used. */
function fontStringsOf(doc) {
  const found = new Map();
  const walk = (node) => {
    if (!node || typeof node !== 'object') return;
    if (Array.isArray(node)) { node.forEach(walk); return; }
    for (const [key, value] of Object.entries(node)) {
      if (typeof value === 'string' && /fontString$/i.test(key)) {
        found.set(value, found.get(value) || key === 'fontString');
      } else if (value && typeof value === 'object') walk(value);
    }
  };
  walk(doc.pages);
  walk(doc.blocks);
  return [...found].map(([font, base]) => ({ font, base }));
}

/** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }.
 *  A string with no weight ('95.8px Young Serif', from a design text) is 400. */
function parseFont(font) {
  const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim());
  if (!m) throw new Error(`Unexpected font string: ${font}`);
  const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]);
  return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' };
}

/** True when a loaded FontFace covers exactly this family, weight and style
 *  (document.fonts.check() is also true 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;
}

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

/** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), 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 v1 ── the same in every recipe · postext.dev/cookbook ───────
/** Shows the pages as facing spreads on a dark desk: the first page is a
 *  recto on its own, then verso | recto pairs, as in a bound book. Pages
 *  are painted when they scroll near the screen. */
function showPages(docs, { title, width = 460 } = {}) {
  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, { 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 v1 ── the same in every recipe that exports a PDF ──────────────
/** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen
 *  used, snapping to a weight the family ships and falling back to upright
 *  when it has no italic: the PDF asks for every face a block could use. */
async function fontsourceProvider(family, weight, style) {
  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 res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`);
  if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`);
  return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
}

/** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new
 *  tab, since CodePen's preview frame cannot show 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 v1 ── 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 = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/${weight}${style === 'italic' ? '-italic' : ''}.css`;
  if (!cjkSlices.cache.has(css)) {
    cjkSlices.cache.set(css, fetch(css)
      .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], css).href, range, ranges };
      }).reverse()));
  }
  return cjkSlices.cache.get(css);
}

/** The file of `slices` that holds code point `cp`, if any. */
function cjkSliceFor(slices, cp) {
  return slices.find((slice) => slice.ranges.some(([lo, hi]) => cp >= lo && cp <= hi));
}

/** 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 buildWithFonts 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 that
 *  hold the characters its pages set (`request.codePoints`); any other
 *  family goes to fontsourceProvider (the "pdf" block). */
async function cjkPdfProvider(family, weight, style, request) {
  if (!(await isCjkFamily(family))) return fontsourceProvider(family, weight, style);
  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.filter((slice) => picked.has(slice)).map(async (slice) => {
    const res = await fetch(slice.url);
    if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`);
    return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
  }));
}

/** 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 ───────────────────────────────────────────────────────────────────────
```

## Variants

### Claudàtors al voltant de cada nota

Algunes edicions tanquen el 夾注 entre claudàtors al cos del text. `cjk.warichu` els posa a totes les notes, també a les de les citacions.

```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: '〕' },
```

### Números a la columna en lloc de notes

L'estil numèric de la GB/T 7714 amb claudàtors de cantonada imprimeix 〔1〕 dret a la columna, i la llista no canvia.

```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
```

## Errors freqüents

- **Etiqueta el document zh-Hans o zh-Hant, no amb LANG.** Les edicions d'una recepta són en i es, però una mostra xinesa és xinesa en totes dues: `locale: LANG` l'etiquetaria com a anglès o castellà, separaria les seves paraules llatines, anomenaria Figure o Figura les seves figures i donaria al PDF una llengua equivocada. Escriu tu l'etiqueta: 'zh-Hans' (convencions de la Xina continental: tall GB, puntuació Kaiming) o 'zh-Hant' (Taiwan: puntuació d'amplada completa centrada); 'zh-HK' per a Hong Kong. Un 'zh' tot sol es llegeix com a xinès simplificat continental.
- **Les fonts xineses es carreguen per fragments, amb el bloc cjk.** Fontsource serveix una família xinesa, japonesa o coreana en un centenar de fitxers per pes, cadascun amb un interval de caràcters. loadFonts només baixa el fitxer latin, així que a la pantalla els caràcters xinesos surten d'una font del sistema i es mesuren malament, i fontsourceProvider lliura al PDF aquest fitxer latin, que els imprimeix com a caixes buides. Afegeix el bloc cjk del kit, crida loadCjkFonts(FONTS, markdown) després de loadFonts (una vegada per veu, amb el text que compon, si el llibre fa servir diverses fonts xineses) i passa a renderToPdf fontProvider: cjkPdfProvider: tots dos agafen els fitxers que contenen els caràcters del text.
- **Compon la puntuació de cada regió amb una font d'aquella regió.** Les amplades de la puntuació mouen el blanc de cada signe al costat on el posa la regió del document, no al costat on el dibuixa la font: amb zh-Hans, una coma Kaiming conserva la meitat esquerra de la seva caixa, on una font simplificada dibuixa el signe, i cedeix la dreta. Una font tradicional centra ，。 a la caixa, de manera que la meitat que se'n va s'emporta part del signe i la coma s'enganxa al caràcter següent. LXGW WenKai TC, l'única Kai de text de Fontsource, ho fa en text simplificat. Compon el text, les notes i les cites continentals en Noto Serif SC o Noto Sans SC, deixa les fonts TC per a zh-Hant i fes servir una lletra de pinzell simplificada (Ma Shan Zheng) només en línies d'exhibició, on el text de disseny no aplica amplades de puntuació. A més, dibuixa formes heretades que no imprimeix ni l'estàndard continental ni el de Taiwan, 為 amb el 爫 de 爲 i 令 (a 冷, 領) amb el peu de 卩: revisa els caràcters que compon a cada pàgina.
- **Un llibre enquadernat per la dreta mostra els seus plecs amb showBook.** En un llibre enquadernat per la dreta (text àrab, hebreu o persa, xinès vertical, o page.binding 'right') la pàgina 1 continua sent senar, però queda a l'esquerra del llom, i els plecs es llegeixen [3 | 2]. showPages compon tots els llibres com si s'enquadernessin per l'esquerra; showBook, del bloc book (el bloc cjk porta la mateixa funció), llegeix doc.binding i reflecteix els plecs. capture.hero continua anomenant un plec en ordre de lectura, [parella, senar]: [2, 3].
- **En una retícula de caràcters, desactiva l'equilibri de columnes.** L'equilibri de columnes omple una pàgina que acaba curta (un títol que es queda amb el seu text deixa línies en blanc al peu) afegint línies de la retícula a sobre dels títols i component un paràgraf una línia més folgat. Una línia xinesa més folgada separa els seus caràcters (0,13 em en una pàgina GB/T 9704, molt per sobre de balancing.maxTracking), de manera que els caràcters surten de les columnes de la retícula i els títols de les seves línies. Una pàgina comptada en caselles acaba curta: posa headings.balancing: { enabled: false }.
- **Qualsevol objecte headings desactiva el salt de pàgina de l'H1.** Per defecte un H1 salta a una pàgina senar (always-odd), però qualsevol objecte headings anul·la aquest valor, de manera que els capítols van seguits i span: 'page' no fa res. Torna a declarar headings.levels[0].breakBefore: { enabled: true, parity } a cada configuració.
- **Posa entre cometes cada valor del frontmatter.** YAML llegeix title: 1984 com un nombre i una data com un objecte Date, i els valors que no són cadenes s'imprimeixen buits als marcadors i deixen el PDF sense títol. Posa entre cometes cada valor: title: "1984".
- **Carrega totes les fonts abans de compondre.** La composició mesura el text amb les fonts que el navegador ha carregat i en desa les amplades, així que una font que arriba després de la primera composició deixa talls de línia erronis i un PDF que ja no coincideix amb la pantalla. Carrega abans tots els pesos i estils, i crida clearMeasurementCache() abans de recompondre si alguna arriba tard.

- Un estil de notes abreuja la segona citació d'una obra, i el de la GB/T 7714 la redueix a 同1, «com a la nota 1». Un warichu no té número, així que el lector no troba la nota 1. Aquest capítol cita cada obra una vegada; un text que torni a citar una obra ha d'escriure la segona citació a mà o fer servir l'estil numèric de la segona variant.
- L'estil escriu puntuació occidental a les entrades xineses, `宿白. 唐宋時期的雕版印刷[M]. 北京: 文物出版社, 1999.`, tal com les imprimeix la norma. En una columna vertical aquests signes conserven la forma occidental: es giren i ocupen menys d'una casella, a diferència del 。 i la ， del text.
- Una fila de warichu va a mig cos: amb un text de 10,5 pt, cada fila fa 5,25 pt, la lletra més petita de la pàgina. Compon el text a 五號 o més gran, o puja `cjk.warichu.fontSize`; les dues files comparteixen sempre l'amplada d'una columna.

## Crèdits

- Recepta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Tipus de lletra: Noto Serif SC (OFL-1.1), Noto Sans SC (OFL-1.1), LXGW WenKai TC (OFL-1.1)
- Codi: MIT · Contingut d'exemple: CC-BY-4.0

## Relacionades

- [Núm. 075 · Una novel·la xinesa vertical, enquadernada per la dreta](https://postext.dev/ca/cookbook/vertical-novel-right-bound.md): El capítol 1 de 三國演義 en una pàgina 25開 de Taiwan: 40 caràcters per 16 columnes, un 回目 entre filets, capçaleres al tall i folis xinesos. · Nivell 3 (Avançat) · Narrativa, teatre i prosa literària
- [Núm. 071 · Notes al peu de la columna que les cita](https://postext.dev/ca/cookbook/novel-footnotes-column-foot.md): Les crides [^id] passen a notes numerades al peu de la columna que les cita, amb interruptors per a una columna, notes finals i numeració contínua. · Nivell 2 (Intermedi) · Narrativa, teatre i prosa literària
- [Núm. 089 · Un assaig d'història amb notes de Chicago](https://postext.dev/ca/cookbook/history-essay-chicago-notes.md): Un assaig d'història de quatre pàgines on les citacions [@clau, 87] surten com a notes de Chicago a peu de pàgina: completes la primera vegada, breus després. · Nivell 2 (Intermedi) · Articles i treballs acadèmics
