# Un articolo di ricerca giapponese citato secondo SIST 02

> Una nota di ricerca in giapponese su A4 a due colonne: citazioni [@key] numerate (1), (2, 3) secondo SIST 02, un elenco 参考文献, note a piè di pagina, 表1 e 図1.

- Versione HTML: https://postext.dev/it/cookbook/sist02-research-paper
- Ricetta Nº 125 · Struttura del libro · Livello 2 (Intermedio) · Output: Canvas, PDF
- Generi: Articoli scientifici e testi accademici
- Richiede postext ≥ 1.25.0, postext-pdf ≥ 1.25.0 · provata con 1.31.0, postext-pdf 1.31.0 il 2026-10-13
- Pagine: [23](https://postext.dev/cookbook/sist02-research-paper/en/p01.webp?v=70385cc1), [24](https://postext.dev/cookbook/sist02-research-paper/en/p02.webp?v=70385cc1)
- PDF: https://postext.dev/cookbook/sist02-research-paper/en/sist02-research-paper.pdf?v=70385cc1
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=sist02-research-paper&lang=en (.postext: https://postext.dev/cookbook/sist02-research-paper/en/sist02-research-paper.postext)
- Ultimo aggiornamento: 2026-10-11
- Altre lingue: [en](https://postext.dev/en/cookbook/sist02-research-paper.md), [es](https://postext.dev/es/cookbook/sist02-research-paper.md), [ca](https://postext.dev/ca/cookbook/sist02-research-paper.md), [de](https://postext.dev/de/cookbook/sist02-research-paper.md), [fr](https://postext.dev/fr/cookbook/sist02-research-paper.md), [pt](https://postext.dev/pt/cookbook/sist02-research-paper.md), [zh](https://postext.dev/zh/cookbook/sist02-research-paper.md), [ja](https://postext.dev/ja/cookbook/sist02-research-paper.md), [ar](https://postext.dev/ar/cookbook/sist02-research-paper.md)

## In breve

Un breve articolo di ricerca di una rivista giapponese. L’autrice scrive un codice per ogni fonte; Postext le numera nell’ordine in cui sono citate e stampa i riferimenti come li chiedono le riviste scientifiche giapponesi.

## Che cosa comporrai

Due pagine di una nota di ricerca, 号数活字の三つの系列 («Tre serie di corpi gōsū»), da una rivista inventata di caratteri e composizione, composta come le società accademiche giapponesi compongono le loro riviste: A4, due colonne di 25 caratteri per 45 righe di Noto Serif JP a 9 pt, il titolo in Shippori Mincho B1 sulla fascia verde della rivista. Le citazioni seguono SIST 02, la guida ai riferimenti bibliografici usata dalle riviste giapponesi di scienza e tecnologia: ogni opera è numerata nell’ordine in cui viene citata la prima volta, (1), (2, 3), e l’elenco sotto 参考文献 dà i nomi giapponesi con il cognome prima, il luogo prima dell’editore e l’anno per ultimo. Una tabella dei nove corpi, una figura che li disegna in scala e due note a piè di pagina hanno etichette giapponesi. [L’articolo cinese citato secondo GB/T 7714](https://postext.dev/it/cookbook/gbt7714-chinese-paper.md) è il suo corrispettivo cinese.

**Questa ricetta risponde a:**

- Come cito le opere in un articolo giapponese secondo SIST 02 e stampo un elenco 参考文献?
- Come compongo le note di un libro giapponese: （1） a fine capitolo, al piede della pagina o accanto al testo lungo la doppia pagina?

## La risposta breve

```js
// script.js, righe 32–45
// Register the engine once, before the first build. SIST 02, the standard Japanese science
// and technology journals cite to, numbers works in the order they are first cited; the
// ja-JP locale writes the list's words and joins authors with と and ほか. The document's
// own locale 'ja' gives the notes their Japanese defaults: at the foot of the column,
// numbered per page, a superscript marker and a rule a third of the column.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'sist02',
  locale: 'ja-JP',
  bibliography: { fontSize: em(8 / BODY), lineHeight: pt(13), entrySpacing: pt(1),
    labelWidth: em(2.2) }, // turnovers under the text of the entry, past the (n)
};
const footnotes = { fontSize: pt(7.5), lineHeight: pt(11.5), color: col('ink'),
  separator: { color: col('rule') } }; // placement, numbering and marker: the 'ja' defaults
```

## Ingredienti

**Insegna**

- [Citazioni in uno stile citazionale](https://postext.dev/it/docs/document-format.md#citazioni-e-bibliografia): Opere citate come [@key, p. 33] e formattate in uno stile CSL (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) scelto nelle impostazioni, collegate alle loro voci.
- [Bibliografia dai riferimenti](https://postext.dev/it/docs/document-format.md#citazioni-e-bibliografia): L’elenco delle opere citate, costruito dai riferimenti del documento (frontmatter o un blocco BibTeX) dove sta :::bibliography o dopo l’ultimo capitolo.
- [Note nei libri giapponesi](https://postext.dev/it/docs/japanese-layout.md#note): Le note come le compongono i libri giapponesi. Nel testo verticale il richiamo è （1） con cifre diritte a destra della riga e le note si raccolgono a fine capitolo (後注); nel testo orizzontale stanno al piede della pagina, numerate per pagina sotto un filetto lungo un terzo della giustezza. footnotes.placement: 'spread' compone note laterali (傍注) sul margine esterno della pagina sinistra di ogni doppia pagina, e footnotes.markerPosition: 'side' mette un piccolo richiamo interlineare accanto alla parola (合印).

**Usa anche**

- [Note a piè di pagina](https://postext.dev/it/docs/document-format.md#note-a-piè-di-pagina)
- [Didascalie numerate](https://postext.dev/it/docs/document-format.md#numerazione-al-primo-rimando)
- [Figura e Tabella nella tua lingua](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)
- [Stile delle tabelle](https://postext.dev/it/docs/configuration-resources.md#stile-delle-tabelle)
- [Stile delle didascalie](https://postext.dev/it/docs/configuration-resources.md#stile-delle-didascalie)
- [Figure e tabelle come risorse](https://postext.dev/it/docs/document-format.md#risorse)
- [Figure proprio qui](https://postext.dev/it/docs/document-format.md#inserimento-di-blocco-facoltativo-collocazione-in-linea-esplicita)
- [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)
- [Stili di titolo](https://postext.dev/it/docs/configuration-styles.md#stili-dei-titoli)
- [Font cinesi, giapponesi e coreani](https://postext.dev/it/docs/configuration-programmatic-usage.md#font-cinesi-giapponesi-e-coreani)
- [Chip in linea](https://postext.dev/it/docs/configuration-styles.md#stili-dei-chip)
- [Testatine e folio](https://postext.dev/it/docs/configuration-page-layout.md#intestazioni-e-piè-di-pagina)
- [Esportazione in PDF](https://postext.dev/it/docs/configuration-programmatic-usage.md#generare-pdf)
- [Riquadri](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri)
- [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)
- [Rimandi](https://postext.dev/it/docs/document-format.md#rimandi-e-ancore)
- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Font incorporati nel PDF](https://postext.dev/it/docs/configuration-programmatic-usage.md#perché-un-provider-di-font)
- [Tipi di risorsa personalizzati](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)
- [Pagine preliminari in numeri romani](https://postext.dev/it/docs/document-format.md#numbering)
- [Tabelle dai dati](https://postext.dev/it/docs/document-format.md#inserimento-di-blocco-facoltativo-collocazione-in-linea-esplicita)
- [A capo nei titoli](https://postext.dev/it/docs/document-format.md#a-capo-nei-titoli)
- [Capitoli non numerati](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), [`calloutStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri), [`captionStyle`](https://postext.dev/it/docs/configuration-resources.md#stile-delle-didascalie), [`chipStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-dei-chip), [`citations`](https://postext.dev/it/docs/configuration-notes-references.md#citazioni), [`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), `footnotes`, [`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), [`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), [`resourceTypes`](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa), [`tableStyle`](https://postext.dev/it/docs/configuration-resources.md#stile-delle-tabelle)

**API**

- [`LOCALES`](https://postext.dev/it/docs/document-format.md#citazioni-e-bibliografia), [`STYLES`](https://postext.dev/it/docs/document-format.md#citazioni-e-bibliografia), [`buildDocument`](https://postext.dev/it/docs/configuration-programmatic-usage.md#costruire-un-documento), [`createCiteprocEngine`](https://postext.dev/it/docs/document-format.md#citazioni-e-bibliografia), [`decompressWoff2`](https://postext.dev/it/docs/configuration-programmatic-usage.md#provider-di-font-per-il-browser-fontsource--woff2), [`defaultResourceTypes`](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa), [`inlineSvgFonts`](https://postext.dev/it/docs/configuration-resources.md#font-nel-testo-svg), [`parseTSV`](https://postext.dev/it/docs/document-format.md#inserimento-di-blocco-facoltativo-collocazione-in-linea-esplicita), [`registerCitationEngine`](https://postext.dev/it/docs/document-format.md#citazioni-e-bibliografia), [`registerResourceImage`](https://postext.dev/it/docs/architecture.md#superficie-dellapi), [`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), Shippori Mincho B1 (OFL-1.1)

## Preparazione

### 1 · Lo stile, il locale e le note

Il codice è nella [risposta breve](https://postext.dev/it/cookbook/sist02-research-paper.md#la-risposta-breve) qui sopra. `postext-citeproc` include SIST 02 come `'sist02'` e il locale CSL `ja-JP`, che scrive in giapponese i termini dell’elenco e in una citazione narrativa unisce due autori giapponesi con と e più autori con ほか ([Citazioni](https://postext.dev/it/docs/configuration-notes-references.md#citazioni)). Nel testo, `[@kawada1949; @suzuki2015]` stampa (2, 3), e `高橋[@takahashi2012]が` mette il numero dopo il nome, 高橋(8)が, che la pagina compone con un quarto di em per lato attorno alle parentesi, come fa attorno a qualsiasi parola occidentale nel testo giapponese. Lo stile si distingue dal cinese GB/T 7714 per il richiamo: SIST 02 compone il numero tra parentesi tonde sulla riga, GB/T 7714 tra parentesi quadre in apice. Le note non richiedono nulla: `locale: 'ja'` dà a un documento orizzontale note al piede della colonna, numerate da 1 a ogni pagina, con un richiamo in apice e un filetto lungo un terzo della colonna.

### 2 · I riferimenti in un blocco CSL-YAML

Le opere sono elencate alla fine del Markdown in `:::references{format=csl-yaml}`, il formato che Zotero esporta come CSL YAML ([Citare](https://postext.dev/it/docs/document-format.md#citare)). I nomi giapponesi si inseriscono come `family` e `given`, `{family: 鈴木, given: 広光}`, e SIST 02 li stampa uniti, 鈴木広光, mentre Kornicki mantiene la sua virgola: Kornicki, Peter. Una collana va in `collection-title` e si stampa tra parentesi dopo l’anno; i due articoli di 日本印刷学会誌 danno volume, numero e pagine come vol. 49, no. 2, p. 76–80. Tutte le opere sono reali; le loro voci si possono verificare su CiNii.

![Page 24: 4. ポイントへの移行. 図1 号数活字の三つの系列。上から初号系、一号系、三号系。正 方形は旧号数の大きさを実寸で示す.](https://postext.dev/cookbook/sist02-research-paper/en/p02.webp?v=70385cc1)

*Pagina 24: le voci da (3) a (9), nomi giapponesi con il cognome prima, il luogo prima dell’editore, l’anno per ultimo.*

### 3 · Il titolo sulla fascia della rivista

```js
// script.js, righe 49–71
const BAND = 78; // mm from the trim's top: the title sits on the journal's green
const at = (id, y) => ({ anchor: { to: `#${id}`, edge: 'below' }, offset: { y: mm(y) },
  size: { width: mm(AREA) } });
const line = (id, content, family, size, color, placement, extra) => ({ kind: 'text', id,
  content, fontFamily: family, fontSize: pt(size), color: col(color), align: 'center',
  overflow: 'wrap', placement, ...extra });
const top = (y) => ({ anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(y) },
  size: { width: mm(AREA) } });
const masthead = { enabled: true, minHeight: pt(10 * PITCH), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('green') },
    placement: { anchor: { to: 'page', edge: 'top-left' },
      size: { width: 'fill', height: mm(BAND) } } },
  line('journal', '活字と組版の研究', GOTHIC, 13, 'paper', top(-13), { fontWeight: 700,
    align: 'left', letterSpacing: pt(2) }),
  line('issue', '第12巻 第2号（2026）', GOTHIC, 8, 'mist', top(-12), { align: 'right' }),
  line('kind', '{attr.kind}', GOTHIC, 8, 'mist', top(2), { fontWeight: 700,
    letterSpacing: pt(2) }),
  line('title', '{titleText}', TITLE, 20, 'paper', at('kind', 3), { fontWeight: 700,
    lineHeight: 1.45 }),
  line('english', '{attr.english}', MINCHO, 8.5, 'mist', at('title', 3)),
  line('author', '{attr.authors}', MINCHO, 11, 'ink', top(BAND - 16)),
  line('affiliation', '{attr.affiliation}', MINCHO, 8, 'muted', at('author', 1.5)),
] } };
```

Il blocco del titolo è la grafica dell’H1, uno stile di titolo su entrambe le colonne. La fascia è un riquadro ancorato al rifilo; l’etichetta, il titolo e la sua traduzione inglese ci stanno sopra, ciascuno collocato sotto il precedente, così un titolo su tre righe spinge in basso la riga inglese. L’autrice e la sua affiliazione vengono da attributi del titolo e stanno sotto la fascia.

### 4 · Il riassunto e le sue etichette

```js
// script.js, righe 75–82
const calloutStyles = [{ id: 'abstract', span: 'page', backgroundEnabled: false,
  stripe: { enabled: true, side: 'top', width: pt(1.5), color: col('green') },
  padding: { top: mm(2.5), bottom: mm(2.5), left: em(2), right: em(2) },
  marginTop: pt(0), marginBottom: pt(PITCH),
  body: { fontFamily: MINCHO, fontSize: pt(8.5), lineHeight: pt(14), firstLineIndent: pt(0),
    textAlign: 'justify' } }];
const chipStyles = [{ id: 'label', fontFamily: GOTHIC, bold: true, color: col('green'),
  backgroundEnabled: false, borderWidth: pt(0), paddingX: em(0), gap: em(1) }];
```

Il riassunto è un riquadro a tutta larghezza sotto una banda superiore verde, con le etichette come chip in gotico grassetto nel colore della rivista. Uno spazio ideografico dopo ogni etichetta, scritto nel Markdown, tiene 要　旨 staccato dal testo.

### 5 · 表1 e 図1 dove sono citati

```js
// script.js, righe 278–300
const SIZES = `号数\t旧号数（pt）\t新号数（pt）\t系列
初号\t42\t42\t初号系
一号\t27.5\t26.25\t一号系
二号\t21\t21\t初号系
三号\t16\t15.75\t三号系
四号\t13.75\t13.125\t一号系
五号\t10.5\t10.5\t初号系
六号\t8\t7.875\t三号系
七号\t5.25\t5.25\t初号系
八号\t4\t3.9375\t三号系`;
const SERIES = [[42, 21, 10.5, 5.25], [27.5, 13.75], [16, 8, 4]]; // old values, pt
const resources = [
  { id: 'tbl:sizes', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
    placement: { position: 'here' },
    caption: '号数活字の大きさ。新号数はJIS Z 8305（1962）による',
    table: { model: { ...parseTSV(SIZES), headerRowCount: 1, columnWidths: [16, 28, 28, 28] } } },
  { id: 'fig:series', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
    placement: { position: 'here' },
    caption: '号数活字の三つの系列。上から初号系、一号系、三号系。正方形は旧号数の大きさを実寸で示す',
    altText: 'Three rows of squares drawn to scale: 42, 21, 10.5 and 5.25 pt; 27.5 and 13.75 pt; '
      + '16, 8 and 4 pt. Each square is half the one before it.',
    svg: { fileId: 'series.svg', width: 790, height: 520 } },
];
```

`defaultResourceTypes('ja')` dà 図 e 表; l’articolo li numera in modo continuo, 表1 e 図1, non per capitolo, quindi il modello è `'{n}'`. Entrambi sono collocati `'here'`, sotto il paragrafo che li cita, dentro la colonna. I quadrati della figura sono disegnati alla misura del corpo che rappresentano, 42 pt per il 初号 fino a 4 pt per l’八号, così il dimezzamento si vede sulla pagina.

## 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/sist02-research-paper

### script.js

```js
// ═══ Postext Cookbook · Nº 125 · A Japanese research paper cited to SIST 02 ═══════════
// https://postext.dev/en/cookbook/sist02-research-paper
// Code: MIT · Text: original (CC BY 4.0) · Pictures: drawn in code
// Fonts: Noto Serif JP, Noto Sans JP, Shippori Mincho B1 (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, registerCitationEngine, defaultResourceTypes,
  parseTSV, registerResourceImage, inlineSvgFonts,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'en'; // @lang: the language of the frame; the paper is Japanese in both
const RECIPE = 'sist02-research-paper';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: black text, one deep green for the journal, its rules and its labels
const palette = {
  ink: '#1b1d1c', green: '#1f5a46', mist: '#cfe0d8', rule: '#a7b3ad', muted: '#5d6662',
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.green })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const [MINCHO, GOTHIC, TITLE] = ['Noto Serif JP', 'Noto Sans JP', 'Shippori Mincho B1'];
const [BODY, PITCH] = [9, 15.5]; // pt: 9 pt text on a 15.5 pt line
const [CHARS, LINES] = [25, 45]; // characters to a column's line, lines to a column
const PT = 25.4 / 72; // mm in a point
const AREA = (2 * CHARS + 2) * BODY * PT; // mm: two columns and a 2-em gutter, 165

// #region answer: SIST 02 in Japanese: numbered citations, the list under 参考文献
// Register the engine once, before the first build. SIST 02, the standard Japanese science
// and technology journals cite to, numbers works in the order they are first cited; the
// ja-JP locale writes the list's words and joins authors with と and ほか. The document's
// own locale 'ja' gives the notes their Japanese defaults: at the foot of the column,
// numbered per page, a superscript marker and a rule a third of the column.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
  style: 'sist02',
  locale: 'ja-JP',
  bibliography: { fontSize: em(8 / BODY), lineHeight: pt(13), entrySpacing: pt(1),
    labelWidth: em(2.2) }, // turnovers under the text of the entry, past the (n)
};
const footnotes = { fontSize: pt(7.5), lineHeight: pt(11.5), color: col('ink'),
  separator: { color: col('rule') } }; // placement, numbering and marker: the 'ja' defaults
// #endregion

// #region masthead: the journal, the paper's kind, its title and English title on a band
const BAND = 78; // mm from the trim's top: the title sits on the journal's green
const at = (id, y) => ({ anchor: { to: `#${id}`, edge: 'below' }, offset: { y: mm(y) },
  size: { width: mm(AREA) } });
const line = (id, content, family, size, color, placement, extra) => ({ kind: 'text', id,
  content, fontFamily: family, fontSize: pt(size), color: col(color), align: 'center',
  overflow: 'wrap', placement, ...extra });
const top = (y) => ({ anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(y) },
  size: { width: mm(AREA) } });
const masthead = { enabled: true, minHeight: pt(10 * PITCH), slot: { elements: [
  { kind: 'box', id: 'band', style: { backgroundColor: col('green') },
    placement: { anchor: { to: 'page', edge: 'top-left' },
      size: { width: 'fill', height: mm(BAND) } } },
  line('journal', '活字と組版の研究', GOTHIC, 13, 'paper', top(-13), { fontWeight: 700,
    align: 'left', letterSpacing: pt(2) }),
  line('issue', '第12巻 第2号（2026）', GOTHIC, 8, 'mist', top(-12), { align: 'right' }),
  line('kind', '{attr.kind}', GOTHIC, 8, 'mist', top(2), { fontWeight: 700,
    letterSpacing: pt(2) }),
  line('title', '{titleText}', TITLE, 20, 'paper', at('kind', 3), { fontWeight: 700,
    lineHeight: 1.45 }),
  line('english', '{attr.english}', MINCHO, 8.5, 'mist', at('title', 3)),
  line('author', '{attr.authors}', MINCHO, 11, 'ink', top(BAND - 16)),
  line('affiliation', '{attr.affiliation}', MINCHO, 8, 'muted', at('author', 1.5)),
] } };
// #endregion

// #region abstract: across both columns, its labels in gothic the colour of the journal
const calloutStyles = [{ id: 'abstract', span: 'page', backgroundEnabled: false,
  stripe: { enabled: true, side: 'top', width: pt(1.5), color: col('green') },
  padding: { top: mm(2.5), bottom: mm(2.5), left: em(2), right: em(2) },
  marginTop: pt(0), marginBottom: pt(PITCH),
  body: { fontFamily: MINCHO, fontSize: pt(8.5), lineHeight: pt(14), firstLineIndent: pt(0),
    textAlign: 'justify' } }];
const chipStyles = [{ id: 'label', fontFamily: GOTHIC, bold: true, color: col('green'),
  backgroundEnabled: false, borderWidth: pt(0), paddingX: em(0), gap: em(1) }];
// #endregion

// Running heads on the body pages: the journal on the verso, the author on the recto.
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(14) } }, ...extra });
const folio = { fontWeight: 700, color: col('green') };
const header = { elements: [
  head('v-folio', '{pageNumber}', 'even', 'top-left', 0, folio),
  head('v-head', '活字と組版の研究　第12巻 第2号', 'even', 'top-left', 9),
  head('r-head', '森川：号数活字の三つの系列', 'odd', 'top-right', -9),
  head('r-folio', '{pageNumber}', 'odd', 'top-right', 0, folio),
] };

const config = () => ({
  locale: 'ja', // written out, never LANG (gotcha: ja-locale-tag)
  resourceTypes: defaultResourceTypes('ja').map((type) => ({ ...type,
    numberingTemplate: '{n}' })), // 図1, 表1: a paper numbers through, not by chapter
  colorPalette,
  citations,
  footnotes,
  page: { sizePreset: 'custom', width: mm(210), height: mm(297), dpi: 150, // A4
    pageNumbering: { startAt: 23 },
    margins: { top: mm(24), bottom: mm(20), left: mm(18), right: mm(18), mirror: true } },
  layout: { layoutType: 'double', gutterWidth: pt(2 * BODY) },
  cjk: { grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES } },
  bodyText: { fontFamily: MINCHO, fontSize: pt(BODY), lineHeight: pt(PITCH), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    referenceBold: false, textAlign: 'justify', firstLineIndent: em(1), indentAfterHeading: true },
  headings: { fontFamily: GOTHIC, fontWeight: 700, color: col('ink'),
    // Off by default on a character grid. On, a short column takes whole grid lines above
    // its heads, and the two columns of the last page end level.
    balancing: { enabled: true },
    levels: [
      { level: 1, breakBefore: { enabled: true, parity: 'any' } }, // the next page, either side
      { level: 2, numberingTemplate: '{2}.', numberSeparator: '　', fontSize: pt(10),
        lineSpan: 2 }, // 2行取り: 1.　はじめに on two lines of the grid
    ] },
  headingStyles: [
    { id: 'article', numbered: false, span: 'page', advancedDesign: masthead },
    { id: 'references', numbered: false },
  ],
  calloutStyles,
  chipStyles,
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackground: col('green'), headerColor: col('paper'), headerFontFamily: GOTHIC,
    bodyFontFamily: MINCHO, bodyFontSize: pt(8), bodyColor: col('ink'), cellPadding: mm(1) },
  captionStyle: { fontFamily: GOTHIC, fontSize: pt(7.5), color: col('ink'), labelBold: true,
    labelColor: col('green') }, // 表1　…, the ja default
  paragraphStyles: [{ id: 'colophon', fontFamily: GOTHIC, fontSize: pt(6.5), lineHeight: pt(9),
    color: col('muted'), firstLineIndent: pt(0), textAlign: 'left', marginTop: pt(PITCH) }],
  header,
  footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "号数活字の三つの系列"
---

# 号数活字の三つの系列：\\近代日本の活字の大きさはどう決められたか {style="article" kind="研究ノート" authors="森川 啓" affiliation="架空大学 文学部" english="Three Series of Gōsū Type Sizes: How the Sizes of Modern Japanese Type Were Set"}

:::callout{type="abstract"}
:chip[要　旨]{style="label"}　明治初期に日本で鋳造が始まった金属活字は、初号から八号までの九つの大きさを「号数」で呼び分けた。本稿では、号数活字の大きさが倍と半分の関係で結ばれた三つの系列からなることを示し、その構造が組版の実務に何をもたらしたかを整理する。また、1962年に日本工業規格が号数をポイントの値で定め直した経緯と、いまも五号に当たる10.5ポイントが和文の標準の大きさとして残っている理由を考察する。

:chip[キーワード]{style="label"}　号数活字、ポイント、明朝体、東京築地活版製造所、JIS Z 8305
:::

## はじめに

日本語の活字印刷は、近世初期の古活字版のように早くから試みられてきたが[@kornicki1998]、出版の主流となったのは明治に入って金属活字が鋳造されてからである[@kawada1949; @suzuki2015]。その活字の大きさは、いまのようなポイントではなく、「号数」という独自の単位で呼ばれていた。

号数活字の名前は、組版や印刷の歴史を扱う文献にたびたび現れる[@yahagi1976; @komiyama2009]。しかし、九つの大きさがどのような関係で並んでいるのかは、個々の数値ほどには知られていない。本稿では、号数の大きさを系列ごとに整理し、その構造が組版に与えた影響を考える[^term]。

## 号数活字の成立

号数の体系は、1869年に長崎で本木昌造らが上海の美華書館の技術者から活字鋳造を学んだことに始まるとされる。その後、平野富二が東京築地活版製造所を設け、号数活字は同所の製品として全国に広まった[@itakura2006]。同所の活字見本は、近代日本の書体と字形を知るための基本資料となっている[@fukawa2005]。

## 三つの系列

号数活字の大きさを[@tbl:sizes]に示す。九つの大きさは、互いに倍と半分の関係で結ばれた三つの系列に分けられる。初号・二号・五号・七号の系列、一号・四号の系列、三号・六号・八号の系列である（[@fig:series]）。

::resource{id="tbl:sizes"}

この構造には実務上の利点があった。五号の本文に七号のルビを付けると、ルビの大きさはちょうど本文の半分になる。また、二号の文字一つは五号の文字二つ分の幅に当たるので、見出しの字数を本文の字数に換算して版面を割り付けやすい。号数の名前は、大きさの序列というより、この倍数関係を前提とした組版の道具立てだったといえる。

::resource{id="fig:series"}

## ポイントへの移行

東京築地活版製造所は、1903年にアメリカ式のポイント制による活字を売り出した。それでも号数活字は長く使われ続け、1962年の日本工業規格JIS Z 8305「活字の基準寸法」は、号数をポイントの値で定め直した。この「新号数」では、一号・三号・四号・六号・八号の値が旧来のものからわずかに変わっている[^jis]。

活字の製造方法の変遷については高橋[@takahashi2012]が、近代印刷の導入期の資料については水野[@mizuno2016]が概観している。

## おわりに

金属活字が使われなくなったいまも、号数の名残は身近に残っている。和文のワープロソフトが本文の既定の大きさとする10.5ポイントは、五号の値そのものである。号数活字の三つの系列は、日本語の組版が長く拠り所としてきた寸法の体系であった。

## 参考文献 {style="references"}

:::bibliography{title=""}

[^term]: 本稿でいう「活字」は、鉛合金で鋳造した金属活字を指す。

[^jis]: 旧号数の値は東京築地活版製造所の製品による。関東の印刷所の多くは、規格の制定後も旧号数の活字を使い続けた。

:::paragraphs{style="colophon"}
A specimen article written for the Postext Cookbook. The journal, its author and her university are fictitious; the works cited are real. Set in Noto Serif JP, Noto Sans JP and Shippori Mincho B1 (SIL OFL). Text: CC BY 4.0.
:::

:::references{format=csl-yaml}
- id: suzuki2015
  type: book
  language: ja
  author: [{family: 鈴木, given: 広光}]
  title: 日本語活字印刷史
  publisher: 名古屋大学出版会
  publisher-place: 名古屋
  issued: 2015
- id: yahagi1976
  type: book
  language: ja
  author: [{family: 矢作, given: 勝美}]
  title: 明朝活字：その歴史と現状
  publisher: 平凡社
  publisher-place: 東京
  issued: 1976
- id: komiyama2009
  type: book
  language: ja
  author: [{family: 小宮山, given: 博史}]
  title: 日本語活字ものがたり：草創期の人と書体
  publisher: 誠文堂新光社
  publisher-place: 東京
  collection-title: 文字と組版ライブラリ
  issued: 2009
- id: itakura2006
  type: book
  language: ja
  author: [{family: 板倉, given: 雅宣}]
  title: 活版印刷発達史：東京築地活版製造所の果たした役割
  publisher: 印刷朝陽会
  publisher-place: 東京
  issued: 2006
- id: fukawa2005
  type: book
  language: ja
  author: [{family: 府川, given: 充男}]
  title: 聚珍録：圖説=近世・近代日本〈文字-印刷〉文化史
  publisher: 三省堂
  publisher-place: 東京
  issued: 2005
- id: kawada1949
  type: book
  language: ja
  author: [{family: 川田, given: 久長}]
  title: 活版印刷史：日本活版印刷史の研究
  publisher: 印刷学会出版部
  publisher-place: 東京
  issued: 1949
- id: kornicki1998
  type: book
  language: en
  author: [{family: Kornicki, given: Peter}]
  title: "The book in Japan: a cultural history from the beginnings to the nineteenth century"
  publisher: Brill
  publisher-place: Leiden
  issued: 1998
- id: takahashi2012
  type: article-journal
  language: ja
  author: [{family: 高橋, given: 恭介}]
  title: 「活字」の技術変遷から見た印刷の歴史
  container-title: 日本印刷学会誌
  volume: 49
  issue: 2
  page: 76-80
  issued: 2012
  DOI: 10.11413/nig.49.076
- id: mizuno2016
  type: article-journal
  language: ja
  author: [{family: 水野, given: 雅生}]
  title: 日本近代印刷事始
  container-title: 日本印刷学会誌
  volume: 53
  issue: 2
  page: 104-108
  issued: 2016
  DOI: 10.11413/nig.53.104
:::
`; // content.<lang>.md: the same Japanese paper in both

// #region resources: the table of sizes, and the three series drawn to scale
const SIZES = `号数\t旧号数（pt）\t新号数（pt）\t系列
初号\t42\t42\t初号系
一号\t27.5\t26.25\t一号系
二号\t21\t21\t初号系
三号\t16\t15.75\t三号系
四号\t13.75\t13.125\t一号系
五号\t10.5\t10.5\t初号系
六号\t8\t7.875\t三号系
七号\t5.25\t5.25\t初号系
八号\t4\t3.9375\t三号系`;
const SERIES = [[42, 21, 10.5, 5.25], [27.5, 13.75], [16, 8, 4]]; // old values, pt
const resources = [
  { id: 'tbl:sizes', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0,
    placement: { position: 'here' },
    caption: '号数活字の大きさ。新号数はJIS Z 8305（1962）による',
    table: { model: { ...parseTSV(SIZES), headerRowCount: 1, columnWidths: [16, 28, 28, 28] } } },
  { id: 'fig:series', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
    placement: { position: 'here' },
    caption: '号数活字の三つの系列。上から初号系、一号系、三号系。正方形は旧号数の大きさを実寸で示す',
    altText: 'Three rows of squares drawn to scale: 42, 21, 10.5 and 5.25 pt; 27.5 and 13.75 pt; '
      + '16, 8 and 4 pt. Each square is half the one before it.',
    svg: { fileId: 'series.svg', width: 790, height: 520 } },
];
// #endregion

// #region art: squares at their size in points; the labels name the gothic, loadSvg embeds it
const LABELS = `text{font-family:"${GOTHIC}";font-size:2.4px;text-anchor:middle;`
  + `fill:${palette.muted}}`;
function seriesArt() {
  let out = '';
  SERIES.forEach((row, r) => {
    let x = 2;
    const base = [17, 33, 47][r]; // mm: the squares of a row stand on one line
    for (const size of row) {
      const s = size * PT; // a square as wide as the type body
      out += `<rect x="${x}" y="${base - s}" width="${s}" height="${s}" fill="${palette.mist}" `
        + `stroke="${palette.green}" stroke-width=".25"/><text x="${x + s / 2}" y="${base + 3}">`
        + `${size}</text>`;
      x += s + 4;
    }
  });
  return `<svg xmlns="http://www.w3.org/2000/svg" width="790" height="520" viewBox="0 0 79 52">`
    + `<style>${LABELS}</style>${out}</svg>`;
}
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // every face the pages use, loaded before the build
  'Noto Serif JP': ['400'], // 明朝: text, abstract, notes, references, author
  'Noto Sans JP': ['400', '700'], // ゴシック: journal, heads, labels, captions, table head
  'Shippori Mincho B1': ['700'], // the title, a display mincho
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
// Each voice loads the files of what it sets (gotcha: cjk-fonts-slices); the text face also
// gets the words SIST 02 adds to the list.
const all = (re) => (markdown.match(re) ?? []).join('');
const gothic = `${all(/^#+ .*$/gm)}${all(/:chip\[[^\]]*\]/g)}${SIZES}`
  + `${resources.map((r) => r.caption).join('')}活字と組版の研究第巻号森川：三つの系列0123456789`;
await loadFonts(FONTS, markdown);
await loadCjkFonts({ [MINCHO]: FONTS[MINCHO] }, `${markdown}${SIZES}ほかと編巻号頁`);
await loadCjkFonts({ [GOTHIC]: FONTS[GOTHIC] }, gothic);
await loadCjkFonts({ [TITLE]: FONTS[TITLE] }, all(/^# .*$/gm));
await loadSvg('series.svg', seriesArt());
const doc = await withLoadedFonts(() => buildDocument({ markdown, resources }, config()),
  { ...kitFonts(FONTS), text: markdown });
showPages(doc, { title: t({ en: 'A Japanese paper cited to SIST 02',
  es: 'Un artículo japonés citado según SIST 02' }) });
offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider, resourceBytes: imageBytes }),
  `${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 · images v3 ── recipes with pictures · postext.dev/cookbook
/** Registers a photo or PNG for the canvas and keeps its bytes for the PDF.
 *  fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */
async function loadImage(fileId, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`);
  const bytes = new Uint8Array(await res.arrayBuffer());
  registerResourceImage(fileId, await createImageBitmap(new Blob([bytes])));
  (loadImage.bytes ??= new Map()).set(fileId, bytes);
}

/** Registers SVG markup as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25).
 *  `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */
async function loadSvg(fileId, svg, options) {
  if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces);
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img, options);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

/** A face's Fontsource files: nearest weight; latin + what the SVG sets. */
async function svgFaces(family, weight, style, { codePoints = [] } = {}) {
  const [id, m] = [fontsourceId(family), await fontsourceMeta(family)];
  const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight;
  const s = m?.styles.includes(style) === false ? 'normal' : style;
  return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array(
    await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer())));
}

/** renderToPdf({ resourceBytes: imageBytes }) */
function imageBytes(fileId) { return loadImage.bytes?.get(fileId); }

/** renderToHtml({ resourceImageUrl: imageUrl }) */
function imageUrl(fileId) {
  const bytes = imageBytes(fileId);
  if (!bytes) return undefined;
  imageUrl.urls ??= new Map();
  if (!imageUrl.urls.has(fileId)) {
    const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg';
    imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type })));
  }
  return imageUrl.urls.get(fileId);
}

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

### Numeri di citazione in apice

Molte riviste giapponesi alzano il numero sopra la riga. `marker: 'superscript'` lo stampa in apice, senza parentesi, e lascia stare l’elenco.

```diff
   locale: 'ja-JP',
+  marker: 'superscript',
```

### Note numerate lungo tutto l’articolo

Un articolo di poche pagine può contare le note da 1 fino alla fine invece che pagina per pagina.

```diff
-const footnotes = { fontSize: pt(7.5), lineHeight: pt(11.5), color: col('ink'),
+const footnotes = { numbering: 'document', fontSize: pt(7.5), lineHeight: pt(11.5),
+  color: col('ink'),
```

## 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.

- Scrivi un nome giapponese come `family` e `given`, o come un unico `literal` senza spazi. Un nome scritto come una sola stringa con uno spazio tra cognome e nome, come spesso lo porta BibTeX (`author = {夏目 漱石}`), viene letto con il cognome prima, ma uno romanizzato, `Natsume Soseki`, viene letto come nome e poi cognome, come i nomi inglesi.
- Il SIST 02 incluso non stampa il DOI; un articolo il cui DOI conta deve riportarlo nel testo o in una nota.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Immagini: Figure 1, the three series of type sizes, drawn in code to scale: Postext Cookbook, CC-BY-4.0
- Caratteri: Noto Serif JP (OFL-1.1), Noto Sans JP (OFL-1.1), Shippori Mincho B1 (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 090 · Un articolo cinese citato secondo GB/T 7714](https://postext.dev/it/cookbook/gbt7714-chinese-paper.md): Un articolo di rivista cinese su due colonne: le citazioni [@key] si stampano in apice come [1] e [2–4], i riferimenti portano [M], [J], [D] e [EB/OL]. · Livello 2 (Intermedio) · Articoli scientifici e testi accademici
- [Nº 107 · Un articolo scientifico arabo con note, citazioni e indice](https://postext.dev/it/cookbook/arabic-research-article.md): Un articolo di rivista in arabo: note «(١)» numerate per pagina, citazioni CSL tra le note con la locale araba e un indice ordinato senza l’articolo ال. · Livello 3 (Avanzato) · Articoli scientifici e testi accademici
- [Nº 087 · Un capitolo di tesi con citazioni APA 7](https://postext.dev/it/cookbook/apa-thesis-with-bibtex.md): Un capitolo di dottorato le cui citazioni [@key, p. 33] diventano APA 7 con citeproc-js, con la bibliografia costruita da un blocco BibTeX. · Livello 2 (Intermedio) · Articoli scientifici e testi accademici, Rapporti
