Zum Hauptinhalt springen
Rezept Nummer 128

Rezepte · Kapitel 2 · Schrift und Text

Eine Meiji-Proklamation in Kanji und Katakana

Die Verfassung von 1889 senkrecht in Kanji und Katakana: Artikel 第一條 aus einem Listenzähler japanese-informal, ein Linienrahmen, Gegenzeichnungen am Fuß.

Auf dieser Seite
Ausgabe
Canvas · PDF
Stufe
Mittel
Postext
Getestet mit Postext 1.31.0
Erfordert ≥ 1.28.0 · postext-pdf ≥ 1.28.0
Lizenz
Aktualisiert am 11. Okt. 2026
Code MIT · Text CC BY 4.0

S. 一 · 1 von 8

  • Englisches Beispiel: noch keine deutsche Ausgabe
  • Endformat 148 × 210 mm
  • 1 Spalte
  • Shippori Mincho B1 10/18
  • Noto Sans JP
  • 8 Seiten
  • Stufe
  • Postext 1.30.0
  • In 60 ms gesetzt
  • 163 Zeilen Code

Kurz gesagt

Die japanische Verfassung von 1889 in Kanji und Katakana, in Zeilen von oben nach unten gedruckt wie im Amtsblatt, die Artikel in Kanji nummeriert.

Was Sie setzen

Ein achtseitiger Nachdruck der Meiji-Verfassung, so wie das Amtsblatt (官報) sie am 11. Februar 1889 druckte: das kaiserliche Edikt, die Gegenzeichnungen der Minister und die ersten beiden Kapitel, zweiunddreißig Artikel. Der Text ist 漢字カタカナ交じり文 (Kanji-Katakana-majiri-bun), die Schriftsprache des Meiji-Rechts: Kanji tragen die Wörter, Katakana die Endungen, und es gibt keine Stimmhaftigkeitszeichen, keine kleinen Kana und überhaupt keine Satzzeichen. Die Seiten sind A5, senkrecht gesetzt und rechts gebunden, mit einem Raster von 40 × 15 Zeichen innerhalb einer dicken und einer dünnen Linie. Die Artikel nummeriert ein Listenzähler in Kanji, 第一條 bis 第三十二條, und Siegel und Gegenzeichnungen stehen dort, wo das Amtsblatt sie hinsetzte.

Dieses Rezept beantwortet

  • Wie setze ich einen Meiji-Text in Kanji und Katakana, mit seinen alten Formen, und nummeriere seine Artikel 第一條?
  • Wie nummeriere ich Kapitel 第一章 mit Kanji-Zahlzeichen oder Listeneinträge mit あ い う und イ ロ ハ?
  • Welche Schriften setzen Japanisch, mit Kanji in ihren japanischen Formen, am Bildschirm und im PDF?

Die kurze Antwort

script.js · Zeilen 40–56im vollständigen Code
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. numberWidth 'item' starts the text of each
// article one gap after its own label: one full-width space follows 第九條 and 第十一條
// alike, and their texts start a character apart. Chapter two starts its list at 18.
const orderedLists = {
  numberFormat: 'japanese-informal', prefix: '第', separator: '條', gap: em(1),
  numberWidth: 'item',
  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 } };

Zutaten

Schrift
Shippori Mincho B1, Noto Sans JP (SIL OFL 1.1)
Dateien
Keine: Jedes Bild wird im Code gezeichnet

Zubereitung

#1 · Einen Listenzähler 第十八條 schreiben lassen

