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

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

1つのテキストを4つの引用スタイルで

6つの引用を含む短いエッセイを、APA 7、Chicagoの著者・年方式、IEEE、ISO 690で組みます。変わるのは引用スタイルの設定だけです。

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

1–1ページ · 1–2/4

  • 英語の見本:日本語版はまだありません
  • 仕上がり155 × 235 mm
  • 1段
  • Source Serif 4 10/14
  • Source Sans 3
  • 4ページ
  • レベル
  • Postext 1.12.0
  • 組版時間298 ms
  • コード113行

かんたんな説明

タイポグラフィーについての同じ2段落を、同じ出典とともに4回印刷します。ページごとに出典の示し方が異なり、引用も末尾の一覧もそれに合わせて変わりますが、本文は一度書いただけです。

できあがり

よいタイポグラフィーがなぜ気づかれないのかを論じる2段落のエッセイで、出典は6つです。叙述的な引用、ページ付きの引用、文中で名前を挙げているため著者を省く引用、2つの著作をまとめた引用が含まれます。テキストを155 × 235 mmのページに4回組み、4つのビルドのあいだで変わるのはcitations.styleだけです。APA 7、Chicagoの著者・年方式、IEEE、ISO 690です。各ページは独自の色の帯にスタイル名を掲げるので、並べると比較表として読めます。引用は形、順序、句読点が変わり、一覧は並べ方と名前の書き方が変わりますが、エッセイそのものは同じファイルです。

このレシピが答える質問

  • 本文に手を加えずに、本の引用スタイルを切り替えるには?
  • APA、IEEEなどの引用スタイルで、文献を引用し参考文献を作るには?

手短な答え

script.js · 28–44行コード全体で見る
// Citations are written once, Pandoc's way: [@key], [@a; @b], [-@key] without the author,
// @key [p. 17] inside the sentence. citeproc-js writes them, and the list, in the style named.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const SHOWN = [ // the same text, built once per entry
  { style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) },
  { style: 'chicago-author-date', band: 'chicago', name: 'Chicago',
    system: t({ en: 'author-date', es: 'autor-año' }) },
  { style: 'ieee', band: 'ieee', name: 'IEEE', system: t({ en: 'numbered', es: 'numérico' }) },
  { style: t({ en: 'iso690-author-date-en', es: 'iso690-author-date-es' }), band: 'iso',
    name: 'ISO 690', system: t({ en: 'author-date', es: 'autor-año' }) },
];
const citations = (style) => ({
  style, // 'apa' → '(Warde, 1955)'; 'ieee' → '[1]'; the list is sorted and set to match
  link: true,
  bibliography: { fontSize: em(0.85), lineHeight: pt(11.2), hangingIndent: mm(6),
    entrySpacing: pt(2), labelWidth: mm(6), doi: 'hide' },
});

材料

種類
Source Serif 4, Source Sans 3 (SIL OFL 1.1)
素材
なし:図はすべてコードで描画

作り方

#1 · 設定は1つ

コードは上の手短な答えにあります。引用は本文の中にPandocの記法で一度だけ書き、文献は末尾のBibTeXブロックに置きます。citations.styleがCSLのスタイルを指定し、citeproc-jsがそれに従ってすべての引用とすべての項目を書きます。APAは年の前にコンマを置いて2つの著作を著者順に並べ、Chicagoは書かれた順を保ち、IEEEは最初に引用された順に番号へ置き換え、ISO 690は一覧の姓を大文字で組みます。執筆中の本なら、変更はこれだけです。設定の値を1つ変えるだけで、本文には触れません。

#2 · 4つのビルド、2つの見開き

script.js · 147–154行コード全体で見る
// The style is set for the whole document, so each style is a build of its own.
const docs = [];
for (at = 0; at < SHOWN.length; at++) {
  // pageIndexOffset makes each page the next one of a book, so the desk pairs them:
  // APA beside Chicago, IEEE beside ISO 690.
  const content = { markdown, continuation: { pageIndexOffset: at + 1 } };
  docs.push(await buildWithFonts(() => buildDocument(content, config()), markdown));
}

