Aller au contenu principal
Recette numéro 125

Recettes · Chapitre 5 · Structure du livre

Un article de recherche japonais cité selon SIST 02

Une note de recherche japonaise sur deux colonnes A4 : citations [@key] numérotées (1), (2, 3) selon SIST 02, liste 参考文献 en japonais, notes de page, 表1 et 図1.

p. 23 · 1 sur 2

  • Exemple en anglais : pas encore d’édition française
  • Format 210 × 297 mm
  • 2 colonnes, Gouttière de 6,3 mm
  • Noto Serif JP 9/15,5
  • Noto Sans JP
  • Shippori Mincho B1
  • 2 pages
  • Niveau
  • Postext 1.30.0
  • Mis en page en 266 ms
  • 179 lignes de code

En bref

Un court article de recherche d’une revue japonaise. L’auteure écrit un code pour chaque source ; Postext les numérote dans l’ordre de citation et imprime la liste des références comme le demandent les revues scientifiques japonaises.

Ce que vous allez composer

Deux pages d’une note de recherche, 号数活字の三つの系列 (« Three series of gōsū type sizes »), tirée d’une revue inventée consacrée aux caractères et à la composition, composée comme les sociétés savantes japonaises composent leurs revues : A4, deux colonnes de 25 caractères sur 45 lignes de Noto Serif JP en 9 pt, le titre en Shippori Mincho B1 sur le bandeau vert de la revue. Les citations suivent SIST 02, le guide des références qu’utilisent les revues japonaises de science et de technologie : chaque ouvrage est numéroté dans l’ordre de sa première citation, (1), (2, 3), et la liste sous 参考文献 donne les noms japonais nom de famille en premier, le lieu avant l’éditeur et l’année à la fin. Un tableau des neuf corps, une figure qui les dessine à l’échelle et deux notes de page portent des libellés japonais. L’article chinois cité selon GB/T 7714 en est le pendant chinois.

Cette recette répond à

  • Comment citer des ouvrages et construire la bibliographie en APA, en IEEE ou dans un autre style de citation ?
  • Comment composer les notes d’un livre japonais : (1) en fin de chapitre, en pied de page ou à côté du texte sur la double page ?

La réponse courte

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

Ingrédients

Typographie
Noto Serif JP, Noto Sans JP, Shippori Mincho B1 (SIL OFL 1.1)
Ressources
  • Figure 1, the three series of type sizes, drawn in code to scale (Postext Cookbook, CC BY 4.0)

Préparation

#1 · Le style, la locale et les notes

Le code est dans la réponse courte plus haut. postext-citeproc fournit SIST 02 sous le nom 'sist02' et la locale CSL ja-JP, qui écrit les termes de la liste en japonais et relie deux auteurs japonais par と, et davantage par ほか, dans une citation narrative (citations). Dans le texte, [@kawada1949; @suzuki2015] imprime (2, 3), et 高橋[@takahashi2012]が place le numéro après le nom, 高橋(8)が, que la page compose avec un quart de cadratin de chaque côté des parenthèses, comme autour de tout mot occidental dans un texte japonais. Le style se distingue du GB/T 7714 chinois par la marque : SIST 02 met le numéro entre parenthèses sur la ligne, GB/T 7714 entre crochets en exposant. Les notes ne demandent rien : locale: 'ja' donne à un document horizontal des notes au pied de la colonne, numérotées à partir de 1 sur chaque page, avec une marque en exposant et un filet long d’un tiers de la colonne.

#2 · Les références dans un bloc CSL-YAML

Les ouvrages sont listés à la fin du Markdown dans :::references{format=csl-yaml}, le format qu’exporte Zotero sous le nom CSL YAML (citer). Les noms japonais s’écrivent en family et given, {family: 鈴木, given: 広光}, et SIST 02 les imprime d’un bloc, 鈴木広光, tandis que Kornicki garde sa virgule : Kornicki, Peter. Une collection va dans collection-title et s’imprime entre parenthèses après l’année ; les deux articles du 日本印刷学会誌 donnent volume, numéro et pages sous la forme vol. 49, no. 2, p. 76–80. Tous les ouvrages existent ; leurs notices peuvent se vérifier dans CiNii.