Der Code steht oben in der kurzen Antwort. Jeder Artikel ist ein Punkt einer nummerierten Markdown-Liste, und orderedLists bringt seine Nummer in die Form des Amtsblatts: numberFormat: 'japanese-informal' schreibt 十八, nicht 一十八, prefix: '第' und separator: '條' umschließen sie, und gap: em(1) lässt den vollbreiten Leerraum des Originals. numberWidth: 'item' beginnt den Text jedes Artikels einen Abstand nach seinem eigenen Label, sodass auf 第九條 und auf 第十一條 je ein vollbreiter Leerraum folgt, wie im Amtsblatt, und ihre Texte ein Zeichen versetzt beginnen. Mit hangingIndent: false kehren die Folgezeilen an den Zeilenanfang zurück, statt unter dem Text zu hängen, wie im Amtsblatt. Kapitel zwei beginnt seine Liste mit 18., sodass der Zähler über die Kapitel weiterläuft, und die Kapitel selbst nummeriert die Überschriftenvorlage 第{2:一}章 als 第一章 und 第二章, im selben Zählerstil (nummerierte Listen).

Seite 5: 第一章 天皇 um zwei Zeichen eingerückt, die Artikel 第一條 bis 第八條 in Fett, der zweite Absatz von Artikel 8 um ein Zeichen eingerückt.

#2 · Die Katakana so lassen, wie das Amtsblatt sie schrieb

locale: 'ja' setzt die japanischen Regeln, und keine davon schreibt den Text um. 侵スヘカラス bleibt ohne Stimmhaftigkeitszeichen (gelesen okasu bekarazu), 行フ behält seine historischen Kana und 奪ハルヽ sein Katakana-Wiederholungszeichen. Da der Text keine kleinen Kana und keine Satzzeichen hat, fallen die Zeilenumbrüche zwischen zwei beliebige Zeichen; die Kinsoku-Regeln halten ヽ und 々 dennoch vom Zeilenanfang fern. Die Anmerkungen des Herausgebers, in modernem Japanisch mit Hiragana, stehen auf Seite 2 neben dem Edikt, sodass sich die beiden Schreibweisen des Japanischen auf einer Seite vergleichen lassen.

#3 · Den Rahmen auf jede Seite zeichnen

script.js · Zeilen 60–75im vollständigen Code
// 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) } } },
] };

Der Platz der Kopfzeile zeichnet in einfachen Seitenmillimetern, nicht im senkrechten Fluss; der Rahmen besteht daher aus zwei Kästen, die mit denselben Zahlen platziert werden wie der Satzspiegel: HEAD von oben, SIDE von jeder Kante. Die Ränder werden aus dem Raster berechnet, nicht ihm überlassen, sodass der Satzspiegel genau 6 mm innerhalb der dünnen Linie liegt. Die Pagina, in Kanji aus pageNumbering.format: 'japanese-informal', steht unter dem Rahmen.

#4 · Die Titelseite entlang der Achse setzen

script.js · Zeilen 79–100im vollständigen Code
// 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) } } },
  ] } },
};

In einem senkrechten Fluss läuft x am Blatt hinab und y nach links quer darüber; die Spalte des Titels wird also nach ihrem Abstand von der rechten Kante des Satzspiegels platziert. Das Datum in der Akzentfarbe und der Name des Amtsblatts stehen zu beiden Seiten des Titels, 18 mm neben der Achse.

#5 · Jede Schrift mit dem Text laden, den sie setzt

script.js · Zeilen 302–303im vollständigen Code
const notes = (markdown.match(/^- .*$/gm) ?? []).join('');
const gothicText = `${notes}凡例上諭一二三四五六七八九十${markdown.match(/latin="([^"]*)"/)[1]}`;

Shippori Mincho B1, eine Mincho mit den alten Formen 國, 條 und 權, lädt die Dateien für Text und Überschriften; Noto Sans JP lädt nur die Anmerkungen des Herausgebers, die Paginas und die englische Zeile. Eine Schrift, die weniger bekommt, als sie setzt, lässt die Aufnahme scheitern (C12).

Das ganze Rezept

