本文へスキップ
レシピ番号30

レシピ集 · 第5章 · 本の構成

著者名入りのアンソロジー

歩くことをめぐる3編の随筆。見出しに著者・年・掲載誌を持たせ、章扉はその3つを、偶数ページの柱と目次は著者名を印字します。

このページの内容
出力
Canvas
レベル
中級
Postext
Postext 1.9.2で検証済み
1.4.1以上が必要
ライセンス
2026年9月26日更新
コードMIT · 本文CC BY 4.0

2–3ページ(全9ページ)

  • 英語の見本:日本語版はまだありません
  • 仕上がり135 × 180 mm
  • 1段
  • Spectral 10/14
  • Gloock
  • Hanken Grotesk
  • 9ページ
  • レベル
  • Postext 1.9.2
  • 組版時間33 ms
  • コード180行

かんたんな説明

歩くことについての随筆3編を集めた小さな本です。各随筆の著者と年は一度書くだけで、目次など本の必要な場所に現れます。

できあがり

『Afoot』の9ページです。歩くことをめぐる3編の随筆、ハズリット(1822年)、ソロー(1862年)、スティーヴンソン(1876年)を収めたポケット版のアンソロジーです。表紙いっぱいに夕暮れの荒野の絵があり、これは拡散モデルで生成したJPEGです。目次では各随筆に錆色の番号、Gloockで組んだ題、ノンブルまで字間を空けたリーダー、その下にヘザー色のイタリックで著者名が付きます。各随筆は番号と題で始まり、字間を空けた大文字の署名と、初出の雑誌名と年を記した行が続きます。偶数ページには著者名、奇数ページには随筆の題が入ります。各随筆の見出しが著者・年・出典を属性として持ち、章扉、柱、目次はそこから印字するので、4編目を加えても設定は変えずにすみます。

このレシピが答える質問

  • 自動で更新される目次(リーダー、ノンブル、著者名、部の行)を追加するには?
  • 章扉に著者名の行、リード文、ドロップキャップ付きの導入文を加えるには?
  • 柱を設定するには(左ページに書名、右ページに章タイトル、ノンブルは小口側)?

手短な答え

script.js · 43–63行コード全体で見る
// Each essay's heading carries its credits, and every {attr.…} below reads them:
//   # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"}
// (a value cannot hold { or }, and one with " goes in single quotes; gotcha: attr-values)
// 1 · The opener: inside a heading's design, {attr.author} is that heading's attribute.
const byline = [
  text('byline', '{attr.author}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
    color: col('heather') }, below('title', 5)),
  text('dateline', '{attr.source}, {attr.year}', { fontFamily: 'Spectral', italic: true,
    fontSize: pt(9.5), color: col('muted'), align: 'left' }, below('byline', 1.2)),
];
// 2 · The verso running head (head() is in the running-heads region): in the page header,
//     {attr.author} is the attribute of the essay the page belongs to.
const versoAuthor = head('verso-author', '{attr.author}', 'even',
  at('page', 'top-left', MARGIN.outer + HEAD.gap, HEAD.y));
// 3 · The contents: toc.subtitle prints the attribute it names as a line under each title.
//     'author' and italic are the defaults; attr is written out so it can become 'source'.
const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
  color: col('heather') };
// Hooked up below: byline → the opener, versoAuthor → header, authorLine → toc.subtitle.
// A heading without author="…" gets an empty byline and running head and no line in the
// contents, and the build gives no warning: check every heading.

材料

併用する機能
用紙の色節ごとの柱
種類
Spectral, Gloock, Hanken Grotesk (SIL OFL 1.1)
素材
  • cover-1200.jpg
  • The cover: a heath and hills at dusk, a painting (Generated With Diffusion Models, オリジナル)

作り方

#1 · 随筆に番号を付け、本文を下げる

script.js · 67–85行コード全体で見る
const opener = { enabled: true,
  // The hairline ends 7 mm above the foot of this reserve, so the text starts on the same grid
  // line under every opener whose title fits on one line (SINK = 14 holds a two-line title).
  minHeight: pt(SINK * LEAD),
  slot: { elements: [
    // {number} prints numberingTemplate '{1:I}': I, II, III. {numberRoman} would print
    // nothing here: it is filled on part pages only (gotcha: heading-number-placeholders).
    text('number', '{number}', { ...gloock, fontSize: pt(34), lineHeight: 1, color: col('rust') },
      at('container', 'top-left', 0, 8)),
    text('title', '{titleText}', { ...gloock, fontSize: pt(26), lineHeight: 1.08,
      color: col('ink') }, below('number', 3, { width: 'fill' })),
    ...byline,
    { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'),
      placement: below('dateline', 6) }, // a horizontal rule runs to the column's edge
  ] } };
