# A Japanese research paper cited to SIST 02

> A two-column A4 research note in Japanese: [@key] citations numbered (1), (2, 3) to SIST 02, a 参考文献 list in Japanese, page notes, 表1 and 図1.

- HTML version: https://postext.dev/en/cookbook/sist02-research-paper
- Recipe Nº 125 · Book structure · Level 2 (Intermediate) · Outputs: Canvas, PDF
- Genres: Papers & academic
- Requires postext ≥ 1.16.1, postext-pdf ≥ 1.16.1 · tested with 1.16.1, postext-pdf 1.16.1 on 2026-10-05
- Pages: [23](https://postext.dev/cookbook/sist02-research-paper/en/p01.webp?v=58f91766), [24](https://postext.dev/cookbook/sist02-research-paper/en/p02.webp?v=58f91766)
- PDF: https://postext.dev/cookbook/sist02-research-paper/en/sist02-research-paper.pdf?v=58f91766
- Open in Sandbox: https://postext.dev/en/sandbox#recipe=sist02-research-paper&lang=en (.postext: https://postext.dev/cookbook/sist02-research-paper/en/sist02-research-paper.postext)
- Last updated: 2026-10-05
- Other languages: [es](https://postext.dev/es/cookbook/sist02-research-paper.md), [ca](https://postext.dev/ca/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 short

A short research article from a Japanese journal. The author writes a code for each source; Postext numbers them in the order they are cited and prints the reference list the way Japanese science journals ask for it.

## What you'll build

Two pages of a research note, 号数活字の三つの系列 (“Three series of gōsū type sizes”), from an invented journal of type and typesetting, set as Japanese academic societies set their journals: A4, two columns of 25 characters by 45 lines of Noto Serif JP at 9 pt, the title in Shippori Mincho B1 on the journal’s green band. The citations follow SIST 02, the guide to references that Japanese science and technology journals use: each work is numbered in the order it is first cited, (1), (2, 3), and the list under 参考文献 gives Japanese names family first, the place before the publisher and the year last. A table of the nine type sizes, a figure drawing them to scale and two page notes come with Japanese labels. [The Chinese paper cited to GB/T 7714](https://postext.dev/en/cookbook/gbt7714-chinese-paper.md) is its Chinese counterpart.

**This recipe answers:**

- How do I cite works in a Japanese paper to SIST 02 and print a 参考文献 list?
- How do I set the notes of a Japanese book: （1） at the chapter end, at the page foot or beside the text across the spread?

## The short answer

SIST 02 in Japanese: numbered citations, the list under 参考文献.

```js
// script.js, lines 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
```

## Ingredients

**Teaches**

- [Citations in a citation style](https://postext.dev/en/docs/document-format.md#citations-and-bibliography): Works cited as [@key, p. 33] and formatted in a CSL style (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) chosen in the settings, linked to their entries.
- [Bibliography from the references](https://postext.dev/en/docs/document-format.md#citations-and-bibliography): The list of works cited, built from the document's references (front matter or a BibTeX block) where :::bibliography stands or after the last chapter.
- [Notes in Japanese books](https://postext.dev/en/docs/japanese-layout.md#notes): Notes as Japanese books set them. In vertical text the marker is （1） with upright digits at the right of the line and the notes gather at the chapter end (後注); in horizontal text they sit at the page foot, numbered per page under a rule a third of the measure. footnotes.placement: 'spread' sets sidenotes (傍注) at the fore edge of each spread's left page, and footnotes.markerPosition: 'side' puts a small interlinear marker beside the word (合印).

**Also uses**

- [Footnotes](https://postext.dev/en/docs/document-format.md#footnotes)
- [Numbered captions](https://postext.dev/en/docs/document-format.md#first-reference-numbering)
- [Figure and Table in your language](https://postext.dev/en/docs/configuration.md#resource-types)
- [Table style](https://postext.dev/en/docs/configuration.md#table-style)
- [Caption style](https://postext.dev/en/docs/configuration.md#caption-style)
- [Figures and tables as resources](https://postext.dev/en/docs/document-format.md#resources)
- [Figures exactly here](https://postext.dev/en/docs/document-format.md#block-embed-optional-explicit-inline-placement)
- [Numbered headings](https://postext.dev/en/docs/configuration.md#per-level-overrides)
- [Headings over body lines (gyōdori)](https://postext.dev/en/docs/japanese-layout.md#headings-and-blocks)
- [One or two columns](https://postext.dev/en/docs/configuration.md#layout-types)
- [Character grid](https://postext.dev/en/docs/configuration.md#character-grid)
- [Designed openers](https://postext.dev/en/docs/configuration.md#span-and-advanced-design)
- [Heading styles](https://postext.dev/en/docs/configuration.md#heading-styles)
- [Chinese, Japanese and Korean fonts](https://postext.dev/en/docs/configuration.md#chinese-japanese-and-korean-fonts)
- [Inline chips](https://postext.dev/en/docs/configuration.md#chip-styles)
- [Running heads and folios](https://postext.dev/en/docs/configuration.md#headers--footers)
- [PDF export](https://postext.dev/en/docs/configuration.md#generating-pdfs)
- [Callout boxes](https://postext.dev/en/docs/configuration.md#callout-styles)
- [Chinese line breaking](https://postext.dev/en/docs/configuration.md#east-asian-typography)
- [Column balancing](https://postext.dev/en/docs/configuration.md#column-balancing)
- [Cross-references](https://postext.dev/en/docs/document-format.md#cross-references-and-anchors)
- [Heading attributes](https://postext.dev/en/docs/document-format.md#heading-attributes)
- [Heads by page role](https://postext.dev/en/docs/configuration.md#text-elements)
- [Paragraph styles](https://postext.dev/en/docs/configuration.md#paragraph-styles)
- [Fonts embedded in the PDF](https://postext.dev/en/docs/configuration.md#why-a-font-provider)
- [Custom resource types](https://postext.dev/en/docs/configuration.md#resource-types)
- [Roman front matter](https://postext.dev/en/docs/document-format.md#numbering)
- [Tables from data](https://postext.dev/en/docs/document-format.md#block-embed-optional-explicit-inline-placement)
- [Line breaks in titles](https://postext.dev/en/docs/document-format.md#line-breaks-in-titles)
- [Unnumbered chapters](https://postext.dev/en/docs/configuration.md#heading-styles)

**Config at a glance**

- [`bodyText`](https://postext.dev/en/docs/configuration.md#body-text), [`calloutStyles`](https://postext.dev/en/docs/configuration.md#callout-styles), [`captionStyle`](https://postext.dev/en/docs/configuration.md#caption-style), [`chipStyles`](https://postext.dev/en/docs/configuration.md#chip-styles), [`citations`](https://postext.dev/en/docs/configuration.md#citations), [`cjk`](https://postext.dev/en/docs/configuration.md#east-asian-typography), [`colorPalette`](https://postext.dev/en/docs/configuration.md#color-palette), [`footer`](https://postext.dev/en/docs/configuration.md#headers--footers), `footnotes`, [`header`](https://postext.dev/en/docs/configuration.md#headers--footers), [`headingStyles`](https://postext.dev/en/docs/configuration.md#heading-styles), [`headings`](https://postext.dev/en/docs/configuration.md#headings), [`layout`](https://postext.dev/en/docs/configuration.md#layout), [`locale`](https://postext.dev/en/docs/configuration.md#hyphenation), [`page`](https://postext.dev/en/docs/configuration.md#page), [`paragraphStyles`](https://postext.dev/en/docs/configuration.md#paragraph-styles), [`resourceTypes`](https://postext.dev/en/docs/configuration.md#resource-types), [`tableStyle`](https://postext.dev/en/docs/configuration.md#table-style)

**APIs**

- [`LOCALES`](https://postext.dev/en/docs/document-format.md#citations-and-bibliography), [`STYLES`](https://postext.dev/en/docs/document-format.md#citations-and-bibliography), [`buildDocument`](https://postext.dev/en/docs/configuration.md#building-a-document), [`clearMeasurementCache`](https://postext.dev/en/docs/configuration.md#measurement-cache), [`createCiteprocEngine`](https://postext.dev/en/docs/document-format.md#citations-and-bibliography), [`decompressWoff2`](https://postext.dev/en/docs/configuration.md#browser-font-provider-fontsource--woff2), [`defaultResourceTypes`](https://postext.dev/en/docs/configuration.md#resource-types), [`parseTSV`](https://postext.dev/en/docs/document-format.md#block-embed-optional-explicit-inline-placement), [`registerCitationEngine`](https://postext.dev/en/docs/document-format.md#citations-and-bibliography), [`registerResourceImage`](https://postext.dev/en/docs/architecture.md#api-surface), [`renderPageToCanvas`](https://postext.dev/en/docs/configuration.md#rendering-a-page-to-a-bitmap), [`renderToPdf`](https://postext.dev/en/docs/configuration.md#generating-pdfs)

**Typefaces**

- Noto Serif JP (OFL-1.1), Noto Sans JP (OFL-1.1), Shippori Mincho B1 (OFL-1.1)

## Method

### 1 · The style, the locale and the notes

The code is [the short answer](#the-short-answer) above. `postext-citeproc` ships SIST 02 as `'sist02'` and the CSL locale `ja-JP`, which writes the list’s terms in Japanese and joins two Japanese authors with と and more with ほか in a narrative citation ([Citations](/en/docs/configuration#citations)). In the text, `[@kawada1949; @suzuki2015]` prints (2, 3), and `高橋[@takahashi2012]が` puts the number after the name, 高橋(8)が. The style differs from the Chinese GB/T 7714 in the marker: SIST 02 sets the number in parentheses on the line, GB/T 7714 in square brackets raised. The notes need nothing: `locale: 'ja'` gives a horizontal document notes at the foot of the column, numbered from 1 on each page, with a superscript marker and a rule a third of the column long.

### 2 · The references in a CSL-YAML block

The works are listed at the end of the Markdown in `:::references{format=csl-yaml}`, the format Zotero exports as CSL YAML ([Citing](/en/docs/document-format#citing)). Japanese names go in as `family` and `given`, `{family: 鈴木, given: 広光}`, and SIST 02 prints them solid, 鈴木広光, while Kornicki keeps his comma: Kornicki, Peter. A book series goes in `collection-title` and prints in parentheses after the year; the two articles from 日本印刷学会誌 give volume, issue and pages as vol. 49, no. 2, p. 76–80. Every work is real; their entries can be checked in CiNii.

![Page 24: 4. ポイントへの移行. 図1 号数活字の三つの系列。上から初号系、一号系、三号系。正 方形は旧号数の大きさを実寸で示す. Page 24: in the left column, three rows of squares drawn to scale, 42 to 5.25 pt, 27.5 and 13.75 pt, 16 to 4 pt, captioned 図1; sections 4 and 5; the heading 参考文献 and entries (1) and (2). In the right column, entries (3) to (9), Japanese books and two articles from 日本印刷学会誌, and a colophon in Latin type.](https://postext.dev/cookbook/sist02-research-paper/en/p02.webp?v=58f91766)

*Page 24: entries (3) to (9), Japanese names family first, place before publisher, the year last.*

### 3 · The title on the journal’s band

```js
// script.js, lines 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)),
] } };
```

The title block is the design of the H1, a heading style across both columns. The band is a box anchored to the trim; the label, the title and its English translation sit on it, each placed under the one before, so a three-line title pushes the English line down. The author and her affiliation come from heading attributes and sit under the band.

### 4 · The abstract and its labels

```js
// script.js, lines 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) }];
```

The abstract is a callout across the page under a green top stripe, its labels chips in bold gothic in the journal’s colour. An ideographic space after each label, typed in the Markdown, keeps 要　旨 apart from the text.

### 5 · 表1 and 図1 where they are cited

```js
// script.js, lines 276–298
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')` gives 図 and 表; the paper numbers them through, 表1 and 図1, not by chapter, so the template is `'{n}'`. Both are placed `'here'`, under the paragraph that cites them, inside the column. The squares of the figure are drawn at the size of the type they stand for, 42 pt for 初号 down to 4 pt for 八号, so the halving can be seen on the page.

## The whole recipe

One file, composed from the recipe's folder with the sample text and the Cookbook's shared kit inlined; it builds its own page. To run it, put it in a `<script type="module">` on an empty page, or paste it into the JS panel of a new CodePen (as a module). It imports postext from esm.sh, so there is nothing to install or build.

- Source folder: 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.16.1
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
  defaultResourceTypes, parseTSV, registerResourceImage,
} 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 = () => ({ // a factory: the engine caches resolved configs per object
  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'),
    balancing: { enabled: false }, // heads stay on the grid
    levels: [
      { level: 1, breakBefore: { enabled: true, parity: 'any' } }, // headings-drop-h1-break
      { 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, labelled in the gothic's latin file
async function labelFace() {
  const url = 'https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5/files/'
    + 'noto-sans-jp-latin-400-normal.woff2';
  const bytes = new Uint8Array(await (await fetch(url)).arrayBuffer());
  let bin = '';
  for (let i = 0; i < bytes.length; i += 8192) {
    bin += String.fromCharCode(...bytes.subarray(i, i + 8192));
  }
  return `@font-face{font-family:L;src:url(data:font/woff2;base64,${btoa(bin)}) format('woff2')}`
    + `text{font-family:L;font-size:2.4px;text-anchor:middle;fill:${palette.muted}}`;
}
function seriesArt(style) {
  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>${style}</style>${out}</svg>`;
}
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { // every face the pages use, loaded before the build (gotcha: fonts-first)
  '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(await labelFace()));
const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()),
  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 v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,'
      + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD',
    'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,'
      + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`;
        const face = new FontFace(family, `url(${url}) format('woff2')`,
          { weight: String(weight), style, unicodeRange: ranges[subset] });
        jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

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

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

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

/** True when a loaded FontFace covers exactly this family, weight and style
 *  (document.fonts.check() is also true for families nobody declared). */
function hasFace(family, weight, style) {
  for (const face of document.fonts) {
    if (face.status !== 'loaded' || face.style !== style) continue;
    if (face.family.replace(/^["']|["']$/g, '') !== family) continue;
    const [low, high = low] = face.weight.split(' ').map(Number);
    if (weight >= low && weight <= high) return true;
  }
  return false;
}

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

/** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */
function fontsourceMeta(family) {
  fontsourceMeta.cache ??= new Map();
  const id = fontsourceId(family);
  if (!fontsourceMeta.cache.has(id)) {
    fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`)
      .then((res) => (res.ok ? res.json() : null), () => null));
  }
  return fontsourceMeta.cache.get(id);
}

// ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ───────
/** Shows the pages as facing spreads on a dark desk: the first page is a
 *  recto on its own, then verso | recto pairs, as in a bound book. Pages
 *  are painted when they scroll near the screen. */
function showPages(docs, { title, width = 460 } = {}) {
  const root = viewer(title);
  const pages = [docs].flat().flatMap((doc) =>
    doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index })));
  const spreads = [];
  let verso = null;
  for (const p of pages) {
    if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; }
    else { spreads.push([verso, p]); verso = null; }
  }
  if (verso) spreads.push([verso, null]);
  const density = Math.min(window.devicePixelRatio || 1, 2);
  showPages.painter?.disconnect();
  const painter = new IntersectionObserver((entries) => {
    for (const { isIntersecting, target } of entries) {
      if (!isIntersecting) continue;
      painter.unobserve(target);
      const { doc, page } = target.postext;
      renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width });
    }
  }, { rootMargin: '800px' });
  showPages.painter = painter;
  root.replaceChildren(...spreads.map((pair) => {
    const spread = document.createElement('div');
    spread.className = 'pt-spread';
    for (const p of pair) {
      const figure = document.createElement('figure');
      if (p) {
        const label = p.page.pageLabel || String(p.n + 1);
        const canvas = document.createElement('canvas');
        canvas.postext = p;
        canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`;
        canvas.setAttribute('role', 'img');
        canvas.setAttribute('aria-label', `Page ${label}`);
        const folio = document.createElement('figcaption');
        folio.textContent = label;
        figure.append(canvas, folio);
        painter.observe(canvas);
      } else figure.className = 'pt-blank';
      spread.append(figure);
    }
    return spread;
  }));
  kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`);
  document.documentElement.dataset.postext = 'ready';
  return pages.length;
}

/** The desk, the bar and the error reporting, created once. */
function viewer(title) {
  if (!document.getElementById('pt-kit')) {
    document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit">
      :root { color-scheme: dark; }
      body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; }
      #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center;
        gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px);
        border-bottom: 1px solid #23262d; }
      #pt-bar strong { color: #f4f1ea; font-weight: 600; }
      #pt-actions { display: flex; gap: 12px; margin-left: auto; }
      #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
      #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; }
      .pt-spread { display: flex; }
      .pt-spread figure { margin: 0; width: min(460px, 44vw); }
      .pt-spread canvas { display: block; width: 100%; background: #fff;
        box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); }
      .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif;
        letter-spacing: .18em; text-transform: uppercase; color: #6c7079; }
      .pt-blank { visibility: hidden; }
      @media (max-width: 760px) {
        .pt-spread { flex-direction: column; gap: 32px; }
        .pt-spread figure { width: min(460px, 92vw); }
        .pt-blank { display: none; }
      }
    </style>`);
    document.body.insertAdjacentHTML('afterbegin',
      '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>');
    document.getElementById('pt-title').textContent = document.title || 'Postext';
    addEventListener('error', (event) => kitFail(event.error ?? event.message));
    addEventListener('unhandledrejection', (event) => kitFail(event.reason));
  }
  if (title) document.getElementById('pt-title').textContent = title;
  return document.getElementById('pages')
    ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' }));
}

function kitStatus(text) {
  viewer();
  document.getElementById('pt-status').textContent = text;
}

function kitFail(error) {
  document.documentElement.dataset.postext = 'error';
  kitStatus(`Error: ${error?.message ?? error}`);
}

// ─── Kit · pdf v1 ── the same in every recipe that exports a PDF ──────────────
/** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen
 *  used, snapping to a weight the family ships and falling back to upright
 *  when it has no italic: the PDF asks for every face a block could use. */
async function fontsourceProvider(family, weight, style) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  const weights = meta?.weights?.length ? meta.weights : [400, 700];
  const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a));
  const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style;
  const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`);
  if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`);
  return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
}

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

// ─── Kit · images v1 ── 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 (drawn in code, or fetched) as a vector image. */
async function loadSvg(fileId, svg) {
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

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

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

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

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

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

/** The PDF font provider for recipes with CJK faces: a family whose
 *  Fontsource subsets are Chinese, Japanese or Korean gets the files that
 *  hold the characters its pages set (`request.codePoints`); any other
 *  family 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.filter((slice) => picked.has(slice)).map(async (slice) => {
    const res = await fetch(slice.url);
    if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`);
    return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
  }));
}

/** 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. Latin comes first: postext-pdf draws a character from the first
 *  file that has it, as the browser takes a character both files hold from
 *  latin. 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 = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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)]);
  return ext ? [latin, ext] : 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 ───────────────────────────────────────────────────────────────────────
```

## Variations

### Superscript citation numbers

Many Japanese journals raise the number above the line. `marker: 'superscript'` prints it raised, with no parentheses, and leaves the list alone.

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

### Notes numbered through the paper

A paper of a few pages may count its notes from 1 to the end instead of per page.

```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'),
```

## Pitfalls

- **Tag a Japanese text 'ja', never zh-Hans or LANG.** A recipe's editions are en and es, but a Japanese sample is Japanese in both: `locale: LANG` would tag it English or Spanish, and a Chinese tag would set it by the Chinese rules (Kaiming punctuation, small kana free to start a line, 图 for 図, Chinese glyph forms in the PDF). Write 'ja': it picks the Japan region (JLReq line breaking and punctuation, sesame emphasis marks, furigana spacing, 図 and 表 labels) and turns hyphenation off. The lint fails a text with kana under a zh or ko tag.
- **Set Japanese in a Japanese face.** Noto Serif SC and TC have kana, but they draw the kanji in Chinese forms (直, 骨 and 角 differ) and the kana in a Chinese design. Set the text in Noto Serif JP or Shippori Mincho B1 and the heads in Noto Sans JP, loaded with loadCjkFonts. Fontsource's Japanese files hold no hentaigana or other historic kana (U+1B000–1B16F): loadCjkFonts fails on them and the PDF prints boxes, so write the modern kana or ship a face that has them in the recipe's assets. Shippori Mincho also lacks the macron vowels ō and ū: set rōmaji in a Latin face.
- **Chinese faces load by slices, through the cjk block.** Fontsource serves a Chinese, Japanese or Korean family as about a hundred files per weight, each covering a range of characters. loadFonts fetches only the latin file, so on screen the Han characters come from a system face and measure wrong, and fontsourceProvider hands the PDF that latin file, which prints them as empty boxes. List the cjk kit block, call loadCjkFonts(FONTS, markdown) after loadFonts (once per voice, with the text it sets, when the book uses several CJK faces) and give renderToPdf fontProvider: cjkPdfProvider: both take the files that hold the text's characters.
- **On a character grid, turn column balancing off.** Column balancing fills a page that ends short, as a head kept with its text leaves blank lines at the foot, by adding grid lines above heads and by setting a paragraph one line looser. A looser Chinese line spreads its characters (0.13 em on a GB/T 9704 page, far past balancing.maxTracking), so the characters leave the columns of the grid and the heads leave their lines. A page counted in cells ends short instead: set headings.balancing: { enabled: false }.
- **Text inside an SVG <img> cannot use web fonts.** An SVG is drawn as an image, and an image has no access to the page's web fonts, so its labels fall back to a system face. Outline the text, embed an @font-face subset in the SVG, or move the labels to the caption.
- **Any headings object switches off the H1 page break.** By default an H1 breaks to a recto (always-odd), but passing any headings object resets that default, so chapters run on and span: 'page' does nothing. Restate headings.levels[0].breakBefore: { enabled: true, parity } in every config.
- **Quote every frontmatter value.** YAML reads title: 1984 as a number and a date as a Date object, and non-string values print empty in placeholders and leave the PDF without a title. Quote every value: title: "1984".
- **Load every face before layout.** Layout measures text with the faces the browser has loaded and caches the widths, so a face that arrives after the first build leaves wrong line breaks and a PDF that no longer matches the screen. Load every weight and style first, and call clearMeasurementCache() before rebuilding when one arrives late.

- Write a Japanese name as `family` and `given`, or as one `literal` with no space. A name typed as one string with a space between family and given name, as BibTeX often carries it (`author = {夏目 漱石}`), is read family first, but a romanised one, `Natsume Soseki`, is read as given name then family name, as English names are.
- The bundled SIST 02 prints no DOI; an article whose DOI matters needs it in the text or a note.

## Credits

- Recipe: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Images: Figure 1, the three series of type sizes, drawn in code to scale: Postext Cookbook, CC-BY-4.0
- Type: Noto Serif JP (OFL-1.1), Noto Sans JP (OFL-1.1), Shippori Mincho B1 (OFL-1.1)
- Code: MIT · Sample content: CC-BY-4.0

## Related

- [Nº 090 · A Chinese paper cited to GB/T 7714](https://postext.dev/en/cookbook/gbt7714-chinese-paper.md): A two-column Chinese journal article: [@key] citations print as superscript [1] and [2–4], the references carry [M], [J], [D] and [EB/OL]. · Level 2 (Intermediate) · Papers & academic
- [Nº 107 · An Arabic research article with notes, citations, index](https://postext.dev/en/cookbook/arabic-research-article.md): An Arabic journal article: notes «(١)» numbered per page, CSL citations among them in the Arabic locale, and an index sorted without the article ال. · Level 3 (Advanced) · Papers & academic
- [Nº 087 · A thesis chapter cited in APA 7](https://postext.dev/en/cookbook/apa-thesis-with-bibtex.md): A doctoral chapter whose [@key, p. 33] citations become APA 7 through citeproc-js, with the reference list built from a BibTeX block. · Level 2 (Intermediate) · Papers & academic, Reports