Sandbox
// ═══ 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.28.0
// The Meiji Constitution as the Official Gazette printed it: kanji and katakana, vertical.
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, 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. numberWidth 'item' starts the text of each
// article one gap after its own label: one full-width space follows 第九條 and 第十一條
// alike, and their texts start a character apart. Chapter two starts its list at 18.
const orderedLists = {
  numberFormat: 'japanese-informal', prefix: '第', separator: '條', gap: em(1),
  numberWidth: 'item',
  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 = () => ({
  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: [
      { 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`---
Markdown-Beispiel · 140 Zeilen · content.en.mdtitle: "大日本帝國憲法" 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 withLoadedFonts(() => buildDocument({ markdown }, config()), { ...kitFonts(FONTS), text: markdown }); showBook(doc, { title: t({ en: 'The Meiji Constitution, 1889', es: 'La Constitución Meiji, 1889' }) }); offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider }), `${RECIPE}.pdf`);
Kit · core, fonts, viewer, pdf, cjk: in jedem Rezept gleich · 458 Zeilen// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · pdf v3 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType, with their range and * place: the nearest weight the family ships, upright with no italic; * latin, then what its letters need (kitSubsetsFor). */ async function fontsourceProvider(family, weight, style, request) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const text = String.fromCodePoint(...(request?.codePoints ?? [])); const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => { const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())), { unicodeRange: kitRanges()[subset], order }); })); return files.length === 1 ? files[0] : files; } /** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame * shows no PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── Kit · cjk v2 ── Chinese, Japanese and Korean books · postext.dev/cookbook // Fontsource ships a CJK family as about a hundred files per weight, each // declared in its stylesheet with the unicode-range it covers. The screen // loads the files the sample touches; the PDF gets the same files for the // characters its pages set in each face, and embeds each as a subset. // A book bound on the right (vertical text) is shown with its spreads // mirrored: page 1 alone on the left of the spine, then [3 | 2]. /** The files of a Fontsource face, read from its stylesheet: { url, range, * ranges }, the last declared first (the order the browser tries them in). */ function cjkSlices(family, weight, style) { cjkSlices.cache ??= new Map(); const id = fontsourceId(family); const css = `${id}/${weight}${style === 'italic' ? '-italic' : ''}.css`; if (!cjkSlices.cache.has(css)) { let at; cjkSlices.cache.set(css, fontsourceAt(id).then((a) => fetch(at = a + css.slice(id.length + 1))) .then((res) => { if (!res.ok) throw new Error(`Fontsource has no ${family} ${weight} ${style} (${res.status})`); return res.text(); }) .then((text) => [...text.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => { const range = /unicode-range:\s*([^;]+);/.exec(rule)?.[1].trim() ?? 'U+0-10FFFF'; const ranges = range.split(',').map((part) => { const [lo, hi = lo] = part.trim().slice(2).split('-'); return [parseInt(lo, 16), parseInt(hi, 16)]; }); return { url: new URL(/url\(([^)]+?\.woff2)\)/.exec(rule)[1], at).href, range, ranges }; }).reverse())); } return cjkSlices.cache.get(css); } /** The file of `slices` the screen sets code point `cp` in, if any: the * last one loadCjkFonts adds whose range holds it. A numbered file that * repeats a letter of latin is added after latin, and wins it. */ function cjkSliceFor(slices, cp) { const holds = (slice) => slice.ranges.some(([lo, hi]) => cp >= lo && cp <= hi); return slices.reduce((last, slice) => (holds(slice) ? slice : last), undefined); } /** Whether Fontsource serves `family` as a Chinese, Japanese or Korean * family (its subsets name the script). Fails when the API does not * answer: a CJK face taken for a Latin one would paint in a system face. */ async function isCjkFamily(family) { const meta = await fontsourceMeta(family); if (!meta) throw new Error(`api.fontsource.org did not describe ${family}: reload to try again`); return !!meta.subsets?.some((subset) => /^(chinese|japanese|korean)/.test(subset)); } /** faces = { 'Noto Serif TC': ['400', '700'] }, as for loadFonts: the * whole FONTS object may be passed, its other families are left to * loadFonts. Adds one FontFace per file of each CJK face with its * unicodeRange, then loads the files `text` touches. `text` is what the * faces set: the sample for the text face; a book in several voices calls * it once per voice (loadCjkFonts({ 'LXGW WenKai TC': ['400'] }, quotes)), * so the heading and quotation faces fetch and check only their own * characters. Fails when a character of `text` is in no file of a face. * List every weight the pages use: a weight left to the engine's later round * gets the latin file only. With { vertical: true } it also loads each * family's vertical forms (brackets, quotes, pause marks) for the canvas, * which needs loadVerticalAlternates imported from postext. Resolves to * the number of files loaded. */ async function loadCjkFonts(faces, text, { vertical = false } = {}) { kitStatus('Loading fonts…'); let loaded = 0; try { if (vertical && typeof loadVerticalAlternates !== 'function') { throw new Error('loadCjkFonts(…, { vertical: true }) needs loadVerticalAlternates imported from postext'); } for (const [family, specs] of Object.entries(faces)) { if (!(await isCjkFamily(family))) continue; const twin = []; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; const slices = await cjkSlices(family, weight, style); const missing = [...new Set(text)].filter((ch) => /\S/.test(ch) && !cjkSliceFor(slices, ch.codePointAt(0))); if (missing.length) { throw new Error(`${family} ${spec} has no file for ${missing.slice(0, 12).join(' ')}: ` + `give each face the text it sets (loadCjkFonts({ '${family}': ['${spec}'] }, text))`); } for (const slice of slices) { document.fonts.add(new FontFace(family, `url(${slice.url}) format('woff2')`, { weight: String(weight), style, unicodeRange: slice.range })); twin.push({ source: slice.url, weight: String(weight), style, unicodeRange: slice.range }); } const font = `${style === 'italic' ? 'italic ' : ''}${weight} 16px "${family}"`; loaded += (await document.fonts.load(font, text)).length; if (!document.fonts.check(font, text)) throw new Error(`${family} ${spec} did not load for the sample`); } // The same files under a twin name with the `vert` feature on: the // canvas paints the punctuation of vertical lines with it. if (vertical && twin.length) await loadVerticalAlternates(family, twin); } } catch (error) { kitFail(error); throw error; } return loaded; } /** The PDF font provider for recipes with CJK faces: a family whose * Fontsource subsets are Chinese, Japanese or Korean gets the files the * screen set its pages' characters in (`request.codePoints`), each marked * with its unicode-range and its place in the order loadCjkFonts added * them: postext-pdf sets a character two files hold in the one added * last, as the browser does. Any other family gets the latin file * fontsourceProvider fetches (the "pdf" block) and, when the face sets * letters only latin-ext has, that file too. */ async function cjkPdfProvider(family, weight, style, request) { if (!(await isCjkFamily(family))) return cjkLatinPdfFiles(family, weight, style, request); const meta = await fontsourceMeta(family); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const slices = await cjkSlices(family, w, s); const picked = new Set(); for (const cp of request?.codePoints ?? []) { const slice = cjkSliceFor(slices, cp); if (slice) picked.add(slice); } if (!picked.size) picked.add(slices[0]); return Promise.all(slices.map(async (slice, order) => { if (!picked.has(slice)) return null; const res = await fetch(slice.url); if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`); const bytes = await decompressWoff2(new Uint8Array(await res.arrayBuffer())); return Object.assign(bytes, { unicodeRange: slice.range, order }); })).then((files) => files.filter(Boolean)); } /** A Latin family set next to the CJK faces: its latin file, then its * latin-ext file when the face sets letters only latin-ext has (ō ū in * Hepburn rōmaji, ǎ in pinyin), the file loadFonts adds on screen for * them, each marked with its unicode-range and its place in that order: * the PDF sets a character in the file the browser took it from. A face * Fontsource ships without latin-ext, or whose file does not come, gets * latin alone, and the PDF names the letters it lacks. */ async function cjkLatinPdfFiles(family, weight, style, request) { const meta = await fontsourceMeta(family); const beyond = [...(request?.codePoints ?? [])].some(cjkLatinExtOnly); if (!beyond || !meta?.subsets?.includes('latin-ext')) return fontsourceProvider(family, weight, style); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const id = fontsourceId(family); const url = `${await fontsourceAt(id)}files/${id}-latin-ext-${w}-${s}.woff2`; const [latin, ext] = await Promise.all([fontsourceProvider(family, weight, style), fetch(url) .then(async (res) => (res.ok ? decompressWoff2(new Uint8Array(await res.arrayBuffer())) : null), () => null)]); const ranges = kitRanges(); return ext ? [Object.assign(latin, { unicodeRange: ranges.latin, order: 0 }), Object.assign(ext, { unicodeRange: ranges['latin-ext'], order: 1 })] : latin; } /** Whether code point `cp` is in Fontsource's latin-ext file and not in * its latin file: Latin Extended-A and -B, IPA, the spacing modifiers and * Latin Extended Additional (loadFonts's test for latin-ext), less the * few latin holds too (ı Œ œ ʻ ʼ ˆ ˚ ˜). */ function cjkLatinExtOnly(cp) { if (!((cp >= 0x100 && cp <= 0x2ff) || (cp >= 0x1e00 && cp <= 0x1eff))) return false; return ![0x131, 0x152, 0x153, 0x2bb, 0x2bc, 0x2c6, 0x2da, 0x2dc].includes(cp); } /** showPages for a book bound on either edge. A right-bound book (the * document says so: doc.binding is 'right' for page.binding 'right' and * for vertical text) lies on the desk as it opens: page 1 alone on the * left of the spine, then [3 | 2], the spine shade on each page's inner * edge. `binding` ('left' | 'right') overrides the document's. */ function showBook(docs, { binding, ...options } = {}) { const count = showPages(docs, options); const right = (binding ?? [docs].flat()[0]?.binding) === 'right'; if (!document.getElementById('pt-kit-cjk')) { // The pages keep direction ltr: a canvas draws text in the direction its // element inherits, and under rtl each run would end where the engine // starts it, its brackets mirrored. document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit-cjk"> .pt-spread[dir="rtl"] canvas { direction: ltr; } .pt-spread[dir="rtl"] figure:first-child canvas { box-shadow: inset 14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } </style>`); } // Each pair stays [verso, recto] in the page; right to left, the verso // sits on the right. Phones stack the pages in reading order either way. for (const spread of document.querySelectorAll('#pages > .pt-spread')) spread.dir = right ? 'rtl' : 'ltr'; document.getElementById('pages').dataset.binding = right ? 'right' : 'left'; return count; } // ─── /Kit ───────────────────────────────────────────────────────────────────────

Das zusammengesetzte script.js läuft unverändert: Fügen Sie es in das Modulskript einer beliebigen Seite ein, oder öffnen Sie das Rezept auf CodePen. Ordner des Rezepts auf GitHub ↗ (öffnet in neuem Tab)

Varianten

#Die Folgezeilen unter den Text hängen

Moderne Gesetzbücher setzen die zweite Zeile eines Artikels unter sein erstes Textzeichen, nicht unter 第.

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

#Die Artikel in Daiji nummerieren

Manche Rechts- und Finanztexte schreiben 壱, 弐, 参: Das Token 壱 oder der Stil japanese-formal ergibt 第壱條, 第弐條.

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

Häufige Fehler

Häufiger Fehler

Zeichnen Sie japanischen Text mit 'ja' aus, nie mit zh-Hans oder LANG

Die Ausgaben eines Rezepts sind en und es, ein japanisches Beispiel ist aber in beiden Japanisch: `locale: LANG` würde es als Englisch oder Spanisch auszeichnen, und eine chinesische Auszeichnung würde es nach chinesischen Regeln setzen (Kaiming-Interpunktion, kleine Kana dürfen eine Zeile beginnen, 图 statt 図, chinesische Glyphenformen im PDF). Seit postext 1.30 ist die Auszeichnung auch die Sprache, in der der Text geformt wird (gemessen, gezeichnet und eingebettet). Jede Auszeichnung außer 'ja' liefert daher andere Glyphenformen als die japanischen (unter einer chinesischen Auszeichnung die chinesischen) und andere Zeilenumbrüche. Schreiben Sie 'ja': Das wählt die Region Japan (JLReq-Zeilenumbruch und -Interpunktion, Sesam-Betonungszeichen, Furigana-Abstände, die Beschriftungen 図 und 表) und schaltet die Silbentrennung ab. Der Lint schlägt bei einem Text mit Kana unter einer zh- oder ko-Auszeichnung fehl. Japanischer Zeilenumbruch (Kinsoku) →

Häufiger Fehler

Setzen Sie Japanisch in einer japanischen Schrift

Noto Serif SC und TC haben Kana, zeichnen die Kanji aber in chinesischen Formen (直, 骨 und 角 unterscheiden sich) und die Kana in chinesischer Gestaltung. Setzen Sie den Text in Noto Serif JP oder Shippori Mincho B1 und die Überschriften in Noto Sans JP, geladen mit loadCjkFonts. Die japanischen Dateien von Fontsource enthalten keine Hentaigana oder andere historische Kana (U+1B000–1B16F): loadCjkFonts scheitert daran und das PDF druckt Kästchen. Schreiben Sie also die modernen Kana oder liefern Sie eine Schrift, die sie hat, in den Assets des Rezepts mit. Shippori Mincho fehlen außerdem die Makronvokale ō und ū: Setzen Sie Rōmaji in einer lateinischen Schrift. Kana, Kanji und Rōmaji →

Häufiger Fehler

Chinesische Schnitte werden in Teilstücken geladen, über den Block cjk

Fontsource liefert eine chinesische, japanische oder koreanische Familie als etwa hundert Dateien pro Schriftstärke aus, jede für einen Zeichenbereich. loadFonts holt nur die Datei latin, am Bildschirm stammen die Han-Zeichen also aus einer Systemschrift und werden falsch gemessen, und fontsourceProvider gibt dem PDF diese latin-Datei, die sie als leere Kästchen druckt. Führen Sie den Kit-Block cjk auf, rufen Sie loadCjkFonts(FONTS, markdown) nach loadFonts auf (einmal pro Stimme, mit dem Text, den sie setzt, wenn das Buch mehrere CJK-Schriften verwendet), und geben Sie renderToPdf fontProvider: cjkPdfProvider: Beide nehmen die Dateien, die die Zeichen des Textes enthalten. Chinesische, japanische und koreanische Schriften →

  • Das Amtsblatt setzt die Katakana der Endungen kleiner als die Kanji und sperrt die Titel der Minister gleichmäßig auf eine Breite (字取り). Postext setzt jedes Zeichen im Grad des Grundtexts, und seine gleichmäßige Sperrung jidori gilt nur für Überschriften; die Gegenzeichnungen sind hier deshalb nur am Fuß der Zeile ausgerichtet.
  • Japanisch und Chinesisch verstehen diesen Zähler verschieden: In einem ja-Dokument bedeuten das Token 一 und {2:一} den Stil japanese-informal (百一, 十八), in einem chinesischen den chinesischen Stil (一百零一, 十八). Das chinesische Lesebuch aus Daten und Akronyme aufrecht im vertikalen Text tippt sein 第一條 von Hand.

Nachweise

Text
Schriften
Shippori Mincho B1 (SIL OFL 1.1) · Noto Sans JP (SIL OFL 1.1)
SandboxPDF