const essays = { level: 1, numberingTemplate: '{1:I}', advancedDesign: opener,
  marginBottom: pt(0), // the heading's default margin would add to minHeight
  // Restated (gotcha: headings-drop-h1-break); the cover and contents styles inherit it too.
  breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page

レベルのnumberingTemplate: '{1:I}'で随筆にI、II、IIIと番号を振り、{number}がその数字を題の上に印字します(幅と詳細デザイン)。minHeightは14ptの行を12行分確保し、ヘアラインはその7mm上で終わります。そのため1行の題の下では、どの章扉でも本文が天の縁から79mmの位置で始まります。26ptの題が2行目に入ると9.9mm増え、本文がグリッド1行分下がりますが、SINK = 14にしておけば、1行の題でも2行の題でも本文は同じ行から始まります。marginBottom: pt(0)は、見出しの既定のマージンがこの確保分に加わるのを防ぎます。

#2 · 目次を自動で埋める

script.js · 89–101行コード全体で見る
const ENTRY = 15; // pt: the essay titles in the contents
const MIDDLE = 0.3125; // em: how far Chrome's textBaseline 'middle' sits above Gloock's baseline
const contents = { // passed to the config as `toc`
  // 1.4.1 centres an entry's number 0.3 × the entry size above its baseline (gotcha:
  // toc-number-baseline): at 0.3 × 15 ÷ 0.3125 = 14.4 pt a Gloock numeral stands on it.
  levels: [{ level: 1, fontFamily: 'Gloock', fontSize: pt(ENTRY), color: col('ink'),
    numberFontSize: pt((0.3 * ENTRY) / MIDDLE), numberFontWeight: 400, // Gloock has one weight
    numberColor: col('rust'), numberWidth: mm(8), numberGap: mm(3), marginBottom: pt(LEAD) }],
  pageNumber: { fontFamily: 'Hanken Grotesk', fontSize: pt(8.5), fontWeight: 600,
    color: col('muted'), width: mm(6) }, // the leader dots take this face and colour too
  leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up
  subtitle: authorLine,
};

Markdownに書いた:::tocは、対象の見出しごとに1項目を印字します。番号、題、リーダー、ページラベル、そしてsubtitleを通じて著者名です。buildDocumentはページラベルが変わらなくなるまで本を組み直すので、随筆の題を変えたり長くしたりすれば目次も更新されます(目次)。Postext 1.4.1は項目の番号の中心を、ベースラインから項目のサイズの0.3倍上に置きます。ChromeはGloockの中心をベースラインから0.3125em上に置くので、0.3 × 15 ÷ 0.3125 = 14.4ptの数字なら15ptの題のベースラインに乗ります。12ptでは0.3mm浮きます。部のある本では、:::partごとにtoc.parts.designで描く行も加わります。色で分ける部を参照してください。

#3 · 偶数ページに著者、奇数ページに随筆

script.js · 105–121行コード全体で見る
// Each head: the label face, the pages of its parity, never an opener (pages: 'body'), where
// the byline names the author. A function declaration, so the answer above can call it.
function head(id, content, parity, placement, look = {}) {
  return { ...text(id, content, { ...label, fontSize: pt(7.5), letterSpacing: pt(1.3),
    color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD.y), folio),
  versoAuthor,
  head('recto-title', '{chapterTitle}', 'odd',
    at('page', 'top-right', -(MARGIN.outer + HEAD.gap), HEAD.y), { align: 'right' }),
  head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD.y),
    { ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', // an opener's folio
  at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };

各柱はページに固定し、天の縁から11mmに置きます。どちら側に出すかはparityが決めます。ノンブルは本文の小口側の端にそろえ、著者名や題はそこから7mm内側に置きます。head()の中のpages: 'body'は、署名がすでに著者を示している章扉に柱を出さないためのもので、代わりにフッターのpages: 'opener'がそこに下がりノンブルを印字します(テキスト要素)。{chapterTitle}は随筆の題を番号なしで印字します。

#4 · 表紙と目次を数に入れない

script.js · 125–148行コード全体で見る
// Unnumbered, so the first essay is I; unlisted; and with no running heads or folio. They
// inherit the level's page break, 'any' (gotcha: style-inherits-break).
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
  { kind: 'image', id: 'art', resourceId: 'cover',
    placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
  text('title', '{titleText}', { ...gloock, fontSize: pt(80), lineHeight: 1, color: col('ink') },
    at('page', 'top-left', MARGIN.inner, 22)), // page 1 is a recto: the inner margin is left
  text('subtitle', '{subtitle}', { fontFamily: 'Spectral', italic: true, fontSize: pt(14),
    color: col('ink'), align: 'left' }, below('title', 1)),
  text('authors', '{attr.authors}', { ...label, fontSize: pt(8.5), letterSpacing: pt(2),
    color: col('heather') }, below('subtitle', 5)),
  text('imprint', '{attr.imprint}', { ...label, fontSize: pt(7.5), letterSpacing: pt(1.8),
    color: col('paper') }, at('page', 'bottom-left', MARGIN.inner, -12)),
] } };
// span: 'page' in a one-column book: a design kept in the column is clipped at the column's top
// and bottom edges, which would leave bands of paper above and below the dusk.
const coverStyle = { id: 'cover', ...bare, span: 'page', advancedDesign: cover };
const contentsOpener = { enabled: true, slot: { elements: [ // {title}, {subtitle}: frontmatter
  text('kicker', '{title} · {subtitle}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
    color: col('heather') }, at('container', 'top-left', 0, 8)),
  text('title', '{titleText}', { ...gloock, fontSize: pt(26), color: col('ink') },
    below('kicker', 2.5)),
] } };

