# 汉字片假名混写的明治文告

> 1889年宪法竖排，用汉字和片假名：条文编号第一條来自japanese-informal列表计数器，外加文武线边框，副署排在行末。

- HTML版本: https://postext.dev/zh/cookbook/katakana-majiri-proclamation
- 食谱 No. 128 · 字体与文字 · 难度 2 (中级) · 输出: Canvas, PDF
- 体裁: 报告, 单页与零散印品
- 需要 postext ≥ 1.16.1, postext-pdf ≥ 1.16.1 · 已用 1.16.1, postext-pdf 1.16.1 测试，日期 2026-10-05
- 页面: [一](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p01.webp?v=3ba810f8), [二](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p02.webp?v=3ba810f8), [三](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p03.webp?v=3ba810f8), [四](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p04.webp?v=3ba810f8), [五](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p05.webp?v=3ba810f8), [六](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p06.webp?v=3ba810f8), [七](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p07.webp?v=3ba810f8), [八](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p08.webp?v=3ba810f8)
- PDF: https://postext.dev/cookbook/katakana-majiri-proclamation/en/katakana-majiri-proclamation.pdf?v=3ba810f8
- 在沙盒中打开: https://postext.dev/zh/sandbox#recipe=katakana-majiri-proclamation&lang=en (.postext: https://postext.dev/cookbook/katakana-majiri-proclamation/en/katakana-majiri-proclamation.postext)
- 最后更新: 2026-10-05
- 其他语言: [en](https://postext.dev/en/cookbook/katakana-majiri-proclamation.md), [es](https://postext.dev/es/cookbook/katakana-majiri-proclamation.md), [ca](https://postext.dev/ca/cookbook/katakana-majiri-proclamation.md), [ja](https://postext.dev/ja/cookbook/katakana-majiri-proclamation.md), [ar](https://postext.dev/ar/cookbook/katakana-majiri-proclamation.md)

## 简单来说

1889年的日本宪法，用汉字和片假名写成，按官报原样从上到下竖排，条文用汉字数字编号。

## 成品一览

一份八页的明治宪法重印本，照1889年2月11日《官报》（官報）的原样排印：天皇的敕语、大臣副署，以及前两章共三十二条。正文是漢字カタカナ交じり文（汉字片假名混写文），即明治法令的书面语：汉字承载词义，片假名写词尾，没有浊点，没有小写假名，也完全没有标点。页面为A5，竖排，右侧装订，在一粗一细两道框线之内用每行40字、每页15行的网格。条文用汉字列表计数器编号，从第一條到第三十二條；御名御玺和副署都放在《官报》原来的位置。

**这份食谱回答的问题:**

- 怎样排汉字片假名混写的明治文献，并把条文编成第一條、第二條？
- 怎样用汉字数字把章编号为第一章，或把列表项编为あいう和イロハ？
- 哪些字体能排日文，并在屏幕和PDF中让汉字采用日文字形？

## 简短回答

```js
// script.js, 行 40–53
const layout = { layoutType: 'single', writingMode: 'vertical-rl' }; // bound on the right
const cjk = { grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES } };
// Each article is an item of an ordered list: the counter is japanese-informal (十八, not
// 一十八), the prefix 第 and the separator 條 wrap it, and the turnover lines go back to
// the head of the line, as in the Gazette. Chapter two starts its list at 18.
const orderedLists = {
  numberFormat: 'japanese-informal', prefix: '第', separator: '條', gap: em(1),
  indent: em(0), hangingIndent: false, fontFamily: MINCHO, fontWeight: 800,
  color: col('ink'), marginTop: pt(0), marginBottom: pt(0), itemSpacing: pt(0),
};
// The chapters: 第{2:一}章 counts them in the same style, two characters down (字下げ).
const chapter = { level: 2, numberingTemplate: '第{2:一}章', numberSeparator: '　',
  fontSize: pt(BODY), fontWeight: 800, indent: em(2), lineSpan: 2, // 二行取り
  breakBefore: { enabled: false } };
```

## 用料

**讲解**

- [假名、汉字与罗马字](https://postext.dev/zh/docs/japanese-layout.md#汉字假名与罗马字): 日文在一行里混用四种文字：汉字、平假名、片假名以及拉丁字母和数字（罗马字）。标记为ja的文档在日文词之间不加空格，日文与拉丁文之间留四分之一字，竖排中的全角Ａ１直立；字体是Noto Serif JP或Shippori Mincho这样的日文字体时，汉字采用日文字形（字体的locl）。罗马字的长音符号（ō ū）需要字体里有这些字。
- [日文数字与假名序号](https://postext.dev/zh/docs/japanese-layout.md#数字与编号): 日文风格的编号：japanese-informal（百一、二千二十六，十百千前不加一）、japanese-formal（壱弐参拾）、平假名和片假名（あいう、アイウ）以及伊吕波顺序（いろは、イロハ）。在标记为ja的文档中，标记一按japanese-informal读，所以第{1:一}章印出第十二章；あ、ア、い、イ和壱在页码、列表、注释和资源编号中选用其他样式。
- [竖排](https://postext.dev/zh/docs/configuration.md#竖排): 中文和日文从上到下排、各行从右往左读（layout.writingMode 'vertical-rl'）：栏变成上下分段，图和表保持直立，标点使用竖排字形。

**还用到**

- [右翻书](https://postext.dev/zh/docs/configuration.md#装订)
- [字符网格](https://postext.dev/zh/docs/configuration.md#字格)
- [中日韩字体](https://postext.dev/zh/docs/configuration.md#中文日文和韩文字体)
- [日文禁则处理](https://postext.dev/zh/docs/japanese-layout.md#断行禁则)
- [占行标题（行取り）](https://postext.dev/zh/docs/japanese-layout.md#标题与文字块)
- [编号列表](https://postext.dev/zh/docs/configuration.md#有序列表)
- [项目符号列表与清单](https://postext.dev/zh/docs/configuration.md#无序列表)
- [设计过的章首页](https://postext.dev/zh/docs/configuration.md#通栏与高级设计)
- [标题样式](https://postext.dev/zh/docs/configuration.md#标题样式)
- [标题属性](https://postext.dev/zh/docs/document-format.md#标题属性)
- [段落样式](https://postext.dev/zh/docs/configuration.md#段落样式)
- [页面设计中的文字、线条和框](https://postext.dev/zh/docs/configuration.md#页眉与页脚)
- [书眉与页码](https://postext.dev/zh/docs/configuration.md#页眉与页脚)
- [纸张颜色](https://postext.dev/zh/docs/configuration.md#页面)
- [语义调色板](https://postext.dev/zh/docs/configuration.md#调色板)
- [导出PDF](https://postext.dev/zh/docs/configuration.md#生成pdf)
- [嵌入PDF的字体](https://postext.dev/zh/docs/configuration.md#为什么需要字体提供函数)
- [中文断行](https://postext.dev/zh/docs/configuration.md#东亚排版)
- [编号标题](https://postext.dev/zh/docs/configuration.md#按级别覆盖)
- [按页面角色显示书眉](https://postext.dev/zh/docs/configuration.md#文本元素)
- [罗马数字页码的前置部分](https://postext.dev/zh/docs/document-format.md#numbering)
- [不编号的章](https://postext.dev/zh/docs/configuration.md#标题样式)

**配置一览**

- [`bodyText`](https://postext.dev/zh/docs/configuration.md#正文), [`cjk`](https://postext.dev/zh/docs/configuration.md#东亚排版), [`colorPalette`](https://postext.dev/zh/docs/configuration.md#调色板), [`footer`](https://postext.dev/zh/docs/configuration.md#页眉与页脚), [`header`](https://postext.dev/zh/docs/configuration.md#页眉与页脚), [`headingStyles`](https://postext.dev/zh/docs/configuration.md#标题样式), [`headings`](https://postext.dev/zh/docs/configuration.md#标题), [`layout`](https://postext.dev/zh/docs/configuration.md#版式), [`locale`](https://postext.dev/zh/docs/configuration.md#断词), [`orderedLists`](https://postext.dev/zh/docs/configuration.md#有序列表), [`page`](https://postext.dev/zh/docs/configuration.md#页面), [`paragraphStyles`](https://postext.dev/zh/docs/configuration.md#段落样式), [`unorderedLists`](https://postext.dev/zh/docs/configuration.md#无序列表)

**API**

- [`buildDocument`](https://postext.dev/zh/docs/configuration.md#构建文档), [`clearMeasurementCache`](https://postext.dev/zh/docs/configuration.md#测量缓存), [`decompressWoff2`](https://postext.dev/zh/docs/configuration.md#浏览器字体提供函数fontsource--woff2), `loadVerticalAlternates`, [`renderPageToCanvas`](https://postext.dev/zh/docs/configuration.md#把页面渲染为位图), [`renderToPdf`](https://postext.dev/zh/docs/configuration.md#生成pdf)

**字体**

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

## 做法

### 1 · 让列表计数器写出第十八條

代码见上文的[简短回答](#简短回答)。每一条都是Markdown有序列表的一项，`orderedLists`把序号写成《官报》的样子：`numberFormat: 'japanese-informal'`写作十八而不是一十八，`prefix: '第'`和`separator: '條'`包在两边，`gap: em(1)`留出原文的全角空格。设置`hangingIndent: false`后，转行回到行首，而不是悬挂在正文下方，与《官报》一致。第二章的列表从`18.`开始，计数器因而跨章连续；各章本身由标题模板`第{2:一}章`编为第一章、第二章，用的是同一种计数风格（[有序列表](/zh/docs/configuration#有序列表)）。

![Page 五: 大日本帝國憲法.](https://postext.dev/cookbook/katakana-majiri-proclamation/en/p05.webp?v=3ba810f8)

*第5页：第一章　天皇低两格，第一條至第八條加粗，第八条第二款低一格。*

### 2 · 片假名照《官报》原样保留

`locale: 'ja'`启用日文规则，但没有哪条规则会改写文字。侵スヘカラス不补浊点（读作okasu bekarazu），行フ保留历史假名，奪ハルヽ保留片假名的叠字符。正文没有小写假名也没有标点，行可以在任意两个字之间断开；禁则仍然不让ヽ和々出现在行首。第2页上，编者凡例用夹带平假名的现代日文写成，与敕语并排，同一页里可以对照日文的两种写法。

### 3 · 每页都画边框

```js
// script.js, 行 57–72
// Header elements are placed on the sheet, not in the flow, so the frame is drawn in
// plain page millimetres: GAP outside the type area, then 1.4 mm to the thin rule.
const GAP = 6;
const rule = (id, out, thickness) => ({ kind: 'box', id, pages: 'all',
  style: { borderColor: col('ink'), borderWidth: pt(thickness) },
  placement: { anchor: { to: 'page', edge: 'top-left' },
    offset: { x: mm(SIDE - out), y: mm(HEAD - out) },
    size: { width: mm(AREA.across + 2 * out), height: mm(AREA.down + 2 * out) } } });
const header = { elements: [
  rule('outer', GAP + 1.4, 1.2), rule('inner', GAP, 0.4),
  // The folio in kanji, under the frame, in the middle of the page.
  { kind: 'text', id: 'folio', content: '{pageNumber}', pages: 'body', fontFamily: GOTHIC,
    fontSize: pt(8), color: col('muted'), align: 'center',
    placement: { anchor: { to: 'page', edge: 'top-left' },
      offset: { x: mm(0), y: mm(HEAD + AREA.down + GAP + 6) }, size: { width: mm(TRIM.w) } } },
] };
```

页眉槽位按纸面的毫米定位，不随竖排的文字流转向，所以边框就是两个方框，用确定版心位置的同一组数字来放：`HEAD`从上边量，`SIDE`从两侧量。页边距由网格算出，而不是交给网格去扩展，版心因此正好位于细线内侧6 mm。页码由`pageNumbering.format: 'japanese-informal'`写成汉字，放在边框下方。

### 4 · 扉页沿中轴排

```js
// script.js, 行 76–97
// In the flow frame of a vertical page x runs down the column, y leftward across it.
const at = (down, across) => ({ anchor: { to: 'container', edge: 'top-left' },
  offset: { x: mm(down), y: mm(across) } });
const MID = AREA.across / 2; // mm: the axis of the page, from the right of the type area
const cover = {
  id: 'cover', numbered: false, toc: false, span: 'page', runningChapter: false,
  breakBefore: { enabled: true, parity: 'any' },
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: MINCHO, fontWeight: 800,
      fontSize: pt(34), lineHeight: 1, letterSpacing: pt(10), color: col('ink'),
      placement: at((AREA.down - (7 * 34 + 6 * 10) * PT) / 2, MID - 34 * PT / 2) }, // centred
    { kind: 'text', id: 'date', content: '{attr.date}', fontFamily: MINCHO, fontSize: pt(11),
      lineHeight: 1, letterSpacing: pt(2), color: col('seal'), placement: at(30, MID + 18) },
    { kind: 'text', id: 'source', content: '{attr.source}', fontFamily: MINCHO,
      fontSize: pt(11), lineHeight: 1, letterSpacing: pt(4), color: col('ink'),
      placement: at(30, MID - 18 - 11 * PT) },
    // The edition's language, turned with the line, at the foot of the type area.
    { kind: 'text', id: 'latin', content: '{attr.latin}', fontFamily: GOTHIC, fontSize: pt(7.5),
      lineHeight: 1.5, color: col('muted'), overflow: 'wrap', align: 'right',
      placement: { ...at(0, AREA.across - 9), size: { width: mm(AREA.down), height: mm(9) } } },
  ] } },
};
```

在竖排流中，`x`沿纸面向下，`y`向左横量，所以标题这一列按它到版心右缘的距离来定位。强调色的日期和《官报》的名称分列标题两侧，各距中轴18 mm。

### 5 · 每种字体只加载它要排的文字

```js
// script.js, 行 300–301
const notes = (markdown.match(/^- .*$/gm) ?? []).join('');
const gothicText = `${notes}凡例上諭一二三四五六七八九十${markdown.match(/latin="([^"]*)"/)[1]}`;
```

Shippori Mincho B1是带有國、條、權等旧字形的明朝体，加载正文和标题所需的文件；Noto Sans JP只加载编者凡例、页码和英文说明。字体拿到的文字少于它实际排的文字时，截图会失败（C12）。

## 完整食谱

一个文件，由食谱文件夹合成，示例文本和排版食谱的公共工具包都已内联；它自己排出页面。要运行它，把它放进空白页面的`<script type="module">`，或粘贴到新建CodePen的JS面板里（设为模块）。它从esm.sh导入postext，不需要安装或构建。

- 食谱文件夹: https://github.com/drnachio/postext/tree/main/cookbook/katakana-majiri-proclamation

### script.js

```js
// ═══ Postext Cookbook · Nº 128 · A Meiji proclamation in kanji and katakana ═════════
// https://postext.dev/en/cookbook/katakana-majiri-proclamation
// Code: MIT · Text: Kanpō 1889, ja.wikisource (CC BY-SA 4.0); notes (CC BY 4.0)
// Fonts: Shippori Mincho B1, Noto Sans JP (SIL OFL 1.1) · Needs postext ≥ 1.16.1
// The Meiji Constitution as the Official Gazette printed it: kanji and katakana, vertical.
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, loadVerticalAlternates,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the frame; the text is Japanese in both editions
const RECIPE = 'katakana-majiri-proclamation';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: gazette black, one vermilion for the seal, toned paper
const palette = {
  ink: '#211d1a', // the text and the frame
  seal: '#a8321f', // 御名御璽 and the cover's date: the one accent (5.5:1 on the paper)
  muted: '#6b6259', // the editor's notes, the folio, the colophon
  paper: '#fbf8f0',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'seal (defaults)', value: { hex: palette.seal, model: 'hex' } },
];
// #endregion

const MINCHO = 'Shippori Mincho B1'; // 明朝 with Meiji type in its bones, old forms 國 條 權
const GOTHIC = 'Noto Sans JP'; // the editor's voice: notes, folios, colophon
const [BODY, LEAD] = [10, 18]; // pt: line pitch 1.8 em
const [CHARS, LINES] = [40, 15]; // the type area: 40 characters down, 15 lines across
const PT = 25.4 / 72; // mm in a point
const TRIM = { w: 148, h: 210 }; // mm, A5
const AREA = { down: CHARS * BODY * PT, across: LINES * LEAD * PT }; // 141.1 × 95.3 mm
const HEAD = 37; // mm from the top edge to the head of the type area: 天 deeper than 地
const SIDE = (TRIM.w - AREA.across) / 2; // the same margin on both sides of the frame

// #region answer: kanji with katakana, vertical, and articles numbered 第一條 in kanji
const layout = { layoutType: 'single', writingMode: 'vertical-rl' }; // bound on the right
const cjk = { grid: { enabled: true, charsPerLine: CHARS, linesPerPage: LINES } };
// Each article is an item of an ordered list: the counter is japanese-informal (十八, not
// 一十八), the prefix 第 and the separator 條 wrap it, and the turnover lines go back to
// the head of the line, as in the Gazette. Chapter two starts its list at 18.
const orderedLists = {
  numberFormat: 'japanese-informal', prefix: '第', separator: '條', gap: em(1),
  indent: em(0), hangingIndent: false, fontFamily: MINCHO, fontWeight: 800,
  color: col('ink'), marginTop: pt(0), marginBottom: pt(0), itemSpacing: pt(0),
};
// The chapters: 第{2:一}章 counts them in the same style, two characters down (字下げ).
const chapter = { level: 2, numberingTemplate: '第{2:一}章', numberSeparator: '　',
  fontSize: pt(BODY), fontWeight: 800, indent: em(2), lineSpan: 2, // 二行取り
  breakBefore: { enabled: false } };
// #endregion

// #region frame: a thick and a thin rule round the type area, on every page
// Header elements are placed on the sheet, not in the flow, so the frame is drawn in
// plain page millimetres: GAP outside the type area, then 1.4 mm to the thin rule.
const GAP = 6;
const rule = (id, out, thickness) => ({ kind: 'box', id, pages: 'all',
  style: { borderColor: col('ink'), borderWidth: pt(thickness) },
  placement: { anchor: { to: 'page', edge: 'top-left' },
    offset: { x: mm(SIDE - out), y: mm(HEAD - out) },
    size: { width: mm(AREA.across + 2 * out), height: mm(AREA.down + 2 * out) } } });
const header = { elements: [
  rule('outer', GAP + 1.4, 1.2), rule('inner', GAP, 0.4),
  // The folio in kanji, under the frame, in the middle of the page.
  { kind: 'text', id: 'folio', content: '{pageNumber}', pages: 'body', fontFamily: GOTHIC,
    fontSize: pt(8), color: col('muted'), align: 'center',
    placement: { anchor: { to: 'page', edge: 'top-left' },
      offset: { x: mm(0), y: mm(HEAD + AREA.down + GAP + 6) }, size: { width: mm(TRIM.w) } } },
] };
// #endregion

// #region cover: the title down the middle, the date and the source to its left
// In the flow frame of a vertical page x runs down the column, y leftward across it.
const at = (down, across) => ({ anchor: { to: 'container', edge: 'top-left' },
  offset: { x: mm(down), y: mm(across) } });
const MID = AREA.across / 2; // mm: the axis of the page, from the right of the type area
const cover = {
  id: 'cover', numbered: false, toc: false, span: 'page', runningChapter: false,
  breakBefore: { enabled: true, parity: 'any' },
  advancedDesign: { enabled: true, slot: { elements: [
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: MINCHO, fontWeight: 800,
      fontSize: pt(34), lineHeight: 1, letterSpacing: pt(10), color: col('ink'),
      placement: at((AREA.down - (7 * 34 + 6 * 10) * PT) / 2, MID - 34 * PT / 2) }, // centred
    { kind: 'text', id: 'date', content: '{attr.date}', fontFamily: MINCHO, fontSize: pt(11),
      lineHeight: 1, letterSpacing: pt(2), color: col('seal'), placement: at(30, MID + 18) },
    { kind: 'text', id: 'source', content: '{attr.source}', fontFamily: MINCHO,
      fontSize: pt(11), lineHeight: 1, letterSpacing: pt(4), color: col('ink'),
      placement: at(30, MID - 18 - 11 * PT) },
    // The edition's language, turned with the line, at the foot of the type area.
    { kind: 'text', id: 'latin', content: '{attr.latin}', fontFamily: GOTHIC, fontSize: pt(7.5),
      lineHeight: 1.5, color: col('muted'), overflow: 'wrap', align: 'right',
      placement: { ...at(0, AREA.across - 9), size: { width: mm(AREA.down), height: mm(9) } } },
  ] } },
};
// #endregion

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: 'ja', // written out, never LANG (gotcha: ja-locale-tag)
  colorPalette,
  page: {
    sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(HEAD), bottom: mm(TRIM.h - HEAD - AREA.down), left: mm(SIDE),
      right: mm(SIDE), mirror: true },
    pageNumbering: { format: 'japanese-informal' }, // 一, 二, 三…
  },
  layout,
  cjk,
  bodyText: {
    fontFamily: MINCHO, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    // A new clause of an article (項) starts one character down; the edict's paragraphs
    // start flush, as the Gazette sets them.
    textAlign: 'justify', firstLineIndent: em(1), indentAfterHeading: true,
  },
  headings: {
    fontFamily: MINCHO, color: col('ink'), fontWeight: 800,
    levels: [
      // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
      { level: 1, breakBefore: { enabled: true, parity: 'any' } },
      chapter,
    ],
  },
  headingStyles: [
    cover,
    // 凡例 and 上諭: in the editor's gothic, one line, not counted as chapters.
    { id: 'notes', numbered: false, fontFamily: GOTHIC, fontWeight: 700, fontSize: pt(9),
      color: col('muted'), lineSpan: 1, indent: em(1) },
    { id: 'edict', numbered: false, fontFamily: GOTHIC, fontWeight: 700, fontSize: pt(9),
      color: col('muted'), lineSpan: 2, indent: em(1) },
    { id: 'law', numbered: false, indent: em(1), lineSpan: 2 },
  ],
  unorderedLists: { bulletChar: '一', gap: em(1), indent: em(1), fontFamily: GOTHIC,
    color: col('muted'), marginTop: pt(0), marginBottom: pt(0), itemSpacing: pt(0) },
  orderedLists,
  paragraphStyles: [
    { id: 'flush', firstLineIndent: em(0) },
    // 御名　御璽, larger, three characters down: where the Emperor signed and sealed.
    { id: 'seal', fontSize: pt(15), lineHeight: pt(2 * LEAD), color: col('seal'),
      indent: em(2), marginTop: pt(LEAD) },
    // The ministers' countersignatures end at the foot of the line (地付き).
    { id: 'countersign', textAlign: 'end', marginTop: pt(LEAD) },
    { id: 'colophon', fontFamily: GOTHIC, fontSize: pt(7), lineHeight: pt(LEAD),
      color: col('muted'), textAlign: 'left', marginTop: pt(2 * LEAD) },
  ],
  header,
  footer: { elements: [] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "大日本帝國憲法"
author: "明治二十二年二月十一日發布"
---

# 大日本帝國憲法 {style="cover" date="明治二十二年二月十一日" source="官報號外" latin="The Constitution of the Empire of Japan · Official Gazette, 11 February 1889"}

## 凡例 {style="notes"}

- 本書には、明治二十二年二月十一日の官報號外に載った大日本帝國憲法のうち、上諭と第一章、第二章を収めた。
- 本文は官報のとおり漢字カタカナ交じり文で組み、旧字体と歴史的仮名遣いを改めていない。原文には濁点も句読点もない。「侵スヘカラス」は「おかすべからず」、「行フ」は「おこなう」と読む。
- 官報は送り仮名のカタカナを漢字より小さく組んでいるが、本書では本文と同じ大きさにそろえた。
- 大臣の副署は、官報では官職名を均等に割り付けているが、本書では行末をそろえるだけにとどめた。

## 上諭 {style="edict"}

:::paragraphs{style="flush"}
朕祖宗ノ遺烈ヲ承ケ萬世一系ノ帝位ヲ踐ミ朕カ親愛スル所ノ臣民ハ即チ朕カ祖宗ノ惠撫慈養シタマヒシ所ノ臣民ナルヲ念ヒ其ノ康福ヲ増進シ其ノ懿徳良能ヲ發達セシメムコトヲ願ヒ又其ノ翼贊ニ依リ與ニ倶ニ國家ノ進運ヲ扶持セムコトヲ望ミ乃チ明治十四年十月十二日ノ詔命ヲ履踐シ茲ニ大憲ヲ制定シ朕カ率由スル所ヲ示シ朕カ後嗣及臣民及臣民ノ子孫タル者ヲシテ永遠ニ循行スル所ヲ知ラシム

國家統治ノ大權ハ朕カ之ヲ祖宗ニ承ケテ之ヲ子孫ニ傳フル所ナリ朕及朕カ子孫ハ將來此ノ憲法ノ條章ニ循ヒ之ヲ行フコトヲ愆ラサルヘシ

朕ハ我カ臣民ノ權利及財産ノ安全ヲ貴重シ及之ヲ保護シ此ノ憲法及法律ノ範圍内ニ於テ其ノ享有ヲ完全ナラシムヘキコトヲ宣言ス

帝國議會ハ明治二十三年ヲ以テ之ヲ召集シ議會開會ノ時ヲ以テ此ノ憲法ヲシテ有効ナラシムルノ期トスヘシ

將來若此ノ憲法ノ或ル條章ヲ改定スルノ必要ナル時宜ヲ見ルニ至ラハ朕及朕カ繼統ノ子孫ハ發議ノ權ヲ執リ之ヲ議會ニ付シ議會ハ此ノ憲法ニ定メタル要件ニ依リ之ヲ議決スルノ外朕カ子孫及臣民ハ敢テ之カ紛更ヲ試ミルコトヲ得サルヘシ

朕カ在廷ノ大臣ハ朕カ爲ニ此ノ憲法ヲ施行スルノ責ニ任スヘク朕カ現在及將來ノ臣民ハ此ノ憲法ニ對シ永遠ニ從順ノ義務ヲ負フヘシ
:::

:::paragraphs{style="seal"}
御名　御璽
:::

:::paragraphs{indent=6}
明治二十二年二月十一日
:::

:::paragraphs{style="countersign"}
内閣總理大臣　伯爵　黒田清隆

樞密院議長　伯爵　伊藤博文

外務大臣　伯爵　大隈重信

海軍大臣　伯爵　西郷從道

農商務大臣　伯爵　井上　馨

司法大臣　伯爵　山田顯義

大藏大臣兼内務大臣　伯爵　松方正義

陸軍大臣　伯爵　大山　巖

文部大臣　子爵　森　有禮

遞信大臣　子爵　榎本武揚
:::

## 大日本帝國憲法 {style="law"}

## 天皇

1. 大日本帝國ハ萬世一系ノ天皇之ヲ統治ス

2. 皇位ハ皇室典範ノ定ムル所ニ依リ皇男子孫之ヲ繼承ス

3. 天皇ハ神聖ニシテ侵スヘカラス

4. 天皇ハ國ノ元首ニシテ統治權ヲ總攬シ此ノ憲法ノ條規ニ依リ之ヲ行フ

5. 天皇ハ帝國議會ノ協贊ヲ以テ立法權ヲ行フ

6. 天皇ハ法律ヲ裁可シ其ノ公布及執行ヲ命ス

7. 天皇ハ帝國議會ヲ召集シ其ノ開會閉會停會及衆議院ノ解散ヲ命ス

8. 天皇ハ公共ノ安全ヲ保持シ又ハ其ノ災厄ヲ避クル爲緊急ノ必要ニ由リ帝國議會閉會ノ場合ニ於テ法律ニ代ルヘキ勅令ヲ發ス

   此ノ勅令ハ次ノ會期ニ於テ帝國議會ニ提出スヘシ若議會ニ於テ承諾セサルトキハ政府ハ將來ニ向テ其ノ効力ヲ失フコトヲ公布スヘシ

9. 天皇ハ法律ヲ執行スル爲ニ又ハ公共ノ安寧秩序ヲ保持シ及臣民ノ幸福ヲ増進スル爲ニ必要ナル命令ヲ發シ又ハ發セシム但シ命令ヲ以テ法律ヲ變更スルコトヲ得ス

10. 天皇ハ行政各部ノ官制及文武官ノ俸給ヲ定メ及文武官ヲ任免ス但シ此ノ憲法又ハ他ノ法律ニ特例ヲ掲ケタルモノハ各々其ノ條項ニ依ル

11. 天皇ハ陸海軍ヲ統帥ス

12. 天皇ハ陸海軍ノ編制及常備兵額ヲ定ム

13. 天皇ハ戰ヲ宣シ和ヲ講シ及諸般ノ條約ヲ締結ス

14. 天皇ハ戒嚴ヲ宣告ス

    戒嚴ノ要件及効力ハ法律ヲ以テ之ヲ定ム

15. 天皇ハ爵位勳章及其ノ他ノ榮典ヲ授與ス

16. 天皇ハ大赦特赦減刑及復權ヲ命ス

17. 攝政ヲ置クハ皇室典範ノ定ムル所ニ依ル

    攝政ハ天皇ノ名ニ於テ大權ヲ行フ

## 臣民權利義務

18. 日本臣民タルノ要件ハ法律ノ定ムル所ニ依ル

19. 日本臣民ハ法律命令ノ定ムル所ノ資格ニ應シ均ク文武官ニ任セラレ及其ノ他ノ公務ニ就クコトヲ得

20. 日本臣民ハ法律ノ定ムル所ニ從ヒ兵役ノ義務ヲ有ス

21. 日本臣民ハ法律ノ定ムル所ニ從ヒ納税ノ義務ヲ有ス

22. 日本臣民ハ法律ノ範圍内ニ於テ居住及移轉ノ自由ヲ有ス

23. 日本臣民ハ法律ニ依ルニ非スシテ逮捕監禁審問處罰ヲ受クルコトナシ

24. 日本臣民ハ法律ニ定メタル裁判官ノ裁判ヲ受クルノ權ヲ奪ハルヽコトナシ

25. 日本臣民ハ法律ニ定メタル場合ヲ除ク外其ノ許諾ナクシテ住所ニ侵入セラレ及搜索セラルヽコトナシ

26. 日本臣民ハ法律ニ定メタル場合ヲ除ク外信書ノ祕密ヲ侵サルヽコトナシ

27. 日本臣民ハ其ノ所有權ヲ侵サルヽコトナシ

    公益ノ爲必要ナル處分ハ法律ノ定ムル所ニ依ル

28. 日本臣民ハ安寧秩序ヲ妨ケス及臣民タルノ義務ニ背カサル限ニ於テ信教ノ自由ヲ有ス

29. 日本臣民ハ法律ノ範圍内ニ於テ言論著作印行集會及結社ノ自由ヲ有ス

30. 日本臣民ハ相當ノ敬禮ヲ守リ別ニ定ムル所ノ規程ニ從ヒ請願ヲ爲スコトヲ得

31. 本章ニ掲ケタル條規ハ戰時又ハ國家事變ノ場合ニ於テ天皇大權ノ施行ヲ妨クルコトナシ

32. 本章ニ掲ケタル條規ハ陸海軍ノ法令又ハ紀律ニ牴觸セサルモノニ限リ軍人ニ準行ス

:::paragraphs{style="colophon"}
Set in Shippori Mincho B1 and Noto Sans JP (SIL OFL) · Text: Official Gazette (官報) extra of 11 February 1889, from the Japanese Wikisource transcription (revision 244180, CC BY-SA 4.0) · Editorial notes: CC BY 4.0
:::
`; // content.<lang>.md: the same Japanese text in both

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = { 'Shippori Mincho B1': ['400', '800'], 'Noto Sans JP': ['400', '700'] };
// #region voices: each face loads the files of the characters it sets
const notes = (markdown.match(/^- .*$/gm) ?? []).join('');
const gothicText = `${notes}凡例上諭一二三四五六七八九十${markdown.match(/latin="([^"]*)"/)[1]}`;
// #endregion

// ─── 4 · Build & show ───────────────────────────────────────────────────────
await loadFonts(FONTS, markdown);
await loadCjkFonts({ [MINCHO]: FONTS[MINCHO] }, markdown.replace(/^- .*$/gm, ''),
  { vertical: true });
await loadCjkFonts({ [GOTHIC]: FONTS[GOTHIC] }, gothicText, { vertical: true });
const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown);
showBook(doc, { title: t({ en: 'The Meiji Constitution, 1889',
  es: 'La Constitución Meiji, 1889' }) });
offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);

// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ─────

// ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ─────────
function mm(value) { return { value, unit: 'mm' }; }
function pt(value) { return { value, unit: 'pt' }; }
function em(value) { return { value, unit: 'em' }; }
/** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */
function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; }
/** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */
function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; }

// ─── Kit · fonts v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

/** The PDF font provider for recipes with CJK faces: a family whose
 *  Fontsource subsets are Chinese, Japanese or Korean gets the files that
 *  hold the characters its pages set (`request.codePoints`); any other
 *  family 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 ───────────────────────────────────────────────────────────────────────
```

## 变化

### 转行悬挂在正文下方

现代的法规汇编让条文的第二行对齐正文的第一个字，而不是对齐“第”。

```diff
-  indent: em(0), hangingIndent: false, fontFamily: MINCHO, fontWeight: 800,
+  indent: em(0), hangingIndent: true, fontFamily: MINCHO, fontWeight: 800,
```

### 用大写数字编号

有些法律和财务文本写壱、弐、参：用记号壱或`japanese-formal`风格，得到第壱條、第弐條。

```diff
-  numberFormat: 'japanese-informal', prefix: '第', separator: '條', gap: em(1),
+  numberFormat: 'japanese-formal', prefix: '第', separator: '條', gap: em(1),
```

## 常见问题

- **日文文本标记为'ja'，不要用zh-Hans或LANG.** 食谱的版本是en和es，但日文示例在两个版本里都是日文：`locale: LANG`会把它标记为英文或西班牙文，中文标记则会按中文规则排它（开明式标点、小假名可以出现在行首、图代替図、PDF中用中文字形）。写'ja'：它选用日本地区（JLReq断行和标点、芝麻着重号、振假名的分布、図和表标签），并关闭断词。lint会让带假名却标记为zh或ko的文本失败。
- **日文要用日文字体排.** Noto Serif SC和TC里有假名，但汉字是中文字形（直、骨、角都不同），假名也是中文字体的设计。正文用Noto Serif JP或Shippori Mincho B1，标题用Noto Sans JP，用loadCjkFonts加载。Fontsource的日文文件里没有变体假名等历史假名（U+1B000–1B16F）：loadCjkFonts遇到它们会失败，PDF里印出空框，所以要写现代假名，或者把含有这些字的字体放进食谱的assets。Shippori Mincho也没有带长音符号的ō和ū：罗马字要用拉丁字体排。
- **中文字体通过cjk块分片加载.** Fontsource把一个中文、日文或韩文字体家族按每个字重约一百个文件提供，每个文件覆盖一段字符范围。loadFonts只获取latin文件，所以屏幕上的汉字来自系统字体，测量不准；fontsourceProvider交给PDF的也是这个latin文件，汉字印出来是空框。列出kit中的cjk块，在loadFonts之后调用loadCjkFonts(FONTS, markdown)（书中用到几种CJK字体时，每种字体调用一次，并传入它所排的文字），并给renderToPdf传fontProvider: cjkPdfProvider：两者都会取用包含文中字符的那些文件。
- **传入任何headings对象都会关掉H1换页.** 默认情况下，H1换页到右页（always-odd），但只要传入headings对象，这个默认值就会被重置，于是各章接排，span: 'page'也不起作用。在每份配置中重新写明headings.levels[0].breakBefore: { enabled: true, parity }。
- **配置按对象身份缓存：每次新建一个对象.** 引擎按对象身份缓存解析后的配置，所以就地修改配置再构建，会复用旧的结果。每次构建都新建一个对象，这也是食谱的配置写成工厂函数config()的原因。
- **排版前加载所有字体.** 排版用浏览器已加载的字体测量文字，并缓存宽度，所以首次构建之后才到的字体会造成断行错误，PDF也不再与屏幕一致。先加载所有字重和样式；有字体迟到时，重新构建前调用clearMeasurementCache()。

- 《官报》把词尾的片假名排得比汉字小，并把大臣的官衔均匀撑开到同一宽度（字取り）。Postext按正文字号排所有字符，它的均排`jidori`只用于标题，所以这里的副署只在行末对齐。
- 日文和中文对这个计数器的理解不同：在`ja`文档中，记号`一`和`{2:一}`表示`japanese-informal`（百一、十八）；在中文文档中表示中文写法（一百零一、十八）。中文读本[竖排中的日期与缩略词直立](https://postext.dev/zh/cookbook/chinese-dates-upright.md)的第一條是手工输入的。

## 致谢

- 食谱: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- 文字: The Constitution of the Empire of Japan, 11 February 1889: the imperial preamble (上諭) with its countersignatures, chapters 1 and 2, as printed in the Kanpō (Official Gazette) extra of that day: Government of Japan; transcription by Japanese Wikisource editors ([来源](https://ja.wikisource.org/w/index.php?title=%E5%A4%A7%E6%97%A5%E6%9C%AC%E5%B8%9D%E5%9C%8B%E6%86%B2%E6%B3%95&oldid=244180)), CC-BY-SA-4.0
- 文字: The editor's notes (凡例), written in Japanese for this recipe: Postext Cookbook, CC-BY-4.0
- 字体: Shippori Mincho B1 (OFL-1.1), Noto Sans JP (OFL-1.1)
- 代码: MIT · 示例内容: CC-BY-4.0

## 相关食谱

- [No. 081 · 竖排中的日期与缩略词直立](https://postext.dev/zh/cookbook/chinese-dates-upright.md): 两份1912年的建国文献竖排、右侧装订：一两位数字不加标记就在一格内直立，其余的用:tcy和:upright标出。 · 难度 2 (中级) · 教科书, 单页与零散印品
- [No. 082 · 按GB/T 9704排一份党政机关公文](https://postext.dev/zh/cookbook/chinese-official-document.md): 一份按国家标准排在A4纸上的四页通知：三号字28 × 22字的版心、红色版头、一、（一）1.（1）四级标题、附件，以及“— 1 —”式页码。 · 难度 2 (中级) · 报告
- [No. 075 · 右翻的竖排中文小说](https://postext.dev/zh/cookbook/vertical-novel-right-bound.md): 把《三國演義》第一回排在台湾25開页面上：每行40字、共16行，加线框的回目章首页，切口书眉，中文数字页码，跨页从右向左阅读。 · 难度 3 (高级) · 小说、戏剧与文学散文