スタイルは文書全体に効くので、4つの版は同じMarkdownの4回のビルドです。それぞれpageIndexOffsetを1つずつ増やすので、各ページは架空の本の次のページになります。デスクはそれらを2つの見開きに組み合わせ、違いの最も小さい2つの著者・年方式を隣り合わせに置きます。

#3 · 帯にスタイル名を掲げる

script.js · 48–69行コード全体で見る
const opener = (s, i) => ({
  enabled: true,
  minHeight: mm(41), // from the top margin to under the title, 63 mm from the trim
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col(s.band) },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: mm(44) } } },
    { kind: 'text', id: 'kicker', content: `${t({ en: 'Citation style', es: 'Estilo de cita' })}`
      + ` · ${i + 1} / ${SHOWN.length} · ${s.system}`, fontFamily: SANS, fontSize: pt(8),
    fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('paper'),
    align: 'left',
    placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(12) } } },
    { kind: 'text', id: 'name', content: s.name, fontFamily: SERIF, fontSize: pt(54),
      fontWeight: 600, lineHeight: 1, color: col('paper'), align: 'left', overflow: 'clip',
      placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
        size: { width: mm(119), height: 'auto' } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF, fontSize: pt(20),
      fontWeight: 600, lineHeight: 1.15, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(52) },
        size: { width: mm(119), height: 'auto' } } },
  ] },
});

見出しのデザインが、仕上がり線から始まる帯、スタイルの番号と方式を示す肩書き、54 ptの名前を描きます。名前と方式はMarkdownからではなくスクリプトから取ります。テキストは4つの文書すべてで同じだからです。

script.js · 15–22行コード全体で見る
const palette = {
  ink: '#1d1b19', muted: '#66605a', rule: '#cfc8bf', paper: '#ffffff',
  apa: '#2f5d8a', chicago: '#8c2f2b', ieee: '#24476b', iso: '#3c6b4f', // the four bands
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Each document points main-color at its own band, so any default left follows it.
const colorPalette = (band) => Object.entries({ ...palette, 'main-color': palette[band] })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));

各文書はmain-colorを自分の帯の色に向けるので、4つの色はリンクされたパレットの項目になります。設定が指定していない既定値は、どれもそのページの帯の色に従います。

レシピの全体

Sandbox
// ═══ Postext Cookbook · Nº 093 · One text in four citation styles ════════════════
// https://postext.dev/en/cookbook/one-text-four-citation-styles
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Source Serif 4, Source Sans 3 (SIL OFL 1.1) · Needs postext ≥ 1.12.0
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
} from 'https://esm.sh/postext';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'one-text-four-citation-styles';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: one ink, and a colour for each style's band
const palette = {
  ink: '#1d1b19', muted: '#66605a', rule: '#cfc8bf', paper: '#ffffff',
  apa: '#2f5d8a', chicago: '#8c2f2b', ieee: '#24476b', iso: '#3c6b4f', // the four bands
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Each document points main-color at its own band, so any default left follows it.
const colorPalette = (band) => Object.entries({ ...palette, 'main-color': palette[band] })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const [SERIF, SANS] = ['Source Serif 4', 'Source Sans 3'];
const LEAD = 14; // pt

// #region answer: the style is one setting; everything else stays the same
// Citations are written once, Pandoc's way: [@key], [@a; @b], [-@key] without the author,
// @key [p. 17] inside the sentence. citeproc-js writes them, and the list, in the style named.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const SHOWN = [ // the same text, built once per entry
  { style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) },
  { style: 'chicago-author-date', band: 'chicago', name: 'Chicago',
    system: t({ en: 'author-date', es: 'autor-año' }) },
  { style: 'ieee', band: 'ieee', name: 'IEEE', system: t({ en: 'numbered', es: 'numérico' }) },
  { style: t({ en: 'iso690-author-date-en', es: 'iso690-author-date-es' }), band: 'iso',
    name: 'ISO 690', system: t({ en: 'author-date', es: 'autor-año' }) },
];
const citations = (style) => ({
  style, // 'apa' → '(Warde, 1955)'; 'ieee' → '[1]'; the list is sorted and set to match
  link: true,
  bibliography: { fontSize: em(0.85), lineHeight: pt(11.2), hangingIndent: mm(6),
    entrySpacing: pt(2), labelWidth: mm(6), doi: 'hide' },
});
// #endregion