numbered: falseにすると表紙と目次はカウンターを進めないので、ハズリットがIになります。toc: falseはこの2つを目次の一覧から外します(見出しスタイル)。どちらのスタイルもレベルのbreakBefore(parityは'any')を受け継ぐので、:::pagebreakなしで目次が表紙に続いて2ページ目に来ます。1段組みの本で表紙スタイルがページ全体にまたがっているのは、段の中にとどめたデザインは段の上下の縁で切り取られ、風景の上に20mmの紙が残ってしまうからです。

#5 · どの色もパレットへのリンクとともに書く

script.js · 13–27行コード全体で見る
const palette = {
  ink: '#241f26', // text: a plum-tinted near-black
  paper: '#f7f2e8', // the page
  heather: '#6b4468', // the bylines and the authors in the contents
  rust: '#a4502a', // the essay numbers
  rule: '#d5cabd', // hairlines
  muted: '#6d6570', // running heads, datelines, page numbers in the contents
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
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' } })),
  // The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];

1.4.1では、colorPaletteはテキストスタイルには届きますがデザイン要素には届きません。そのためcol()は各色のhexをpaletteIdと並べて書き、署名、柱、表紙はそのhexで塗られます。main-colorをインクの色に向けておけば、設定で書き直していない既定値もすべてカバーできます。そうしないと、エンジンの青#295AA3で印字されます。

レシピの全体

Sandbox
// ═══ Postext Cookbook · Nº 030 · Anthology with bylines ═══════════════════════════════
// https://postext.dev/en/cookbook/anthology-with-bylines
// Code: MIT · Text: Hazlitt, Thoreau, Stevenson (public domain) · Cover: diffusion models
// Fonts: Spectral, Gloock, Hanken Grotesk (SIL OFL 1.1) · Needs postext ≥ 1.4.1
import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage }
  from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'anthology-with-bylines';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: heather for the credits, rust for the numbers, plum ink on warm paper
const palette = {
  ink: '#241f26', // text: a plum-tinted near-black
  paper: '#f7f2e8', // the page
  heather: '#6b4468', // the bylines and the authors in the contents
  rust: '#a4502a', // the essay numbers
  rule: '#d5cabd', // hairlines
  muted: '#6d6570', // running heads, datelines, page numbers in the contents
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
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' } })),
  // The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
// #endregion
const TRIM = { width: 135, height: 180 }; // a pocket book, 3 : 4
const MARGIN = { top: 20, bottom: 22, inner: 19, outer: 15 }; // mirrored
const LEAD = 14; // body leading in pt: the baseline grid
const SINK = 12; // grid lines an essay opener reserves above its first line of text
const HEAD = { y: 11, gap: 7 }; // running heads: mm from the top edge, folio to words
const label = { fontFamily: 'Hanken Grotesk', fontWeight: 600, textTransform: 'uppercase',
  align: 'left' }; // design text is centred by default
// One weight, no italic; 'wrap' breaks a long title (gotcha: overflow-ellipsis-default).
const gloock = { fontFamily: 'Gloock', overflow: 'wrap', align: 'left' };
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const below = (id, y, size) => ({ ...at(`#${id}`, 'below', 0, y), ...(size && { size }) });
const text = (id, content, look, placement) => ({ kind: 'text', id, content, ...look, placement });

// #region answer: one set of heading attributes, read in three places
// Each essay's heading carries its credits, and every {attr.…} below reads them:
//   # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"}
// (a value cannot hold { or }, and one with " goes in single quotes; gotcha: attr-values)
// 1 · The opener: inside a heading's design, {attr.author} is that heading's attribute.
const byline = [
  text('byline', '{attr.author}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
    color: col('heather') }, below('title', 5)),
  text('dateline', '{attr.source}, {attr.year}', { fontFamily: 'Spectral', italic: true,
    fontSize: pt(9.5), color: col('muted'), align: 'left' }, below('byline', 1.2)),
];
// 2 · The verso running head (head() is in the running-heads region): in the page header,
//     {attr.author} is the attribute of the essay the page belongs to.
const versoAuthor = head('verso-author', '{attr.author}', 'even',
  at('page', 'top-left', MARGIN.outer + HEAD.gap, HEAD.y));
