# Un indice giapponese in ordine gojūon, per lettura

> Un capitolo sui termini della composizione giapponese e il suo indice: ogni segno dà la lettura (furigana o yomi), le voci vanno sotto あ行, か行, さ行 in ordine JIS.

- Versione HTML: https://postext.dev/it/cookbook/gojuon-index
- Ricetta Nº 126 · Struttura del libro · Livello 2 (Intermedio) · Output: Canvas, PDF
- Generi: Manuali, guide e opere di consultazione, Libri di testo
- Richiede postext ≥ 1.28.0, postext-pdf ≥ 1.28.0 · provata con 1.31.0, postext-pdf 1.31.0 il 2026-10-13
- Pagine: [9](https://postext.dev/cookbook/gojuon-index/en/p01.webp?v=ea935335), [10](https://postext.dev/cookbook/gojuon-index/en/p02.webp?v=ea935335), [11](https://postext.dev/cookbook/gojuon-index/en/p03.webp?v=ea935335)
- PDF: https://postext.dev/cookbook/gojuon-index/en/gojuon-index.pdf?v=ea935335
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=gojuon-index&lang=en (.postext: https://postext.dev/cookbook/gojuon-index/en/gojuon-index.postext)
- Ultimo aggiornamento: 2026-10-11
- Altre lingue: [en](https://postext.dev/en/cookbook/gojuon-index.md), [es](https://postext.dev/es/cookbook/gojuon-index.md), [ca](https://postext.dev/ca/cookbook/gojuon-index.md), [de](https://postext.dev/de/cookbook/gojuon-index.md), [fr](https://postext.dev/fr/cookbook/gojuon-index.md), [pt](https://postext.dev/pt/cookbook/gojuon-index.md), [zh](https://postext.dev/zh/cookbook/gojuon-index.md), [ja](https://postext.dev/ja/cookbook/gojuon-index.md), [ar](https://postext.dev/ar/cookbook/gojuon-index.md)

## In breve

Un breve capitolo che spiega le parole dei compositori giapponesi, con un indice in fondo. Ogni parola indicizzata porta la sua pronuncia, e Postext la usa per ordinare l’indice secondo l’ordine dei kana, sotto le righe あ行, か行 e così via.

## Che cosa comporrai

Un capitolo di un manuale immaginario di composizione giapponese, 組版の言葉 (Le parole della composizione), con il suo indice. Il capitolo spiega i nomi delle parti di una pagina, dei corpi dei caratteri e delle regole di punteggiatura, su una pagina A5 di 36 caratteri per 28 righe; ogni termine è segnato per l’indice dove viene spiegato. L’indice è il vero soggetto: il giapponese non ha un ordine alfabetico per i kanji, quindi un indice registra ogni voce secondo la sua lettura in kana, nell’ordine della tabella gojūon (五十音), sotto le righe あ行, か行, さ行. Un termine con furigana fornisce da sé la sua lettura; ogni altro termine con un kanji la dichiara in un attributo `yomi`. [L’indice dei nomi cinesi](https://postext.dev/it/cookbook/chinese-name-index.md) ordina invece i nomi cinesi per pinyin e per tratti.

**Questa ricetta risponde a:**

- Come ordino un indice giapponese in ordine gojūon secondo le letture?

## La risposta breve

```js
// script.js, righe 30–42
// Each mark gives the reading the entry sorts by. A term written with furigana gives it
// itself: :index[{版面|はん|めん}] files 版面 as はんめん. Any other term with a kanji says it
// with yomi: :index[明朝体]{yomi="みんちょうたい"}. Kana and Latin terms need nothing. The
// reading orders the entries in JIS X 4061 order (katakana as hiragana, small kana as large,
// voiced after plain, ー as the vowel before it); Latin terms come first, under A to Z.
const index = {
  groupBy: 'gojuon', // what 'auto' picks in a 'ja' document: heads あ行 か行 さ行 …
  fontFamily: MINCHO, fontSize: pt(8.5), lineHeight: pt(14),
  separator: '　', locatorSeparator: '、', // 版面　3、5: an ideographic space, then 、
  see: { italic: false }, // → ルビ, → 柱も見よ: upright, no italic in Japanese
  groups: { fontFamily: GOTHIC, fontSize: pt(9), fontWeight: 700, color: col('vermilion'),
    marginTop: pt(7) },
};
```

## Ingredienti

**Insegna**

- [Indice in ordine gojūon](https://postext.dev/it/docs/japanese-layout.md#indice-in-ordine-gojūon): Un indice giapponese ordinato per lettura, non per punto di codice: :index[漱石]{yomi="そうせき"} fornisce la lettura (altrimenti il ruby della parola segnata), le voci seguono JIS X 4061 (sonore dopo sorde, katakana con hiragana, ー secondo la vocale che lo precede) e si raggruppano sotto あ か さ た な は ま や ら わ, le righe della tabella dei kana. index.groupBy: 'kana' mette invece un’intestazione per ogni iniziale.
- [Indice analitico](https://postext.dev/it/docs/document-format.md#indice-analitico): Termini segnati nel testo con :index ed elencati da :::index in ordine alfabetico, con sottovoci, intervalli, rimandi e numeri di pagina che seguono il testo.

**Usa anche**

- [Furigana: letture in kana sopra i kanji](https://postext.dev/it/docs/japanese-layout.md#furigana)
- [Ruby: letture in pinyin e zhuyin](https://postext.dev/it/docs/document-format.md#segni-cinesi-ruby-e-warichu)
- [Font cinesi, giapponesi e coreani](https://postext.dev/it/docs/configuration-programmatic-usage.md#font-cinesi-giapponesi-e-coreani)
- [Divisione in righe del giapponese (kinsoku)](https://postext.dev/it/docs/japanese-layout.md#divisione-in-righe-kinsoku)
- [Spaziatura della punteggiatura giapponese (yakumono)](https://postext.dev/it/docs/japanese-layout.md#yakumono-la-spaziatura-della-punteggiatura)
- [Stili di titolo](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)
- [Capitoli non numerati](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)
- [Titoli numerati](https://postext.dev/it/docs/configuration-text.md#sovrascritture-per-livello)
- [Titoli su più righe di testo (gyōdori)](https://postext.dev/it/docs/japanese-layout.md#titoli-e-blocchi)
- [Una o due colonne](https://postext.dev/it/docs/configuration-page-layout.md#tipi-di-impaginazione)
- [Griglia dei caratteri](https://postext.dev/it/docs/configuration-east-asian.md#griglia-di-caratteri)
- [Aperture progettate](https://postext.dev/it/docs/configuration-text.md#estensione-e-progetto-grafico-avanzato)
- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)
- [Testatine e folio](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Margini speculari](https://postext.dev/it/docs/configuration-page-layout.md#margini-speculari)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Esportazione in PDF](https://postext.dev/it/docs/configuration-programmatic-usage.md#generare-pdf)
- [Divisione in righe del cinese](https://postext.dev/it/docs/configuration-east-asian.md#tipografia-dellasia-orientale)
- [Bilanciamento delle colonne](https://postext.dev/it/docs/configuration-text.md#bilanciamento-delle-colonne)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)
- [Font incorporati nel PDF](https://postext.dev/it/docs/configuration-programmatic-usage.md#perché-un-provider-di-font)
- [Geometria della sezione](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)

**La configurazione in breve**

- [`bodyText`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), [`cjk`](https://postext.dev/it/docs/configuration-east-asian.md#tipografia-dellasia-orientale), [`colorPalette`](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori), [`footer`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`header`](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina), [`headingStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli), [`headings`](https://postext.dev/it/docs/configuration-text.md#titoli), [`index`](https://postext.dev/it/docs/configuration-notes-references.md#indice-analitico), [`layout`](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione), [`locale`](https://postext.dev/it/docs/configuration-text.md#sillabazione), [`page`](https://postext.dev/it/docs/configuration-page-layout.md#pagina), [`paragraphStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)

**API**

- [`buildDocument`](https://postext.dev/it/docs/configuration-programmatic-usage.md#costruire-un-documento), [`decompressWoff2`](https://postext.dev/it/docs/configuration-programmatic-usage.md#provider-di-font-per-il-browser-fontsource--woff2), [`renderPageToCanvas`](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-una-pagina-come-bitmap), [`renderToPdf`](https://postext.dev/it/docs/configuration-programmatic-usage.md#generare-pdf), [`withLoadedFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione)

**Caratteri**

- Noto Serif JP (OFL-1.1), Noto Sans JP (OFL-1.1)

## Preparazione

### 1 · Una lettura per ogni termine con un kanji

Il codice è [la risposta breve](https://postext.dev/it/cookbook/gojuon-index.md#la-risposta-breve) qui sopra. Nel testo, `:index[{版面|はん|めん}]{main}` stampa 版面 con はんめん sopra e registra la voce come はんめん; `:index[明朝体]{yomi="みんちょうたい" main}` non stampa alcuna lettura e la registra come みんちょうたい. I termini in kana, ノンブル o のど, e i termini latini, DTP o JIS X 4051, non hanno bisogno di nulla ([segni di indice](https://postext.dev/it/docs/document-format.md#segni-dellindice)). La lettura decide solo l’ordine: la voce si stampa come è scritta. Le letture dei kanji non si possono indovinare, perché 天 qui è てん e altrove あめ, quindi il motore non ci prova mai.

### 2 · Ordine JIS e intestazioni di riga

`groupBy: 'gojuon'` è ciò che `'auto'` sceglie per `locale: 'ja'`. Le voci si ordinano secondo JIS X 4061: katakana come hiragana, kana piccoli come grandi, un kana sonoro subito dopo quello semplice, ー come la vocale che lo precede. Così 級 (きゅう) viene prima di 行取り (ぎょうどり), e ゴシック体 (ごしっくたい) dopo 小口 (こぐち). I termini latini vengono per primi, sotto le loro lettere, come li ordina JIS; simboli e cifre verrebbero prima di loro ([indice analitico](https://postext.dev/it/docs/configuration-notes-references.md#indice-analitico)).

![Opening page 11: 索引.](https://postext.dev/cookbook/gojuon-index/en/p03.webp?v=ea935335)

*Pagina 11: termini latini sotto D e J, poi le righe di kana da か行 a わ行, ogni voce secondo la sua lettura.*

### 3 · → e も見よ

Un rimando «vedi» è un segno con `see`, `:index{term="振り仮名" yomi="ふりがな" see="ルビ"}`, e stampa 振り仮名　→ルビ. Un rimando «vedi anche» usa `seealso` e stampa le destinazioni prima di も見よ, 級　10　→歯も見よ, come lo formulano gli indici giapponesi. `see: { italic: false }` mantiene le etichette in tondo, perché un carattere giapponese non ha corsivo. Lo spazio ideografico dopo ogni termine e il 、 tra le pagine vengono da `separator` e `locatorSeparator`.

### 4 · L’indice su due colonne

```js
// script.js, righe 46–60
const band = { kind: 'box', id: 'band', reserve: false, style: { backgroundColor: col('tint') },
  placement: { anchor: { to: 'page', edge: 'top-left' },
    size: { width: 'fill', height: mm(56) } } };
const opener = (sink, ground = []) => ({ enabled: true, minHeight: pt(PITCH * sink),
  slot: { elements: [...ground,
  { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: GOTHIC, fontSize: pt(9),
    fontWeight: 700, letterSpacing: pt(2), color: col('vermilion'), align: 'left',
    placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(10) } } },
  { kind: 'text', id: 'title', content: '{titleText}', fontFamily: GOTHIC, fontSize: pt(22),
    fontWeight: 700, color: col('ink'), align: 'left', overflow: 'wrap',
    placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { y: mm(3) } } },
  { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5),
    color: col('vermilion'), placement: { anchor: { to: '#title', edge: 'below' },
      offset: { y: mm(4) }, size: { width: mm(12) } } },
] } });
```

Il titolo dell’indice è uno stile di titolo con `span: 'page'` e un `layout` a due colonne: il suo progetto grafico, una fascia chiara con il titolo, occupa tutta la pagina, e `:::index` scorre su due colonne sotto di esso. Le intestazioni di riga sono composte nel gotico vermiglio, le voci nel mincho del testo a 8,5 pt su un’interlinea di 14 pt.

## La ricetta completa

Un unico file, composto a partire dalla cartella della ricetta con il testo di esempio e il kit comune del Ricettario già inclusi; costruisce la propria pagina. Per eseguirlo, mettilo in uno `<script type="module">` di una pagina vuota, oppure incollalo nel pannello JS di un nuovo pen di CodePen (come modulo). Importa postext da esm.sh: non c’è niente da installare né da compilare.

- Cartella della ricetta: https://github.com/drnachio/postext/tree/main/cookbook/gojuon-index

### script.js

```js
// ═══ Postext Cookbook · Nº 126 · A Japanese index in gojūon order, read from its readings ═══
// https://postext.dev/en/cookbook/gojuon-index
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Noto Serif JP, Noto Sans JP (SIL OFL 1.1) · Needs postext ≥ 1.28.0
import { buildDocument, withLoadedFonts, renderPageToCanvas } from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the frame; the chapter is Japanese in both
const RECIPE = 'gojuon-index';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: ink, one vermilion for the row heads and the chapter label, pale rules
const palette = {
  ink: '#211e1c', // text: a warm near-black
  vermilion: '#b33a22', // the one accent: あ行 heads, the kicker, folios
  tint: '#f5ece6', // the index heading's band
  rule: '#d3c8c0', // the rule under each title
  muted: '#6c625b', // running heads, the colophon
  paper: '#ffffff',
};
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 [MINCHO, GOTHIC] = ['Noto Serif JP', 'Noto Sans JP'];
const [BODY, PITCH] = [9, 16]; // pt: 9 pt text on a 16 pt line
const [CHARS, LINES] = [36, 28]; // the type area in characters

// #region answer: the index in gojūon order: by reading, under あ行 か行 さ行…
// Each mark gives the reading the entry sorts by. A term written with furigana gives it
// itself: :index[{版面|はん|めん}] files 版面 as はんめん. Any other term with a kanji says it
// with yomi: :index[明朝体]{yomi="みんちょうたい"}. Kana and Latin terms need nothing. The
// reading orders the entries in JIS X 4061 order (katakana as hiragana, small kana as large,
// voiced after plain, ー as the vowel before it); Latin terms come first, under A to Z.
const index = {
  groupBy: 'gojuon', // what 'auto' picks in a 'ja' document: heads あ行 か行 さ行 …
  fontFamily: MINCHO, fontSize: pt(8.5), lineHeight: pt(14),
  separator: '　', locatorSeparator: '、', // 版面　3、5: an ideographic space, then 、
  see: { italic: false }, // → ルビ, → 柱も見よ: upright, no italic in Japanese
  groups: { fontFamily: GOTHIC, fontSize: pt(9), fontWeight: 700, color: col('vermilion'),
    marginTop: pt(7) },
};
// #endregion

// #region opener: the chapter label, the title and a short rule; the index's on a band
const band = { kind: 'box', id: 'band', reserve: false, style: { backgroundColor: col('tint') },
  placement: { anchor: { to: 'page', edge: 'top-left' },
    size: { width: 'fill', height: mm(56) } } };
const opener = (sink, ground = []) => ({ enabled: true, minHeight: pt(PITCH * sink),
  slot: { elements: [...ground,
  { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: GOTHIC, fontSize: pt(9),
    fontWeight: 700, letterSpacing: pt(2), color: col('vermilion'), align: 'left',
    placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(10) } } },
  { kind: 'text', id: 'title', content: '{titleText}', fontFamily: GOTHIC, fontSize: pt(22),
    fontWeight: 700, color: col('ink'), align: 'left', overflow: 'wrap',
    placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { y: mm(3) } } },
  { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.5),
    color: col('vermilion'), placement: { anchor: { to: '#title', edge: 'below' },
      offset: { y: mm(4) }, size: { width: mm(12) } } },
] } });
// #endregion

// Running heads: the book on the verso, the chapter on the recto, folios outside.
const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity,
  pages: 'body', fontFamily: GOTHIC, fontSize: pt(7.5), color: col('muted'),
  align: edge.endsWith('left') ? 'left' : 'right', placement: { anchor: { to: 'container',
    edge }, offset: { x: mm(x), y: mm(12) } }, ...extra });
const folio = { fontWeight: 700, color: col('vermilion') };

const config = () => ({
  locale: 'ja', // written out, never LANG (gotcha: ja-locale-tag)
  colorPalette,
  index,
  page: { sizePreset: 'custom', width: mm(148), height: mm(210), dpi: 150, // A5
    margins: { top: mm(23), bottom: mm(19), left: mm(18), right: mm(15), mirror: true } },
  layout: { layoutType: 'single' },
  cjk: { grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES },
    ruby: { fontSize: em(0.5) } }, // furigana on a term's first mention, in the 7 pt gap
  bodyText: { fontFamily: MINCHO, fontSize: pt(BODY), lineHeight: pt(PITCH), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: em(1), indentAfterHeading: true, avoidRunts: true },
  headings: { fontFamily: GOTHIC, fontWeight: 700, color: col('ink'),
    balancing: { enabled: false }, // heads stay on the grid
    levels: [
      // parity 'odd': the next recto; the default 'always-odd' also leaves a blank page before it.
      { level: 1, breakBefore: { enabled: true, parity: 'odd' }, advancedDesign: opener(6) },
      { level: 2, numberingTemplate: '{1}.{2}', numberSeparator: '　', fontSize: pt(10.5),
        lineSpan: 3 }, // 3行取り
    ] },
  headingStyles: [{ id: 'index', numbered: false, span: 'page', // the title over both columns
    breakBefore: { enabled: true, parity: 'any' },
    advancedDesign: opener(5, [band]), // two columns, two characters apart
    layout: { layoutType: 'double', gutterWidth: pt(2 * BODY) } }],
  paragraphStyles: [{ id: 'colophon', fontFamily: GOTHIC, fontSize: pt(6.5), lineHeight: pt(9),
    color: col('muted'), firstLineIndent: pt(0), textAlign: 'left', marginTop: pt(PITCH) }],
  header: { elements: [
    head('v-folio', '{pageNumber}', 'even', 'top-left', 0, folio),
    head('v-title', '{title}', 'even', 'top-left', 8),
    head('r-title', '{chapterTitle}', 'odd', 'top-right', -8),
    head('r-folio', '{pageNumber}', 'odd', 'top-right', 0, folio),
  ] },
  footer: { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
    pages: 'opener', fontFamily: GOTHIC, fontSize: pt(7.5), fontWeight: 700,
    color: col('vermilion'), align: 'center', placement: { anchor: { to: 'container',
      edge: 'bottom' }, offset: { y: mm(-10) } } }] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "組版の言葉"
---

# 紙面を呼ぶ言葉 {kicker="第1章"}

本を作る現場には、紙面のあちこちを呼び分ける言葉がある。編集者と組版者とデザイナーが同じ言葉を使えば、指示は短く、間違いも少なくなる。本章では、日本語の本づくりで使われる基本的な用語を、紙面の名前、文字の大きさと書体、約物と組版の規則の三つに分けて紹介する。

## 紙面の名前

ページのうち、本文が組まれる範囲を:index[{版面|はん|めん}]{main}という。版面の外側の余白には、それぞれ名前がある。上の余白が:index[{天|てん}]、下の余白が:index[{地|ち}]、綴じる側の余白が:index[のど]、その反対側が:index[{小口|こ|ぐち}]である。横組みの本では、のどは左ページの右側と右ページの左側にくる。

余白に置かれる書名や章の名前を:index[{柱|はしら}]{main}と呼び、ページの番号を:index[ノンブル]{main}と呼ぶ。ノンブルはフランス語のnombreから来た言葉で、印刷所ではいまも普通に使われている。:index{term="ノンブル" seealso="柱"}柱とノンブルは同じ行に並べることが多く、まとめて「柱」と呼ぶ人もいる。

## 文字の大きさと書体

金属活字の時代、文字の大きさは:index[{号数|ごう|すう}]で呼ばれていた。本文に多く使われた五号は、いまの10.5ポイントに当たる。:index[ポイント]{main}は欧米から入った単位で、DTPでは1ポイントを72分の1インチ、約0.353ミリとする。

写真植字、略して:index[{写植|しゃ|しょく}]{main}の時代には、文字の大きさを:index[{級|きゅう}]、行の送りを:index[{歯|は}]で測った。どちらも0.25ミリを単位とし、13級は3.25ミリの大きさになる。:index{term="級" seealso="歯"}写植の文字盤は、1990年代に:index[DTP]{main}が広まると使われなくなったが、級と歯はいまもデザインの指定に残っている。:index{term="写植" seealso="DTP"}

書体では、縦の線が太く横の線が細い:index[明朝体]{yomi="みんちょうたい" main}が本文に、線の太さがそろった:index[ゴシック体]{yomi="ごしっくたい"}が見出しや注に使われる。明朝体の名は中国の明の時代の木版の文字に、ゴシック体の名は欧文のサンセリフを指した英語の呼び名に由来する。

## 約物と組版の規則

句読点や括弧のように、文字以外の記号を:index[{約物|やく|もの}]{main}という。:index[{句読点|く|とう|てん}]の「、」と「。」は全角の幅を持ち、括弧と並ぶと間を詰めて組む。行の頭に「。」や「」」が来たり、行の末に「「」が残ったりしないように行の分け方を調整する処理を、:index[禁則処理]{yomi="きんそくしょり" main}という。日本語の組版の規則は、日本工業規格の:index[JIS X 4051]「日本語文書の組版方法」にまとめられ、W3Cの:index[JLReq]「日本語組版処理の要件」がその内容を英語でも紹介している。

漢字の読みを小さな仮名で示すものを:index[ルビ]{main}と呼ぶ。:index{term="振り仮名" yomi="ふりがな" see="ルビ"}ルビの名は、5.5ポイントの活字を英国でrubyと呼んだことに由来する。語を強調するために文字の横に打つ点は:index[{圏点|けん|てん}]{main}といい、:index{term="傍点" yomi="ぼうてん" see="圏点"}縦組みでは文字の右に、横組みでは上に置く。

縦組みの中で二桁の数字などを横に並べて一字分に収めるのが:index[{縦中横|たて|ちゅう|よこ}]、本文の行の中に小さな文字の注を二行に割って入れるのが:index[{割注|わり|ちゅう}]である。見出しを本文の何行分かの幅に収めて組むことを:index[{行取|ぎょう|ど}り]といい、3行取りの見出しなら、前後の本文の行の位置を変えずに済む。

これらの言葉の多くは、活字の時代から写植、DTPへと道具が変わっても使われ続けてきた。道具が変わっても、紙面の上で解くべき問題が同じだからである。

# 索引 {style="index" kicker="五十音順"}

:::index

:::paragraphs{style="colophon"}
A specimen chapter written for the Postext Cookbook; the book is fictitious. The index files each term by its reading, taken from the furigana or from yomi. Set in Noto Serif JP and Noto Sans JP (SIL OFL). Text: CC BY 4.0.
:::
`; // content.<lang>.md: the same Japanese chapter in both

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // every face the pages use, loaded before the build
  'Noto Serif JP': ['400', '700'], // 明朝: the text and the index, its main pages bold
  'Noto Sans JP': ['400', '700'], // ゴシック: titles, heads, row heads, folios, colophon
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// Each face loads the files of what it sets (gotcha: cjk-fonts-slices): the gothic the heads
// and the row heads the index prints, あ行 to わ行 and A to Z.
const all = (re) => (markdown.match(re) ?? []).join('');
const ROWS = 'あかさたなはまやらわ行ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
await loadFonts(FONTS, markdown);
await loadCjkFonts({ [MINCHO]: FONTS[MINCHO] }, `${markdown}→も見よ、`);
await loadCjkFonts({ [GOTHIC]: FONTS[GOTHIC] },
  `${all(/^#+ .*$/gm)}${all(/colophon"\}\n[^\n]*/g)}組版の言葉${ROWS}`);
// Page 1 is page 9 of the book, a recto; the index follows the chapter.
const continuation = { pageIndexOffset: 8, pageNumbering: { startAt: 9 } };
const doc = await withLoadedFonts(() => buildDocument({ markdown, continuation }, config()),
  { ...kitFonts(FONTS), text: markdown });
showPages(doc, { title: t({ en: 'A Japanese index in gojūon order',
  es: 'Un índice japonés en orden gojūon' }) });
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 ───────────────────────────────────────────────────────────────────────
```

## Varianti

### Intestazioni per primo kana

`'kana'` intesta ogni gruppo con il primo kana della voce, come fanno i dizionari e i lunghi indici dei nomi: か, き, く invece di un unico か行.

```diff
-  groupBy: 'gojuon', // what 'auto' picks in a 'ja' document: heads あ行 か行 さ行 …
+  groupBy: 'kana', // heads か き く け こ…
```

### Una lettura senza furigana nel testo

Per lasciare nuda la prima occorrenza di un termine e fornirne comunque la lettura, sposta la lettura in `yomi`.

```diff
-ページのうち、本文が組まれる範囲を:index[{版面|はん|めん}]{main}という。
+ページのうち、本文が組まれる範囲を:index[版面]{yomi="はんめん" main}という。
```

## Errori frequenti

- **Assegna a un testo giapponese il tag 'ja', mai zh-Hans o LANG.** Le edizioni di una ricetta sono en ed es, ma un esempio giapponese è giapponese in entrambe: `locale: LANG` gli darebbe il tag inglese o spagnolo, e un tag cinese lo comporrebbe con le regole cinesi (punteggiatura Kaiming, kana piccoli liberi di aprire una riga, 图 per 図, forme cinesi dei glifi nel PDF). Da postext 1.30 il tag è anche la lingua usata per lo shaping del testo (misura, disegno e incorporamento), quindi qualunque tag diverso da 'ja' dà forme dei glifi diverse da quelle giapponesi (quelle cinesi con un tag cinese) e altri a capo. Scrivi 'ja': sceglie la regione Giappone (divisione in righe e punteggiatura JLReq, segni d’enfasi a sesamo, spaziatura dei furigana, etichette 図 e 表) e disattiva la sillabazione. Il lint fa fallire un testo con kana sotto un tag zh o ko.
- **Componi il giapponese in un carattere giapponese.** Noto Serif SC e TC hanno i kana, ma disegnano i kanji in forme cinesi (直, 骨 e 角 sono diversi) e i kana con un disegno cinese. Componi il testo in Noto Serif JP o Shippori Mincho B1 e i titoli in Noto Sans JP, caricati con loadCjkFonts. I file giapponesi di Fontsource non contengono hentaigana né altri kana storici (U+1B000–1B16F): loadCjkFonts fallisce su di essi e il PDF stampa riquadri, quindi scrivi i kana moderni o includi negli asset della ricetta un carattere che li abbia. A Shippori Mincho mancano anche le vocali con macron ō e ū: componi il rōmaji in un carattere latino.
- **I caratteri cinesi si caricano a fette, tramite il blocco cjk.** Fontsource serve una famiglia cinese, giapponese o coreana come un centinaio di file per peso, ognuno dei quali copre un intervallo di caratteri. loadFonts scarica solo il file latin, così a schermo i caratteri Han vengono da un font di sistema e sono misurati male, e fontsourceProvider passa al PDF quel file latin, che li stampa come riquadri vuoti. Elenca il blocco cjk del kit, chiama loadCjkFonts(FONTS, markdown) dopo loadFonts (una volta per voce tipografica, con il testo che compone, quando il libro usa più caratteri CJK) e passa a renderToPdf fontProvider: cjkPdfProvider: entrambi prendono i file che contengono i caratteri del testo.
- **Il cinese non ha corsivo.** Le famiglie CJK non includono corsivi, e la tipografia cinese segna l’enfasi con punti accanto ai caratteri, non con un’inclinazione. In un documento con tag cinese, *…* mette i punti d’enfasi sui caratteri cinesi che contiene e riserva il corsivo alle parole latine (cjk.emphasis: 'dots', il valore predefinito per il cinese); :dots[…] li mette ovunque. Con cjk.emphasis: 'italic', o in un documento con tag en o es, *…* fa inclinare al canvas i glifi in tondo, un obliquo sintetico che la tipografia cinese non usa mai (il PDF ripiega sulla variante in tondo): mantieni il tag cinese, oppure componi l’enfasi in grassetto o in un carattere Kai (LXGW WenKai) tramite uno stile di paragrafo o di chip.
- **Avviso di impaginazione: Voce dell’indice senza lettura** (`indexReadingMissing`). Un indice giapponese ordina le voci in base alla lettura, e nessun algoritmo legge i kanji in modo affidabile: questa voce contiene un kanji e nessuno dei suoi marcatori fornisce una lettura (yomi, ruby in kana sul testo marcato, o una chiave di ordinamento in kana), quindi viene ordinata dopo le voci in kana, per punto di codice. Soluzione: Aggiungi yomi="…" con la lettura in kana a uno dei marcatori della voce (:index[東京]{yomi="とうきょう"}), oppure marca un testo che porti il ruby ({東京|とう|きょう}). ([Documentazione](https://postext.dev/it/docs/japanese-layout.md#indice-in-ordine-gojūon))

- Un termine con un kanji e senza lettura solleva `indexReadingMissing`. Senza il suo `yomi`, 明朝体 finiva in fondo all’indice, dopo わ行, senza intestazione, e la build avvisava che la voce «has a kanji and no reading».
- `:ruby[…]` dentro `:index[…]` richiede l’escape delle parentesi quadre, perché il segno finisce alla prima `]`; scrivi la forma compatta, `{版面|はん|めん}`.
- Una lettura ruby conta solo quando copre ogni kanji del termine: `:index[行{取|ど}り]` lascia 行 senza lettura e avvisa. `{行取|ぎょう|ど}り` dà ぎょうどり.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Caratteri: Noto Serif JP (OFL-1.1), Noto Sans JP (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 079 · Un indice di nomi cinesi, per pinyin e per tratti](https://postext.dev/it/cookbook/chinese-name-index.md): Gli stessi segni :index in due edizioni di un’antologia del 紅樓夢: index.groupBy ordina i nomi per iniziale pinyin in zh-Hans e per numero di tratti in zh-Hant. · Livello 2 (Intermedio) · Narrativa, teatro e prosa letteraria
- [Nº 072 · Un indice analitico che segue il testo](https://postext.dev/it/cookbook/back-of-book-index.md): Termini marcati dove il testo ne parla, stampati da :::index su due colonne con le pagine trovate da buildBundle, intervalli e rimandi. · Livello 3 (Avanzato) · Libri di testo
- [Nº 073 · Un indice dei nomi e un indice degli argomenti](https://postext.dev/it/cookbook/names-and-subjects-indexes.md): I segni con index="names" formano l’indice delle persone, gli altri quello degli argomenti; :::index li stampa su due colonne, la costruzione fissa le pagine. · Livello 2 (Intermedio) · Libri di testo