// #region band: the style's name, large, in a band of its colour
const opener = (s, i) => ({
  enabled: true,
  minHeight: mm(41), // from the top margin to under the title, 63 mm from the trim
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col(s.band) },
      placement: { anchor: { to: 'page', edge: 'top-left' },
        size: { width: 'fill', height: mm(44) } } },
    { kind: 'text', id: 'kicker', content: `${t({ en: 'Citation style', es: 'Estilo de cita' })}`
      + ` · ${i + 1} / ${SHOWN.length} · ${s.system}`, fontFamily: SANS, fontSize: pt(8),
    fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('paper'),
    align: 'left',
    placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(12) } } },
    { kind: 'text', id: 'name', content: s.name, fontFamily: SERIF, fontSize: pt(54),
      fontWeight: 600, lineHeight: 1, color: col('paper'), align: 'left', overflow: 'clip',
      placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
        size: { width: mm(119), height: 'auto' } } },
    { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF, fontSize: pt(20),
      fontWeight: 600, lineHeight: 1.15, color: col('ink'), align: 'left', overflow: 'wrap',
      placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(52) },
        size: { width: mm(119), height: 'auto' } } },
  ] },
});
// #endregion

let at = 0; // the entry of SHOWN being built
const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: t({ en: 'en-us', es: 'es' }),
  colorPalette: colorPalette(SHOWN[at].band),
  citations: citations(SHOWN[at].style),
  page: { sizePreset: 'custom', width: mm(155), height: mm(235), dpi: 150,
    margins: { top: mm(22), bottom: mm(20), left: mm(18), right: mm(18) } },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: SERIF, fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: { fontFamily: SERIF, color: col('ink'), levels: [
    { level: 1, breakBefore: { enabled: true, parity: 'any' }, // gotcha: headings-drop-h1-break
      advancedDesign: opener(SHOWN[at], at) },
  ] },
  header: { elements: [] },
  footer: { elements: [
    { kind: 'text', id: 'colophon', content: t({
      en: 'Set in Source Serif 4 and Source Sans 3 (SIL OFL) · Text: original, CC BY 4.0',
      es: 'Compuesto en Source Serif 4 y Source Sans 3 (SIL OFL) · Texto: original, CC BY 4.0' }),
    fontFamily: SANS, fontSize: pt(7), color: col('muted'),
    placement: { anchor: { to: 'page', edge: 'bottom-left' }, offset: { x: mm(18), y: mm(-11) } } },
  ] },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Markdownの見本 · 33行 · content.en.mdtitle: "The invisible page" author: "Postext Cookbook" --- # The page that is not seen Good typography is meant to go unnoticed. @warde1955 put the idea in an image that printers still quote: a page should be a crystal goblet, a clear glass that shows the wine, rather than a cup of gold that hides it. Tschichold argued much the same about the book, which in his essays serves its reader before it serves its designer [-@tschichold1991]. The argument is moral as much as practical, and @bringhurst2004 [p. 17] makes it the first principle of his manual: typography exists to honour the content. The practical half has evidence behind it. Tests of legibility measured how the size of the type, the length of the line and the leading change the speed of reading, and the study of eye movements later explained why an even texture matters: the eye takes in a few letters at each fixation and plans its next jump from the shape of the line ahead [@tinker1963; @rayner1998]. A page that draws attention to itself costs the reader something, however little. None of this makes design a matter of taste alone. A reader who never notices a well-made page notices a badly made one at once, and the grammar of a good one can be taught [@lupton2010]. :::references{format=bibtex} @book{warde1955, author = {Warde, Beatrice}, title = {The Crystal Goblet: Sixteen Essays on Typography}, editor = {Jacob, Henry}, publisher = {Sylvan Press}, address = {London}, year = 1955} @book{tschichold1991, author = {Tschichold, Jan}, title = {The Form of the Book: Essays on the Morality of Good Design}, translator = {Hadeler, Hajo}, publisher = {Hartley \& Marks}, address = {Point Roberts, WA}, year = 1991} @book{bringhurst2004, author = {Bringhurst, Robert}, title = {The Elements of Typographic Style}, edition = {3}, publisher = {Hartley \& Marks}, address = {Point Roberts, WA}, year = 2004} @book{tinker1963, author = {Tinker, Miles A.}, title = {Legibility of Print}, publisher = {Iowa State University Press}, address = {Ames, IA}, year = 1963} @article{rayner1998, author = {Rayner, Keith}, title = {Eye movements in reading and information processing: 20 years of research}, journal = {Psychological Bulletin}, volume = 124, number = 3, pages = {372--422}, year = 1998, doi = {10.1037/0033-2909.124.3.372}} @book{lupton2010, author = {Lupton, Ellen}, title = {Thinking with Type}, edition = {2}, publisher = {Princeton Architectural Press}, address = {New York}, year = 2010} :::