// 3 · The contents: toc.subtitle prints the attribute it names as a line under each title.
//     'author' and italic are the defaults; attr is written out so it can become 'source'.
const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
  color: col('heather') };
// Hooked up below: byline → the opener, versoAuthor → header, authorLine → toc.subtitle.
// A heading without author="…" gets an empty byline and running head and no line in the
// contents, and the build gives no warning: check every heading.
// #endregion

// #region opener: the essay's number and title, then the byline, over a sunk first line
const opener = { enabled: true,
  // The hairline ends 7 mm above the foot of this reserve, so the text starts on the same grid
  // line under every opener whose title fits on one line (SINK = 14 holds a two-line title).
  minHeight: pt(SINK * LEAD),
  slot: { elements: [
    // {number} prints numberingTemplate '{1:I}': I, II, III. {numberRoman} would print
    // nothing here: it is filled on part pages only (gotcha: heading-number-placeholders).
    text('number', '{number}', { ...gloock, fontSize: pt(34), lineHeight: 1, color: col('rust') },
      at('container', 'top-left', 0, 8)),
    text('title', '{titleText}', { ...gloock, fontSize: pt(26), lineHeight: 1.08,
      color: col('ink') }, below('number', 3, { width: 'fill' })),
    ...byline,
    { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'),
      placement: below('dateline', 6) }, // a horizontal rule runs to the column's edge
  ] } };
const essays = { level: 1, numberingTemplate: '{1:I}', advancedDesign: opener,
  marginBottom: pt(0), // the heading's default margin would add to minHeight
  // Restated (gotcha: headings-drop-h1-break); the cover and contents styles inherit it too.
  breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page
// #endregion

// #region contents: the essays' numbers, titles, leaders and page labels, from the headings
const ENTRY = 15; // pt: the essay titles in the contents
const MIDDLE = 0.3125; // em: how far Chrome's textBaseline 'middle' sits above Gloock's baseline
const contents = { // passed to the config as `toc`
  // 1.4.1 centres an entry's number 0.3 × the entry size above its baseline (gotcha:
  // toc-number-baseline): at 0.3 × 15 ÷ 0.3125 = 14.4 pt a Gloock numeral stands on it.
  levels: [{ level: 1, fontFamily: 'Gloock', fontSize: pt(ENTRY), color: col('ink'),
    numberFontSize: pt((0.3 * ENTRY) / MIDDLE), numberFontWeight: 400, // Gloock has one weight
    numberColor: col('rust'), numberWidth: mm(8), numberGap: mm(3), marginBottom: pt(LEAD) }],
  pageNumber: { fontFamily: 'Hanken Grotesk', fontSize: pt(8.5), fontWeight: 600,
    color: col('muted'), width: mm(6) }, // the leader dots take this face and colour too
  leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up
  subtitle: authorLine,
};
// #endregion

// #region running-heads: author on the verso, essay title on the recto, folios outside
// Each head: the label face, the pages of its parity, never an opener (pages: 'body'), where
// the byline names the author. A function declaration, so the answer above can call it.
function head(id, content, parity, placement, look = {}) {
  return { ...text(id, content, { ...label, fontSize: pt(7.5), letterSpacing: pt(1.3),
    color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD.y), folio),
  versoAuthor,
  head('recto-title', '{chapterTitle}', 'odd',
    at('page', 'top-right', -(MARGIN.outer + HEAD.gap), HEAD.y), { align: 'right' }),
  head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD.y),
    { ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', // an opener's folio
  at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };
// #endregion

// #region front: the cover and the contents page, two headings kept out of the count
// Unnumbered, so the first essay is I; unlisted; and with no running heads or folio. They
// inherit the level's page break, 'any' (gotcha: style-inherits-break).
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
  { kind: 'image', id: 'art', resourceId: 'cover',
    placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
  text('title', '{titleText}', { ...gloock, fontSize: pt(80), lineHeight: 1, color: col('ink') },
    at('page', 'top-left', MARGIN.inner, 22)), // page 1 is a recto: the inner margin is left
  text('subtitle', '{subtitle}', { fontFamily: 'Spectral', italic: true, fontSize: pt(14),
    color: col('ink'), align: 'left' }, below('title', 1)),
  text('authors', '{attr.authors}', { ...label, fontSize: pt(8.5), letterSpacing: pt(2),
    color: col('heather') }, below('subtitle', 5)),
  text('imprint', '{attr.imprint}', { ...label, fontSize: pt(7.5), letterSpacing: pt(1.8),
    color: col('paper') }, at('page', 'bottom-left', MARGIN.inner, -12)),
] } };
// span: 'page' in a one-column book: a design kept in the column is clipped at the column's top
// and bottom edges, which would leave bands of paper above and below the dusk.
const coverStyle = { id: 'cover', ...bare, span: 'page', advancedDesign: cover };
const contentsOpener = { enabled: true, slot: { elements: [ // {title}, {subtitle}: frontmatter
  text('kicker', '{title} · {subtitle}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
    color: col('heather') }, at('container', 'top-left', 0, 8)),
  text('title', '{titleText}', { ...gloock, fontSize: pt(26), color: col('ink') },
    below('kicker', 2.5)),
] } };
// #endregion