Page 24 : les entrées (3) à (9), les noms japonais avec le nom de famille en premier, le lieu avant l’éditeur, l’année à la fin.

#3 · Le titre sur le bandeau de la revue

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

Le bloc de titre est la maquette du H1, un style de titre sur les deux colonnes. Le bandeau est une boîte ancrée au format de la page ; l’étiquette, le titre et sa traduction anglaise s’y placent, chacun sous le précédent, si bien qu’un titre sur trois lignes fait descendre la ligne anglaise. L’auteure et son affiliation viennent d’attributs du titre et se placent sous le bandeau.

#4 · Le résumé et ses étiquettes

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

Le résumé est un encadré sur toute la largeur de la page sous une bande verte en haut, ses étiquettes des pastilles en gothique grasse dans la couleur de la revue. Une espace idéographique après chaque étiquette, saisie dans le Markdown, sépare 要 旨 du texte.

#5 · 表1 et 図1 là où ils sont cités

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

defaultResourceTypes('ja') donne 図 et 表 ; l’article les numérote en continu, 表1 et 図1, et non par chapitre : le modèle est donc '{n}'. Les deux sont placés 'here', sous le paragraphe qui les cite, dans la colonne. Les carrés de la figure sont dessinés à la taille du corps qu’ils représentent, de 42 pt pour le 初号 à 4 pt pour le 八号 : on voit ainsi les divisions par deux sur la page.

La recette complète

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

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

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

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

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

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

// Running heads on the body pages: the journal on the verso, the author on the recto.
const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity,
  pages: 'body', fontFamily: GOTHIC, fontSize: pt(7.5), color: col('muted'),
  align: edge.endsWith('left') ? 'left' : 'right', placement: { anchor: { to: 'container',
    edge }, offset: { x: mm(x), y: mm(14) } }, ...extra });