`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Source Serif 4': ['400', '400i', '600', '600i', '700', '700i'], // 700: the list's title 'Source Sans 3': ['400', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); // #region build: one text, four documents, shown as two spreads // The style is set for the whole document, so each style is a build of its own. const docs = []; for (at = 0; at < SHOWN.length; at++) { // pageIndexOffset makes each page the next one of a book, so the desk pairs them: // APA beside Chicago, IEEE beside ISO 690. const content = { markdown, continuation: { pageIndexOffset: at + 1 } }; docs.push(await buildWithFonts(() => buildDocument(content, config()), markdown)); } // #endregion showPages(docs, { title: t({ en: 'One text, four styles', es: 'Un texto, cuatro estilos' }) });
キット · core, fonts, viewer:全レシピ共通 · 235行// ─── 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 ───────────────────────────────────────────────────────────────────────

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

アレンジ

#注方式のスタイルを試す

Chicagoの注と参考文献の方式では、引用はそれぞれ脚注になり、一覧は参考文献になります。この場合も本文は変わりません。

-  { style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) },
+  { style: 'chicago-notes-bibliography', band: 'apa', name: 'Chicago', system: 'notes' },

#番号を上付きで書く

番号方式のスタイルは、一覧はそのままに、NatureやVancouverのように引用を上付きの番号で印刷することもできます。

-  link: true,
+  link: true, marker: 'superscript',

よくあるつまずき

つまずき

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

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

つまずき

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

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

つまずき

フロントマターの値はすべて引用符で囲む

YAMLはtitle: 1984を数値として、日付をDateオブジェクトとして読みます。文字列でない値はプレースホルダーに空で出力され、PDFにもタイトルが付きません。値はすべて引用符で囲んでください(title: "1984")。 文書のメタデータ →

つまずき

設定はオブジェクトの同一性でキャッシュされる。毎回新しいオブジェクトを作る

エンジンは解決済みの設定をオブジェクトの同一性でキャッシュします。そのため、設定をその場で書き換えて再ビルドすると前の結果が再利用されます。ビルドのたびに新しいオブジェクトを作ってください。レシピの設定がファクトリー関数config()になっているのはこのためです。 キャンバス上のページ →

  • 位置指定子は一度だけ書き、各スタイルがそれぞれの語で印刷します。ここでは4つとも「p. 17」です。同梱されているISO 690のスペイン語スタイルは、節や章を含めどの種類の位置指定子にも「p.」を印刷するので、このテキストではページを引用しています。
  • 版の番号はBibTeXに数値で書きます(edition = {3})。そうすれば各スタイルが「3rd ed.」「3.ª ed.」「3.」とそれぞれの形で書きます。「3rd」と書くと、その語がそのまま印刷され、スタイル独自の形が失われます。

クレジット

レシピ
Ignacio Ferro
本文
書き下ろしの文章, CC BY 4.0
フォント
Source Serif 4 (SIL OFL 1.1) · Source Sans 3 (SIL OFL 1.1)
Sandbox