const config = () => ({ // a factory: the engine caches resolved configs per object
  colorPalette,
  page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150,
    backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom),
      left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true } }, // left = inner
  layout: { layoutType: 'single' },
  bodyText: { fontFamily: 'Spectral', fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    firstLineIndent: mm(4), indentAfterHeading: false,
    minWordSpacing: 0.7, maxWordSpacing: 1.6, // tighter than the 0.6–2 defaults
    maxRuntTracking: 0 }, // tracking 1.4.1 never paints (gotcha: runt-tracking-unpainted)
  // The hidden heading line is still measured, in this face; otherwise the build needs Open Sans.
  headings: { fontFamily: 'Gloock', fontWeight: 400, levels: [essays] },
  headingStyles: [coverStyle, { id: 'contents', ...bare, advancedDesign: contentsOpener }],
  toc: contents,
  // Quoted verse, one paragraph per line (a paragraph keeps no line breaks, and 1.4.1 prints a
  // Markdown blockquote in a fixed #666666 grey); 'runon' resumes the sentence after it.
  paragraphStyles: [{ id: 'verse', firstLineIndent: mm(8), textAlign: 'left' },
    { id: 'runon', firstLineIndent: pt(0) },
    // In the note, under a :::space: a style's margins do not count inside a box (gotcha:
    // box-paragraph-margins). It takes the note body's indent, 0.
    { id: 'colophon', fontSize: pt(7.5), lineHeight: pt(10.5), color: col('muted') }],
  calloutStyles: [{ id: 'note', placement: 'fixed', backgroundEnabled: false, // the page foot
    stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') },
    padding: { top: mm(3), right: pt(0), bottom: pt(0), left: pt(0) },
    titleStyle: { ...label, fontSize: pt(7.5), letterSpacing: pt(1.5), color: col('heather') },
    body: { fontSize: pt(9), lineHeight: pt(12.5), firstLineIndent: pt(0) } }],
  header, footer,
});

// #region art: the cover, a painting: a JPEG in assets/ cut to the trim's 3 : 4
// The design's image element names the resource by id; loadImage() below registers the file
// under its fileId. A bitmap is declared at its pixels.
const resources = [{ id: 'cover', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'cover-1200.jpg', format: 'jpeg', width: 1200, height: 1600 },
  altText: 'Heath and hills at dusk under a pale apricot sky, a low sun on the horizon, three '
    + 'trees on a ridge and a footpath winding up from the foot of the page through the '
    + 'heather.' }];