const folio = { fontWeight: 700, color: col('green') };
const header = { elements: [
  head('v-folio', '{pageNumber}', 'even', 'top-left', 0, folio),
  head('v-head', '活字と組版の研究 第12巻 第2号', 'even', 'top-left', 9),
  head('r-head', '森川:号数活字の三つの系列', 'odd', 'top-right', -9),
  head('r-folio', '{pageNumber}', 'odd', 'top-right', 0, folio),
] };

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

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Exemple Markdown · 134 lignes · content.en.mdtitle: "号数活字の三つの系列" --- # 号数活字の三つの系列:\\近代日本の活字の大きさはどう決められたか {style="article" kind="研究ノート" authors="森川 啓" affiliation="架空大学 文学部" english="Three Series of Gōsū Type Sizes: How the Sizes of Modern Japanese Type Were Set"} :::callout{type="abstract"} :chip[要 旨]{style="label"} 明治初期に日本で鋳造が始まった金属活字は、初号から八号までの九つの大きさを「号数」で呼び分けた。本稿では、号数活字の大きさが倍と半分の関係で結ばれた三つの系列からなることを示し、その構造が組版の実務に何をもたらしたかを整理する。また、1962年に日本工業規格が号数をポイントの値で定め直した経緯と、いまも五号に当たる10.5ポイントが和文の標準の大きさとして残っている理由を考察する。 :chip[キーワード]{style="label"} 号数活字、ポイント、明朝体、東京築地活版製造所、JIS Z 8305 ::: ## はじめに 日本語の活字印刷は、近世初期の古活字版のように早くから試みられてきたが[@kornicki1998]、出版の主流となったのは明治に入って金属活字が鋳造されてからである[@kawada1949; @suzuki2015]。その活字の大きさは、いまのようなポイントではなく、「号数」という独自の単位で呼ばれていた。 号数活字の名前は、組版や印刷の歴史を扱う文献にたびたび現れる[@yahagi1976; @komiyama2009]。しかし、九つの大きさがどのような関係で並んでいるのかは、個々の数値ほどには知られていない。本稿では、号数の大きさを系列ごとに整理し、その構造が組版に与えた影響を考える[^term]。 ## 号数活字の成立 号数の体系は、1869年に長崎で本木昌造らが上海の美華書館の技術者から活字鋳造を学んだことに始まるとされる。その後、平野富二が東京築地活版製造所を設け、号数活字は同所の製品として全国に広まった[@itakura2006]。同所の活字見本は、近代日本の書体と字形を知るための基本資料となっている[@fukawa2005]。 ## 三つの系列 号数活字の大きさを[@tbl:sizes]に示す。九つの大きさは、互いに倍と半分の関係で結ばれた三つの系列に分けられる。初号・二号・五号・七号の系列、一号・四号の系列、三号・六号・八号の系列である([@fig:series])。 ::resource{id="tbl:sizes"} この構造には実務上の利点があった。五号の本文に七号のルビを付けると、ルビの大きさはちょうど本文の半分になる。また、二号の文字一つは五号の文字二つ分の幅に当たるので、見出しの字数を本文の字数に換算して版面を割り付けやすい。号数の名前は、大きさの序列というより、この倍数関係を前提とした組版の道具立てだったといえる。 ::resource{id="fig:series"} ## ポイントへの移行 東京築地活版製造所は、1903年にアメリカ式のポイント制による活字を売り出した。それでも号数活字は長く使われ続け、1962年の日本工業規格JIS Z 8305「活字の基準寸法」は、号数をポイントの値で定め直した。この「新号数」では、一号・三号・四号・六号・八号の値が旧来のものからわずかに変わっている[^jis]。 活字の製造方法の変遷については高橋[@takahashi2012]が、近代印刷の導入期の資料については水野[@mizuno2016]が概観している。 ## おわりに 金属活字が使われなくなったいまも、号数の名残は身近に残っている。和文のワープロソフトが本文の既定の大きさとする10.5ポイントは、五号の値そのものである。号数活字の三つの系列は、日本語の組版が長く拠り所としてきた寸法の体系であった。 ## 参考文献 {style="references"} :::bibliography{title=""} [^term]: 本稿でいう「活字」は、鉛合金で鋳造した金属活字を指す。 [^jis]: 旧号数の値は東京築地活版製造所の製品による。関東の印刷所の多くは、規格の制定後も旧号数の活字を使い続けた。 :::paragraphs{style="colophon"} A specimen article written for the Postext Cookbook. The journal, its author and her university are fictitious; the works cited are real. Set in Noto Serif JP, Noto Sans JP and Shippori Mincho B1 (SIL OFL). Text: CC BY 4.0. ::: :::references{format=csl-yaml} - id: suzuki2015 type: book language: ja author: [{family: 鈴木, given: 広光}] title: 日本語活字印刷史 publisher: 名古屋大学出版会 publisher-place: 名古屋 issued: 2015 - id: yahagi1976 type: book language: ja author: [{family: 矢作, given: 勝美}] title: 明朝活字:その歴史と現状 publisher: 平凡社 publisher-place: 東京 issued: 1976 - id: komiyama2009 type: book language: ja author: [{family: 小宮山, given: 博史}] title: 日本語活字ものがたり:草創期の人と書体 publisher: 誠文堂新光社 publisher-place: 東京 collection-title: 文字と組版ライブラリ issued: 2009 - id: itakura2006 type: book language: ja author: [{family: 板倉, given: 雅宣}] title: 活版印刷発達史:東京築地活版製造所の果たした役割 publisher: 印刷朝陽会 publisher-place: 東京 issued: 2006 - id: fukawa2005 type: book language: ja author: [{family: 府川, given: 充男}] title: 聚珍録:圖説=近世・近代日本〈文字-印刷〉文化史 publisher: 三省堂 publisher-place: 東京 issued: 2005 - id: kawada1949 type: book language: ja author: [{family: 川田, given: 久長}] title: 活版印刷史:日本活版印刷史の研究 publisher: 印刷学会出版部 publisher-place: 東京 issued: 1949 - id: kornicki1998 type: book language: en author: [{family: Kornicki, given: Peter}] title: "The book in Japan: a cultural history from the beginnings to the nineteenth century" publisher: Brill publisher-place: Leiden issued: 1998 - id: takahashi2012 type: article-journal language: ja author: [{family: 高橋, given: 恭介}] title: 「活字」の技術変遷から見た印刷の歴史 container-title: 日本印刷学会誌 volume: 49 issue: 2 page: 76-80 issued: 2012 DOI: 10.11413/nig.49.076 - id: mizuno2016 type: article-journal language: ja author: [{family: 水野, given: 雅生}] title: 日本近代印刷事始 container-title: 日本印刷学会誌 volume: 53 issue: 2 page: 104-108 issued: 2016 DOI: 10.11413/nig.53.104 :::
`; // content.<lang>.md: the same Japanese paper in both // #region resources: the table of sizes, and the three series drawn to scale const SIZES = `号数\t旧号数(pt)\t新号数(pt)\t系列 初号\t42\t42\t初号系 一号\t27.5\t26.25\t一号系 二号\t21\t21\t初号系 三号\t16\t15.75\t三号系 四号\t13.75\t13.125\t一号系 五号\t10.5\t10.5\t初号系 六号\t8\t7.875\t三号系 七号\t5.25\t5.25\t初号系 八号\t4\t3.9375\t三号系`; const SERIES = [[42, 21, 10.5, 5.25], [27.5, 13.75], [16, 8, 4]]; // old values, pt const resources = [ { id: 'tbl:sizes', typeId: 'table', kind: 'table', createdAt: 0, updatedAt: 0, placement: { position: 'here' }, caption: '号数活字の大きさ。新号数はJIS Z 8305(1962)による', table: { model: { ...parseTSV(SIZES), headerRowCount: 1, columnWidths: [16, 28, 28, 28] } } }, { id: 'fig:series', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, placement: { position: 'here' }, caption: '号数活字の三つの系列。上から初号系、一号系、三号系。正方形は旧号数の大きさを実寸で示す', altText: 'Three rows of squares drawn to scale: 42, 21, 10.5 and 5.25 pt; 27.5 and 13.75 pt; ' + '16, 8 and 4 pt. Each square is half the one before it.', svg: { fileId: 'series.svg', width: 790, height: 520 } }, ]; // #endregion // #region art: squares at their size in points; the labels name the gothic, loadSvg embeds it const LABELS = `text{font-family:"${GOTHIC}";font-size:2.4px;text-anchor:middle;` + `fill:${palette.muted}}`; function seriesArt() { let out = ''; SERIES.forEach((row, r) => { let x = 2; const base = [17, 33, 47][r]; // mm: the squares of a row stand on one line for (const size of row) { const s = size * PT; // a square as wide as the type body out += `<rect x="${x}" y="${base - s}" width="${s}" height="${s}" fill="${palette.mist}" ` + `stroke="${palette.green}" stroke-width=".25"/><text x="${x + s / 2}" y="${base + 3}">` + `${size}</text>`; x += s + 4; } }); return `<svg xmlns="http://www.w3.org/2000/svg" width="790" height="520" viewBox="0 0 79 52">` + `<style>${LABELS}</style>${out}</svg>`; } // #endregion // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // every face the pages use, loaded before the build 'Noto Serif JP': ['400'], // 明朝: text, abstract, notes, references, author 'Noto Sans JP': ['400', '700'], // ゴシック: journal, heads, labels, captions, table head 'Shippori Mincho B1': ['700'], // the title, a display mincho }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── // Each voice loads the files of what it sets (gotcha: cjk-fonts-slices); the text face also // gets the words SIST 02 adds to the list. const all = (re) => (markdown.match(re) ?? []).join(''); const gothic = `${all(/^#+ .*$/gm)}${all(/:chip\[[^\]]*\]/g)}${SIZES}` + `${resources.map((r) => r.caption).join('')}活字と組版の研究第巻号森川:三つの系列0123456789`; await loadFonts(FONTS, markdown); await loadCjkFonts({ [MINCHO]: FONTS[MINCHO] }, `${markdown}${SIZES}ほかと編巻号頁`); await loadCjkFonts({ [GOTHIC]: FONTS[GOTHIC] }, gothic); await loadCjkFonts({ [TITLE]: FONTS[TITLE] }, all(/^# .*$/gm)); await loadSvg('series.svg', seriesArt()); const doc = await withLoadedFonts(() => buildDocument({ markdown, resources }, config()), { ...kitFonts(FONTS), text: markdown }); showPages(doc, { title: t({ en: 'A Japanese paper cited to SIST 02', es: 'Un artículo japonés citado según SIST 02' }) }); offerPdf(() => renderToPdf(doc, { fontProvider: cjkPdfProvider, resourceBytes: imageBytes }), `${RECIPE}.pdf`);
Kit · core, fonts, viewer, pdf, images, cjk : identique dans toutes les recettes · 504 lignes// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · pdf v3 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType, with their range and * place: the nearest weight the family ships, upright with no italic; * latin, then what its letters need (kitSubsetsFor). */ async function fontsourceProvider(family, weight, style, request) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const text = String.fromCodePoint(...(request?.codePoints ?? [])); const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => { const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())), { unicodeRange: kitRanges()[subset], order }); })); return files.length === 1 ? files[0] : files; } /** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame * shows no PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── Kit · images v3 ── recipes with pictures · postext.dev/cookbook /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25). * `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */ async function loadSvg(fileId, svg, options) { if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces); const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img, options); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** A face's Fontsource files: nearest weight; latin + what the SVG sets. */ async function svgFaces(family, weight, style, { codePoints = [] } = {}) { const [id, m] = [fontsourceId(family), await fontsourceMeta(family)]; const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight; const s = m?.styles.includes(style) === false ? 'normal' : style; return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array( await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer()))); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── Kit · cjk v2 ── Chinese, Japanese and Korean books · postext.dev/cookbook // Fontsource ships a CJK family as about a hundred files per weight, each // declared in its stylesheet with the unicode-range it covers. The screen // loads the files the sample touches; the PDF gets the same files for the // characters its pages set in each face, and embeds each as a subset. // A book bound on the right (vertical text) is shown with its spreads // mirrored: page 1 alone on the left of the spine, then [3 | 2]. /** The files of a Fontsource face, read from its stylesheet: { url, range, * ranges }, the last declared first (the order the browser tries them in). */ function cjkSlices(family, weight, style) { cjkSlices.cache ??= new Map(); const id = fontsourceId(family); const css = `${id}/${weight}${style === 'italic' ? '-italic' : ''}.css`; if (!cjkSlices.cache.has(css)) { let at; cjkSlices.cache.set(css, fontsourceAt(id).then((a) => fetch(at = a + css.slice(id.length + 1))) .then((res) => { if (!res.ok) throw new Error(`Fontsource has no ${family} ${weight} ${style} (${res.status})`); return res.text(); }) .then((text) => [...text.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => { const range = /unicode-range:\s*([^;]+);/.exec(rule)?.[1].trim() ?? 'U+0-10FFFF'; const ranges = range.split(',').map((part) => { const [lo, hi = lo] = part.trim().slice(2).split('-'); return [parseInt(lo, 16), parseInt(hi, 16)]; }); return { url: new URL(/url\(([^)]+?\.woff2)\)/.exec(rule)[1], at).href, range, ranges }; }).reverse())); } return cjkSlices.cache.get(css); } /** The file of `slices` the screen sets code point `cp` in, if any: the * last one loadCjkFonts adds whose range holds it. A numbered file that * repeats a letter of latin is added after latin, and wins it. */ function cjkSliceFor(slices, cp) { const holds = (slice) => slice.ranges.some(([lo, hi]) => cp >= lo && cp <= hi); return slices.reduce((last, slice) => (holds(slice) ? slice : last), undefined); } /** Whether Fontsource serves `family` as a Chinese, Japanese or Korean * family (its subsets name the script). Fails when the API does not * answer: a CJK face taken for a Latin one would paint in a system face. */ async function isCjkFamily(family) { const meta = await fontsourceMeta(family); if (!meta) throw new Error(`api.fontsource.org did not describe ${family}: reload to try again`); return !!meta.subsets?.some((subset) => /^(chinese|japanese|korean)/.test(subset)); } /** faces = { 'Noto Serif TC': ['400', '700'] }, as for loadFonts: the * whole FONTS object may be passed, its other families are left to * loadFonts. Adds one FontFace per file of each CJK face with its * unicodeRange, then loads the files `text` touches. `text` is what the * faces set: the sample for the text face; a book in several voices calls * it once per voice (loadCjkFonts({ 'LXGW WenKai TC': ['400'] }, quotes)), * so the heading and quotation faces fetch and check only their own * characters. Fails when a character of `text` is in no file of a face. * List every weight the pages use: a weight left to the engine's later round * gets the latin file only. With { vertical: true } it also loads each * family's vertical forms (brackets, quotes, pause marks) for the canvas, * which needs loadVerticalAlternates imported from postext. Resolves to * the number of files loaded. */ async function loadCjkFonts(faces, text, { vertical = false } = {}) { kitStatus('Loading fonts…'); let loaded = 0; try { if (vertical && typeof loadVerticalAlternates !== 'function') { throw new Error('loadCjkFonts(…, { vertical: true }) needs loadVerticalAlternates imported from postext'); } for (const [family, specs] of Object.entries(faces)) { if (!(await isCjkFamily(family))) continue; const twin = []; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; const slices = await cjkSlices(family, weight, style); const missing = [...new Set(text)].filter((ch) => /\S/.test(ch) && !cjkSliceFor(slices, ch.codePointAt(0))); if (missing.length) { throw new Error(`${family} ${spec} has no file for ${missing.slice(0, 12).join(' ')}: ` + `give each face the text it sets (loadCjkFonts({ '${family}': ['${spec}'] }, text))`); } for (const slice of slices) { document.fonts.add(new FontFace(family, `url(${slice.url}) format('woff2')`, { weight: String(weight), style, unicodeRange: slice.range })); twin.push({ source: slice.url, weight: String(weight), style, unicodeRange: slice.range }); } const font = `${style === 'italic' ? 'italic ' : ''}${weight} 16px "${family}"`; loaded += (await document.fonts.load(font, text)).length; if (!document.fonts.check(font, text)) throw new Error(`${family} ${spec} did not load for the sample`); } // The same files under a twin name with the `vert` feature on: the // canvas paints the punctuation of vertical lines with it. if (vertical && twin.length) await loadVerticalAlternates(family, twin); } } catch (error) { kitFail(error); throw error; } return loaded; } /** The PDF font provider for recipes with CJK faces: a family whose * Fontsource subsets are Chinese, Japanese or Korean gets the files the * screen set its pages' characters in (`request.codePoints`), each marked * with its unicode-range and its place in the order loadCjkFonts added * them: postext-pdf sets a character two files hold in the one added * last, as the browser does. Any other family gets the latin file * fontsourceProvider fetches (the "pdf" block) and, when the face sets * letters only latin-ext has, that file too. */ async function cjkPdfProvider(family, weight, style, request) { if (!(await isCjkFamily(family))) return cjkLatinPdfFiles(family, weight, style, request); const meta = await fontsourceMeta(family); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const slices = await cjkSlices(family, w, s); const picked = new Set(); for (const cp of request?.codePoints ?? []) { const slice = cjkSliceFor(slices, cp); if (slice) picked.add(slice); } if (!picked.size) picked.add(slices[0]); return Promise.all(slices.map(async (slice, order) => { if (!picked.has(slice)) return null; const res = await fetch(slice.url); if (!res.ok) throw new Error(`Fontsource file ${slice.url} (${res.status})`); const bytes = await decompressWoff2(new Uint8Array(await res.arrayBuffer())); return Object.assign(bytes, { unicodeRange: slice.range, order }); })).then((files) => files.filter(Boolean)); } /** A Latin family set next to the CJK faces: its latin file, then its * latin-ext file when the face sets letters only latin-ext has (ō ū in * Hepburn rōmaji, ǎ in pinyin), the file loadFonts adds on screen for * them, each marked with its unicode-range and its place in that order: * the PDF sets a character in the file the browser took it from. A face * Fontsource ships without latin-ext, or whose file does not come, gets * latin alone, and the PDF names the letters it lacks. */ async function cjkLatinPdfFiles(family, weight, style, request) { const meta = await fontsourceMeta(family); const beyond = [...(request?.codePoints ?? [])].some(cjkLatinExtOnly); if (!beyond || !meta?.subsets?.includes('latin-ext')) return fontsourceProvider(family, weight, style); const weights = meta.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && !meta.styles.includes('italic') ? 'normal' : style; const id = fontsourceId(family); const url = `${await fontsourceAt(id)}files/${id}-latin-ext-${w}-${s}.woff2`; const [latin, ext] = await Promise.all([fontsourceProvider(family, weight, style), fetch(url) .then(async (res) => (res.ok ? decompressWoff2(new Uint8Array(await res.arrayBuffer())) : null), () => null)]); const ranges = kitRanges(); return ext ? [Object.assign(latin, { unicodeRange: ranges.latin, order: 0 }), Object.assign(ext, { unicodeRange: ranges['latin-ext'], order: 1 })] : latin; } /** Whether code point `cp` is in Fontsource's latin-ext file and not in * its latin file: Latin Extended-A and -B, IPA, the spacing modifiers and * Latin Extended Additional (loadFonts's test for latin-ext), less the * few latin holds too (ı Œ œ ʻ ʼ ˆ ˚ ˜). */ function cjkLatinExtOnly(cp) { if (!((cp >= 0x100 && cp <= 0x2ff) || (cp >= 0x1e00 && cp <= 0x1eff))) return false; return ![0x131, 0x152, 0x153, 0x2bb, 0x2bc, 0x2c6, 0x2da, 0x2dc].includes(cp); } /** showPages for a book bound on either edge. A right-bound book (the * document says so: doc.binding is 'right' for page.binding 'right' and * for vertical text) lies on the desk as it opens: page 1 alone on the * left of the spine, then [3 | 2], the spine shade on each page's inner * edge. `binding` ('left' | 'right') overrides the document's. */ function showBook(docs, { binding, ...options } = {}) { const count = showPages(docs, options); const right = (binding ?? [docs].flat()[0]?.binding) === 'right'; if (!document.getElementById('pt-kit-cjk')) { // The pages keep direction ltr: a canvas draws text in the direction its // element inherits, and under rtl each run would end where the engine // starts it, its brackets mirrored. document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit-cjk"> .pt-spread[dir="rtl"] canvas { direction: ltr; } .pt-spread[dir="rtl"] figure:first-child canvas { box-shadow: inset 14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } </style>`); } // Each pair stays [verso, recto] in the page; right to left, the verso // sits on the right. Phones stack the pages in reading order either way. for (const spread of document.querySelectorAll('#pages > .pt-spread')) spread.dir = right ? 'rtl' : 'ltr'; document.getElementById('pages').dataset.binding = right ? 'right' : 'left'; return count; } // ─── /Kit ───────────────────────────────────────────────────────────────────────

Le script.js composé s’exécute tel quel : collez-le dans le script de module de n’importe quelle page, ou ouvrez la recette sur CodePen. Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)

Variantes

#Numéros de citation en exposant

Beaucoup de revues japonaises placent le numéro au-dessus de la ligne. marker: 'superscript' l’imprime en exposant, sans parenthèses, et ne touche pas à la liste.

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

#Notes numérotées sur tout l’article

Un article de quelques pages peut compter ses notes de 1 jusqu’à la fin au lieu de recommencer à chaque page.

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

Erreurs fréquentes

Erreur fréquente

Balisez un texte japonais 'ja', jamais zh-Hans ni LANG

Les éditions d’une recette sont en et es, mais un exemple japonais est en japonais dans les deux : `locale: LANG` le baliserait en anglais ou en espagnol, et une balise chinoise le composerait selon les règles chinoises (ponctuation Kaiming, petits kana libres de commencer une ligne, 图 pour 図, formes de glyphes chinoises dans le PDF). Depuis postext 1.30, la balise est aussi la langue dans laquelle le texte est mis en forme (mesuré, peint et incorporé) : toute balise autre que 'ja' donne d’autres formes de glyphes que les japonaises (les chinoises sous une balise chinoise) et d’autres coupures de ligne. Écrivez 'ja' : elle choisit la région Japon (coupure des lignes et ponctuation JLReq, points d’emphase sésame, espacement des furigana, étiquettes 図 et 表) et désactive la césure. Le lint fait échouer un texte contenant des kana sous une balise zh ou ko. Coupure des lignes en japonais (kinsoku) →

Erreur fréquente

Composez le japonais dans un caractère japonais

Noto Serif SC et TC ont des kana, mais ils dessinent les kanji sous leurs formes chinoises (直, 骨 et 角 diffèrent) et les kana dans un dessin chinois. Composez le texte en Noto Serif JP ou Shippori Mincho B1 et les titres en Noto Sans JP, chargés avec loadCjkFonts. Les fichiers japonais de Fontsource ne contiennent ni hentaigana ni autres kana historiques (U+1B000–1B16F) : loadCjkFonts échoue sur eux et le PDF imprime des cases. Écrivez donc les kana modernes, ou livrez dans les ressources de la recette un caractère qui les possède. Shippori Mincho n’a pas non plus les voyelles à macron ō et ū : composez le rōmaji dans un caractère latin. Kana, kanji et rōmaji →

Erreur fréquente

Les polices chinoises se chargent par tranches, avec le bloc cjk

Fontsource sert une famille chinoise, japonaise ou coréenne sous la forme d’une centaine de fichiers par graisse, chacun couvrant une plage de caractères. loadFonts ne récupère que le fichier latin : à l’écran, les caractères han viennent d’une police système et sont mal mesurés, et fontsourceProvider remet ce fichier latin au PDF, qui les imprime en cases vides. Listez le bloc cjk du kit, appelez loadCjkFonts(FONTS, markdown) après loadFonts (une fois par voix, avec le texte qu’elle compose, quand le livre utilise plusieurs polices CJK) et donnez à renderToPdf fontProvider: cjkPdfProvider : les deux prennent les fichiers qui contiennent les caractères du texte. Polices chinoises, japonaises et coréennes →

  • Écrivez un nom japonais en family et given, ou en un seul literal sans espace. Un nom saisi en une chaîne avec une espace entre nom de famille et prénom, comme le donne souvent BibTeX (author = {夏目 漱石}), est lu nom de famille en premier, mais un nom romanisé, Natsume Soseki, est lu prénom puis nom de famille, comme les noms anglais.
  • Le SIST 02 fourni n’imprime pas de DOI ; un article dont le DOI compte doit le porter dans le texte ou dans une note.

Crédits

Texte
Texte original, CC BY 4.0
Images
  • Figure 1, the three series of type sizes, drawn in code to scale · Postext Cookbook · CC BY 4.0
Polices
Noto Serif JP (SIL OFL 1.1) · Noto Sans JP (SIL OFL 1.1) · Shippori Mincho B1 (SIL OFL 1.1)
SandboxPDF