// #endregion

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Markdownの見本 · 67行 · content.en.mdtitle: "Afoot" subtitle: "Three essays on walking" --- # Afoot {style="cover" authors="Hazlitt · Thoreau · Stevenson" imprint="The Fieldpath Library"} # Contents {style="contents"} :::toc :::callout{type="note" title="A note on the texts"} Each of these essays was first printed in a magazine, two in London and the third in Boston. Hazlitt’s appeared in *The New Monthly Magazine* in January 1822. Thoreau worked his up from a lecture he first gave in 1851, and *The Atlantic Monthly* printed it in June 1862, a month after his death. Stevenson’s came out in *The Cornhill Magazine* in 1876, and in its second paragraph he quotes Hazlitt by name. Each essay starts at its first line and stops well short of its last, in its author’s own spelling; […] marks a cut within a paragraph. :::space{lines=1} :::paragraphs{style="colophon"} Set in Spectral, Gloock and Hanken Grotesk (SIL Open Font License). The essays are in the public domain, from Project Gutenberg eBooks #3020, #1022 and #386; this note is CC BY 4.0; the cover is a painting. ::: ::: # On Going a Journey {author="William Hazlitt" year="1822" source="The New Monthly Magazine"} One of the pleasantest things in the world is going a journey; but I like to go by myself. I can enjoy society in a room; but out of doors, nature is company enough for me. I am then never less alone than when alone. :::paragraphs{style="verse"} *The fields his study, nature was his book.* ::: I cannot see the wit of walking and talking at the same time. When I am in the country I wish to vegetate like the country. I am not for criticising hedge-rows and black cattle. I go out of town in order to forget the town and all that is in it. There are those who for this purpose go to watering-places, and carry the metropolis with them. I like more elbow-room and fewer encumbrances. I like solitude, when I give myself up to it, for the sake of solitude; nor do I ask for :::paragraphs{style="verse"} *A friend in my retreat,* *Whom I may whisper solitude is sweet.* ::: The soul of a journey is liberty, perfect liberty, to think, feel, do, just as one pleases. We go a journey chiefly to be free of all impediments and of all inconveniences; to leave ourselves behind much more to get rid of others. It is because I want a little breathing-space to muse on indifferent matters, where Contemplation :::paragraphs{style="verse"} *May plume her feathers and let grow her wings,* *That in the various bustle of resort* *Were all too ruffled, and sometimes impair’d,* ::: :::paragraphs{style="runon"} that I absent myself from the town for a while, without feeling at a loss the moment I am left by myself. Instead of a friend in a postchaise or in a Tilbury, to exchange good things with, and vary the same stale topics over again, for once let me have a truce with impertinence. Give me the clear blue sky over my head, and the green turf beneath my feet, a winding road before me, and a three hours’ march to dinner—and then to thinking! It is hard if I cannot start some game on these lone heaths. I laugh, I run, I leap, I sing for joy. […] ::: # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"} I wish to speak a word for Nature, for absolute Freedom and Wildness, as contrasted with a freedom and culture merely civil,—to regard man as an inhabitant, or a part and parcel of Nature, rather than a member of society. I wish to make an extreme statement, if so I may make an emphatic one, for there are enough champions of civilization: the minister and the school committee and every one of you will take care of that. I have met with but one or two persons in the course of my life who understood the art of Walking, that is, of taking walks—who had a genius, so to speak, for sauntering, which word is beautifully derived “from idle people who roved about the country, in the Middle Ages, and asked charity, under pretense of going à la Sainte Terre,” to the Holy Land, till the children exclaimed, “There goes a Sainte-Terrer,” a Saunterer, a Holy-Lander. They who never go to the Holy Land in their walks, as they pretend, are indeed mere idlers and vagabonds; but they who do go there are saunterers in the good sense, such as I mean. Some, however, would derive the word from sans terre without land or a home, which, therefore, in the good sense, will mean, having no particular home, but equally at home everywhere. For this is the secret of successful sauntering. He who sits still in a house all the time may be the greatest vagrant of all; but the saunterer, in the good sense, is no more vagrant than the meandering river, which is all the while sedulously seeking the shortest course to the sea. But I prefer the first, which, indeed, is the most probable derivation. For every walk is a sort of crusade, preached by some Peter the Hermit in us, to go forth and reconquer this Holy Land from the hands of the Infidels. It is true, we are but faint-hearted crusaders, even the walkers, nowadays, who undertake no persevering, never-ending enterprises. Our expeditions are but tours, and come round again at evening to the old hearth-side from which we set out. Half the walk is but retracing our steps. We should go forth on the shortest walk, perchance, in the spirit of undying adventure, never to return,—prepared to send back our embalmed hearts only as relics to our desolate kingdoms. If you are ready to leave father and mother, and brother and sister, and wife and child and friends, and never see them again,—if you have paid your debts, and made your will, and settled all your affairs, and are a free man; then you are ready for a walk. To come down to my own experience, my companion and I, for I sometimes have a companion, take pleasure in fancying ourselves knights of a new, or rather an old, order—not Equestrians or Chevaliers, not Ritters or Riders, but Walkers, a still more ancient and honorable class, I trust. The chivalric and heroic spirit which once belonged to the Rider seems now to reside in, or perchance to have subsided into, the Walker—not the Knight, but Walker Errant. He is a sort of fourth estate, outside of Church and State and People. We have felt that we almost alone hereabouts practiced this noble art; though, to tell the truth, at least if their own assertions are to be received, most of my townsmen would fain walk sometimes, as I do, but they cannot. No wealth can buy the requisite leisure, freedom, and independence which are the capital in this profession. It comes only by the grace of God. It requires a direct dispensation from Heaven to become a walker. You must be born into the family of the Walkers. Ambulator nascitur, non fit. Some of my townsmen, it is true, can remember and have described to me some walks which they took ten years ago, in which they were so blessed as to lose themselves for half an hour in the woods; but I know very well that they have confined themselves to the highway ever since, whatever pretensions they may make to belong to this select class. No doubt they were elevated for a moment as by the reminiscence of a previous state of existence, when even they were foresters and outlaws. # Walking Tours {author="Robert Louis Stevenson" year="1876" source="The Cornhill Magazine"} It must not be imagined that a walking tour, as some would have us fancy, is merely a better or worse way of seeing the country. There are many ways of seeing landscape quite as good; and none more vivid, in spite of canting dilettantes, than from a railway train. But landscape on a walking tour is quite accessory. He who is indeed of the brotherhood does not voyage in quest of the picturesque, but of certain jolly humours—of the hope and spirit with which the march begins at morning, and the peace and spiritual repletion of the evening’s rest. He cannot tell whether he puts his knapsack on, or takes it off, with more delight. […] Now, to be properly enjoyed, a walking tour should be gone upon alone. If you go in a company, or even in pairs, it is no longer a walking tour in anything but name; it is something else and more in the nature of a picnic. A walking tour should be gone upon alone, because freedom is of the essence; because you should be able to stop and go on, and follow this way or that, as the freak takes you; and because you must have your own pace, and neither trot alongside a champion walker, nor mince in time with a girl. And then you must be open to all impressions and let your thoughts take colour from what you see. You should be as a pipe for any wind to play upon. “I cannot see the wit,” says Hazlitt, “of walking and talking at the same time. When I am in the country I wish to vegetate like the country,”—which is the gist of all that can be said upon the matter. There should be no cackle of voices at your elbow, to jar on the meditative silence of the morning. And so long as a man is reasoning he cannot surrender himself to that fine intoxication that comes of much motion in the open air, that begins in a sort of dazzle and sluggishness of the brain, and ends in a peace that passes comprehension.
`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces (gotcha: fonts-first) Spectral: ['400', '400i'], Gloock: ['400'], 'Hanken Grotesk': ['600'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadImage('cover-1200.jpg', asset('cover-1200.jpg')); await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'Anthology with bylines', es: 'Antología con firmas de autor' }) });
キット · core, fonts, viewer, images:全レシピ共通 · 270行// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ───────── function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · images v1 ── recipes with pictures · postext.dev/cookbook ────────── /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── /Kit ───────────────────────────────────────────────────────────────────────

組み立てたscript.jsはそのまま動きます。任意のページのモジュールスクリプトに貼り付けるか、レシピをCodePenで開いてください。 GitHub上のレシピのフォルダー ↗ (新しいタブで開きます)

アレンジ

#各随筆の初出を載せる

toc.subtitleは見出しのどの属性でも印字できるので、同じMarkdownのまま、題の下に各随筆が最初に載った雑誌を並べられます。

-const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
+const authorLine = { enabled: true, attr: 'source', fontFamily: 'Spectral', fontSize: pt(10),

#偶数ページに書名を入れる

偶数ページに書名、奇数ページに章名を入れる本も多くあります。{title}はMarkdownのフロントマターから書名を読み、AFOOTと印字します。

-const versoAuthor = head('verso-author', '{attr.author}', 'even',
+const versoAuthor = head('verso-author', '{title}', 'even',

#どの随筆も奇数ページから始める

parityを'odd'にすると、スティーヴンソンは白紙の8ページのあと9ページに移ります。このとき前付けの2つのスタイルには専用の'any'が必要です。そうしないと目次が'odd'を受け継ぎ、白紙の偶数ページのあと3ページから始まります。

-  breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page
+  breakBefore: { enabled: true, parity: 'odd' } };
-const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
+const bare = { numbered: false, toc: false, breakBefore: { enabled: true, parity: 'any' },
+  header: { elements: [] }, footer: { elements: [] } };

よくあるつまずき

つまずき

属性値には{も}も書けない。"を含む値は一重引用符で囲む

属性値は閉じ波かっこで終わるため、{や}を含められません。二重引用符を含む値は一重引用符で囲みます。ドル記号は問題ありません。 見出しの属性 →

つまずき

headingsオブジェクトを渡すとH1の改ページが消える

既定ではH1は奇数ページへ改ページします(always-odd)。ところがheadingsオブジェクトを渡すと中身にかかわらずこの既定がリセットされ、章は改ページせずに続けて組まれ、span: 'page'も効かなくなります。どの設定でもheadings.levels[0].breakBefore: { enabled: true, parity }を書き直してください。 奇数ページから始まる章 →

つまずき

見出しスタイルはその階層の改ページを受け継ぐ

headingStylesの項目は、指定しなかったフィールドをすべて見出しの階層から受け継ぎ、breakBeforeも例外ではありません。:::pagebreakのあとのH1に付けた目次や奥付のスタイルはparity 'odd'を受け継ぎ、白ページの後ろに置かれます。こうしたスタイルにはbreakBefore: { enabled: false }を指定してください。 見出しスタイル →

つまずき

目次の番号が項目のベースラインより少し上に出る

postext 1.4.1の:::tocは、各項目の番号をテキストのベースラインではなく行の中央に合わせて描くため、章番号がタイトルの横でわずかに浮きます。16 ptのタイトルの横で約0.7 mmで、書体や大きさを変えても同じです。これを動かすtocのオプションはまだないので、印刷する前に目次のページを原寸で確認してください。 目次 →

つまずき

{number}/{chapterNumber}はH1の番号を出す。{numberRoman}は部だけ

{number}と{chapterNumber}は見出しの書式付きの番号を出力しますが、{numberRoman}、{numberDecimal}などの数値のバリエーションが埋まるのは部扉だけです。章番号の書式はnumberingTemplate({1:I})で指定するか、属性として渡してください。 番号付き見出し →

つまずき

パレットを差し替えても、デザイン要素と参照色は変わらない

postext 1.4.1はcolorPaletteをテキストのスタイル(本文、見出し、リスト、キャプション、表、囲み)には反映しますが、ヘッダー、フッター、章扉、部扉の要素と、bodyText.referenceColorには反映しません。これらはpaletteIdの横に書いた16進の色のままです。画面用のダーク版や色替えのためにパレットを差し替えるときは、ビルドの前に、リンクしたすべての色をcolorPaletteから書き直してください。 セマンティックカラーパレット →

つまずき

段落スタイルの前後の空きは囲みの中では効かない

postext 1.4.1では、:::calloutの中に入れ子にした:::paragraphsコンテナーは、スタイルのmarginTopとmarginBottomを無視します。そのため、注の本文の下に置いた小さな文字の行は本文にぴったり接します。スタイルのlineHeightを大きくして1行目の上に空きを作るか、その行を囲みの外に出してください。 段落スタイル →

つまずき

ラントの修正が、描かれないトラッキングを詰めることがある

postext 1.4.1では、段落がラント(最終行に残った短い一語)で終わると、レイアウトはその段落を1行短く組みます。まず語間を詰め、次にmaxRuntTracking(1000分の1 em単位)までの負のトラッキングを使います。CanvasとPDFのレンダラーは0より大きいトラッキングしか描かないため、トラッキングを詰めた段落はトラッキングなしで印字されます。両端そろえの行はその差の分だけ語間を失って詰まって見え、最終行は行長を超えて段の端で切れることがあります。bodyText.maxRuntTracking: 0を設定すれば語間による修正は残るので、それでも出るラントは文を書き換えてください。 ウィドウ、オーファン、ラント →

つまずき

レイアウトの前にすべてのフォントを読み込む

レイアウトはブラウザーが読み込んだフォントで文字を計測し、その幅をキャッシュします。最初のビルドのあとに届いたフォントがあると改行位置が狂い、PDFも画面と一致しなくなります。すべてのウェイトとスタイルを先に読み込み、遅れて届いたときは再ビルドの前にclearMeasurementCache()を呼んでください。 レイアウト前のフォント読み込み →

つまずき

デザインのテキストのoverflowの既定値は'ellipsis-end'

幅に収まらないデザインのテキスト要素は、既定では省略記号で終わります。複数行に折り返したいタイトルにはoverflow: 'wrap'を設定してください。 ページデザインのテキスト・罫・ボックス →

  • ハズリットは文の途中で詩を引用します。段落は改行を保たないので、詩の各行を:::paragraphs{style="verse"}コンテナーの中でそれぞれ1つの段落にし、そのあとに再開する文は字下げなしのrunonスタイルに入れます。Markdownの引用ブロックにすると、固定の#666666のグレーのイタリックで、本文の1行目の字下げが付き、左右の字下げはなしで印字されます。1.4.1にはこれを変える設定がありません。
  • 見出しにない属性は何も印字せず、警告も出ません。author="…"がないと、その随筆の署名と偶数ページの柱は空になり、目次の題の下の行も消えます。
  • ステップ2の14.4ptが成り立つのはGloockだけです。ほかの書体では、Chromeがその書体の中心をベースラインからどれだけ上に置くかを調べます。textBaseline = 'middle'にしてmeasureText('I').alphabeticBaselineを取ると、符号が反転した値がピクセルで返ります。そのうえでnumberFontSizeを、項目のサイズの0.3倍をその距離(em)で割った値にします(Hanken Groteskでは0.2675em)。

クレジット

レシピ
Ignacio Ferro
本文
画像
  • The cover: a heath and hills at dusk, a painting · Generated With Diffusion Models · オリジナル
フォント
Spectral (SIL OFL 1.1) · Gloock (SIL OFL 1.1) · Hanken Grotesk (SIL OFL 1.1)
Sandbox