かんたんな説明
行の並びがそのまま絵に見えるように印刷する、英語の古い詩を2編組みます。祭壇と一対の翼です。各行を中央にそろえるだけで、行の長さが形を描きます。
できあがり
A5判4ページの記念冊子です。A4の紙1枚を二つ折りにし、ジョージ・ハーバートの『The Temple』から形の詩を2編、1633年の初版の綴りで収めます。扉ページは1633年版のものにならい、詩篇第29篇のエピグラフまで再現しています。中を開くと、『The Altar』と『Easter Wings』が向かい合います。どちらの詩も、字下げしたりスペースで詰めたりした行はありません。各行を中央にそろえるだけで、祭壇は細い柱の上に立ち、翼の各連は2語まで細くなってまた広がります。最後のページでは、ハーバートのモットーの下に、1633年の刷り手たちが翼を横向きに組んだいきさつを記した注記があり、奥付は1行ごとに短くなります。すべてをIM Fellの書体で、本文か3つのレベルの見出しのどれかとして組み、名前つきのスタイルもページのデザインも使っていません。スペイン語版では、両方の形を保った1行対1行の訳を組んでいます。
このレシピが答える質問
- 空行を入れても効かないとき、2つのブロックの間に縦のスペースを追加するには?
- 詩を組むには(1行に1詩行、連の間のアキ、折り返した行のぶら下げインデント、ハイフネーションなし)?
- エピグラフ、献辞、署名、大きな引用符の付いたプルクオートを組むには?
- 改ページや改段を強制し、すべての章を右ページから始めるには?
手短な答え
// Postext joins the lines of a Markdown paragraph, so every line of the poem is a paragraph
// of its own, with a blank line after it. Centred, each line keeps its natural width on the
// axis of the measure, and the lengths Herbert gave his lines draw the figure:
// Lord, who createdst man in wealth and store,
//
// Though foolishly he lost the same,
//
// Decaying more and more,
const verse = {
fontFamily: 'IM Fell English', fontSize: pt(12.4), lineHeight: pt(LEAD), color: col('ink'),
textAlign: 'center', // ragged on both sides; 1.4.1 hyphenates justified text only
firstLineIndent: pt(0), // the default 1.5 em moves every line 3.3 mm off the titles' axis
};
材料
- 機能
- 字下げ、行そろえ、段落間隔見出しの階層明示的な縦のアキ改ページと改段奇数ページから始まる章本文の書体太字・イタリックとその色仕上がりサイズ見開きの余白1段組みと2段組み用紙の色セマンティックカラーパレットキャンバス上のページ
- 種類
- IM Fell English, IM Fell Great Primer, IM Fell English SC (SIL OFL 1.1)
- 素材
- なし:図はすべてコードで描画
作り方
#1 · 詩の1行は中央にそろえた1段落
コードは上の手短な答えにあります。PostextはMarkdownの段落の中の行をひとつにつなぐので、詩の行はそれぞれ1段落とし、後ろに空行を置きます。textAlign: 'center'は各行を自然な幅のまま行長の軸に置きます。1.4.1は両端そろえのテキストしかハイフネーションしないので、分割される単語もありません(本文)。各行はその段落の1行目なので、既定の1.5 emの字下げがあると、表題の軸より3.3 mm右へずれます。firstLineIndent: pt(0)で、詩と表題を同じ軸にそろえます。
#2 · 行長はいちばん長い行で決める
// A line too long for the measure wraps, and both halves are centred, which adds a step to
// the outline.
// The widest line, 'Lord, who createdst man in wealth and store,', is 80.7 mm in 12.4 pt
// Fell, so the 110 mm measure takes every line of both poems whole.
const TRIM = { w: 148, h: 210 }; // mm: A5, one A4 sheet folded once
const MARGIN = { top: 22, bottom: 24, inner: 17, outer: 21 }; // mm: the measure is 110 mm
収まらない行は折り返し、前半も後半も中央にそろうので、輪郭にハーバートの図形にはない段差ができます。行長を75 mmにすると、『The Altar』の3行と『Easter Wings』の2行が折り返し、冊子は6ページになります。ここでいちばん幅の広い行は『Easter Wings』の1行目で、英語で80.7 mm、スペイン語で80.0 mmです。A5判の110 mmの行長なら、どの行も折り返さずに収まります。
#3 · 飾りのない見出しを中央に
// headings.textAlign centres every level. The Fell faces ship weight 400 only, so no level
// asks for bold.
const titles = {
fontFamily: 'IM Fell Great Primer', fontWeight: 400, color: col('violet'), textAlign: 'center',
levels: [
// The keepsake's title opens page 1, where a break changes nothing. It is restated because
// any headings object drops it, and a second # title would then run on
// (gotcha: headings-drop-h1-break).
{ level: 1, fontSize: pt(40), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
marginBottom: pt(LEAD), breakBefore: { enabled: true, parity: 'odd' } },
// A poem opens the next page, recto or verso: The Altar (page 2) faces Easter Wings.
// 1.4.1 drops the top margin of a heading that opens a page, so the title is lowered by
// its line box instead: three lines deep, it sets the title about 5 mm down. The default
// half-em margin under it rounds up to a whole line, and each poem starts on line 5.
{ level: 2, fontSize: pt(24), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
breakBefore: { enabled: true, parity: 'any' } },
// The small-capital labels on pages 1 and 4: the sister face at the body size, with no
// margins, since the :::space lines place them.
{ level: 3, fontFamily: 'IM Fell English SC', fontSize: pt(12.4), lineHeight: pt(LEAD),
marginTop: pt(0), marginBottom: pt(0) },
],
};
どのレベルにもデザインはなく、書体の設定だけです。headings.textAlignが3つのレベルすべてを中央にそろえます(見出し)。パリティ'any'で各詩は奇数・偶数を問わず次のページから始まるので、2ページの祭壇と3ページの翼が向かい合います。marginTopでこれらの表題を下げることはできません。1.4.1はページの先頭に来た見出しの上のアキを捨てるからです。代わりに、3行分あるレベル2の行ボックスが各表題を約5 mm下げ、どちらの詩も版面の5行目から始まります。レベル1は文書の先頭にあり、ここでは改ページは要りませんが、2つ目の#表題を置いても奇数ページから始まるように、改ページを書き直してあります。レベル3は、1ページと4ページの4つのラベルを、本文と同じ大きさのIM Fell English SCのスモールキャピタルで組みます。
#4 · アキと改ページはMarkdownで作る
// A blank line adds no space, so every gap on these pages is a :::space{lines=N}, which adds
// N lines of the 17 pt grid (one without the attribute). ## opens a page for each poem, and
// :::pagebreak opens the last page.
const config = () => ({ // a factory: the engine caches resolved configs per object
colorPalette,
page: { // mirror: left is the inner margin; 150 dpi is for the screen
sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150,
backgroundColor: col('paper'),
margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
right: mm(MARGIN.outer), mirror: true },
},
layout: { layoutType: 'single' }, // the default is two columns
// 1.4.1 does not link referenceColor to main-color (gotcha: palette-skips-designs), so a
// :ref added to these pages would print in the default blue.
bodyText: { ...verse, referenceColor: col('ink') },
headings: titles,
header: { elements: [] }, // a folded keepsake of four pages: no running heads,
footer: { elements: [] }, // and no folios
});
段落のあいだの空行ではアキが入らないので、これらのページのアキはすべて:::spaceで作っています。『Easter Wings』の連のあいだに1行、扉ページでは詩篇のラベルの上に5行、エピグラフの下に10行です(:::space)。この行数で、1ページの刊記は版面の最後の行で終わり、詩篇の上のラベルと注記の上のラベルはどちらも13行目、ページの上端から94 mmに立ちます。最後のページは:::pagebreakで始めます(:::pagebreak)。奥付も1行を1段落とし、句の切れ目で改行しているので、4行がしだいに細くなってテールピースの形になります。
レシピの全体
// ═══ Postext Cookbook · Nº 062 · Shaped verse: Herbert's Easter Wings, centred line by line ═ // https://postext.dev/en/cookbook/shaped-verse // Code: MIT · Text: G. Herbert, The Temple, 1633 (PD; EEBO-TCP, CC0) · Spanish version: CC BY 4.0 // Fonts: IM Fell English, Great Primer and English SC (SIL OFL 1.1) · Needs postext ≥ 1.4.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'shaped-verse'; // ─── 1 · Design ───────────────────────────────────────────────────────────── const palette = { // every colour in the config links to one of these ink: '#241d29', // the text: a violet-black violet: '#5c2a6f', // the one accent: the titles and the small-capital labels paper: '#fbf7ee', // the sheet: an uncoated cream }; 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' } })), // Italic and bold take their colour from 'main-color' unless set. With 'main-color' in ink, // the italic subtitle, epigraph and motto print in ink instead of the default blue. { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } }, ]; const LEAD = 17; // pt: the leading of every line, verse and prose alike // #region answer: centred text, one line of verse to a paragraph // Postext joins the lines of a Markdown paragraph, so every line of the poem is a paragraph // of its own, with a blank line after it. Centred, each line keeps its natural width on the // axis of the measure, and the lengths Herbert gave his lines draw the figure: // Lord, who createdst man in wealth and store, // // Though foolishly he lost the same, // // Decaying more and more, const verse = { fontFamily: 'IM Fell English', fontSize: pt(12.4), lineHeight: pt(LEAD), color: col('ink'), textAlign: 'center', // ragged on both sides; 1.4.1 hyphenates justified text only firstLineIndent: pt(0), // the default 1.5 em moves every line 3.3 mm off the titles' axis }; // #endregion // #region measure: an A5 leaf whose measure holds the longest line whole // A line too long for the measure wraps, and both halves are centred, which adds a step to // the outline. // The widest line, 'Lord, who createdst man in wealth and store,', is 80.7 mm in 12.4 pt // Fell, so the 110 mm measure takes every line of both poems whole. const TRIM = { w: 148, h: 210 }; // mm: A5, one A4 sheet folded once const MARGIN = { top: 22, bottom: 24, inner: 17, outer: 21 }; // mm: the measure is 110 mm // #endregion // #region titles: plain headings, centred, in the display face and the accent // headings.textAlign centres every level. The Fell faces ship weight 400 only, so no level // asks for bold. const titles = { fontFamily: 'IM Fell Great Primer', fontWeight: 400, color: col('violet'), textAlign: 'center', levels: [ // The keepsake's title opens page 1, where a break changes nothing. It is restated because // any headings object drops it, and a second # title would then run on // (gotcha: headings-drop-h1-break). { level: 1, fontSize: pt(40), lineHeight: pt(LEAD * 3), textTransform: 'uppercase', marginBottom: pt(LEAD), breakBefore: { enabled: true, parity: 'odd' } }, // A poem opens the next page, recto or verso: The Altar (page 2) faces Easter Wings. // 1.4.1 drops the top margin of a heading that opens a page, so the title is lowered by // its line box instead: three lines deep, it sets the title about 5 mm down. The default // half-em margin under it rounds up to a whole line, and each poem starts on line 5. { level: 2, fontSize: pt(24), lineHeight: pt(LEAD * 3), textTransform: 'uppercase', breakBefore: { enabled: true, parity: 'any' } }, // The small-capital labels on pages 1 and 4: the sister face at the body size, with no // margins, since the :::space lines place them. { level: 3, fontFamily: 'IM Fell English SC', fontSize: pt(12.4), lineHeight: pt(LEAD), marginTop: pt(0), marginBottom: pt(0) }, ], }; // #endregion // #region pages: four pages, spaced and broken from the Markdown // A blank line adds no space, so every gap on these pages is a :::space{lines=N}, which adds // N lines of the 17 pt grid (one without the attribute). ## opens a page for each poem, and // :::pagebreak opens the last page. const config = () => ({ // a factory: the engine caches resolved configs per object colorPalette, page: { // mirror: left is the inner margin; 150 dpi is for the screen sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true }, }, layout: { layoutType: 'single' }, // the default is two columns // 1.4.1 does not link referenceColor to main-color (gotcha: palette-skips-designs), so a // :ref added to these pages would print in the default blue. bodyText: { ...verse, referenceColor: col('ink') }, headings: titles, header: { elements: [] }, // a folded keepsake of four pages: no running heads, footer: { elements: [] }, // and no folios }); // #endregion // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Markdownの見本 · 129行 · content.en.md
title: "The Temple" author: "George Herbert" --- # The Temple *Sacred Poems and Private Ejaculations* :::space By Mr. George Herbert :::space{lines=5} ### Psal. 29. *In his Temple doth every man speak of his honour.* :::space{lines=10} ### Two shaped poems in the text of the first edition Cambridge, 1633 ## The Altar A broken ALTAR, Lord, thy servant reares, Made of a heart, and cemented with teares: Whose parts are as thy hand did frame; No workmans tool hath touch’d the same. A HEART alone Is such a stone, As nothing but Thy pow’r doth cut. Wherefore each part Of my hard heart Meets in this frame, To praise thy name. That if I chance to hold my peace, These stones to praise thee may not cease. O let thy blessed SACRIFICE be mine, And sanctifie this ALTAR to be thine. ## Easter Wings Lord, who createdst man in wealth and store, Though foolishly he lost the same, Decaying more and more, Till he became Most poore: With thee O let me rise As larks, harmoniously, And sing this day thy victories: Then shall the fall further the flight in me. :::space My tender age in sorrow did beginne: And still with sicknesses and shame Thou didst so punish sinne, That I became Most thinne. With thee Let me combine, And feel this day thy victorie: For, if I imp my wing on thine, Affliction shall advance the flight in me. :::pagebreak ### Herbert’s motto *Lesse then the least of Gods mercies.* :::space With it, his printers wrote, he used to conclude “all things that might seem to tend any way to his own honour”. :::space{lines=7} ### A note on the text The poems keep the spelling of the first edition, printed at Cambridge in 1633. There “The Altar” stands upright on page 18, and the two stanzas of “Easter Wings” lie sideways on pages 34 and 35. Here they stand upright; give the booklet a quarter turn and each opens into a pair of wings. :::space{lines=5} Set in IM Fell English, English SC and Great Primer, Igino Marini’s revivals of the types John Fell gathered at Oxford. Text from EEBO-TCP.`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses. Layout measures with the browser's fonts, so the // kit loads them from Fontsource before the first build (gotcha: fonts-first). const FONTS = { 'IM Fell English': ['400', '400i'], 'IM Fell Great Primer': ['400'], 'IM Fell English SC': ['400'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown); showPages(doc, { title: t({ en: 'Easter Wings and The Altar', es: 'Alas de Pascua y El altar' }) });キット · 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上のレシピのフォルダー ↗ (新しいタブで開きます)
アレンジ
#各詩を右ページから始める
パリティを'odd'にすると、『The Altar』と『Easter Wings』はどちらも奇数ページの3ページと5ページから始まり、冊子は6ページになって、2ページと4ページは白ページになります。
- breakBefore: { enabled: true, parity: 'any' } },
+ breakBefore: { enabled: true, parity: 'odd' } },#詩を左そろえで組む
textAlign: 'left'にすると、各行は余白から始まり、2編の詩には右側の輪郭だけが残ります。副題から奥付までのほかの本文も左そろえになり、中央にそろうのは見出しだけです。
- textAlign: 'center', // ragged on both sides; 1.4.1 hyphenates justified text only
+ textAlign: 'left', // ragged right; 1.4.1 hyphenates justified text onlyよくあるつまずき
つまずき
headingsオブジェクトを渡すとH1の改ページが消える
既定ではH1は奇数ページへ改ページします(always-odd)。ところがheadingsオブジェクトを渡すと中身にかかわらずこの既定がリセットされ、章は改ページせずに続けて組まれ、span: 'page'も効かなくなります。どの設定でもheadings.levels[0].breakBefore: { enabled: true, parity }を書き直してください。 奇数ページから始まる章 →
つまずき
行末不ぞろいのテキストはラントを検査されない
optimalLineBreaking、avoidRunts、runtPenalty、runtMinCharactersはKnuth–Plassの改行処理に働きますが、postext 1.4.1がこれを使うのは両端そろえのテキストだけです。左そろえの段落は1行ずつ改行されるため、これらの設定にかかわらず短い一語で終わることがあります。左そろえのテキストの最終行を読み、ラントで終わる段落は書き換えてください。 ウィドウ、オーファン、ラント →
つまずき
レイアウトの前にすべてのフォントを読み込む
レイアウトはブラウザーが読み込んだフォントで文字を計測し、その幅をキャッシュします。最初のビルドのあとに届いたフォントがあると改行位置が狂い、PDFも画面と一致しなくなります。すべてのウェイトとスタイルを先に読み込み、遅れて届いたときは再ビルドの前にclearMeasurementCache()を呼んでください。 レイアウト前のフォント読み込み →
- 行長に収まらない詩の行は折り返し、前半と後半がそれぞれ中央にそろいます。書体の大きさと余白を決める前に、いちばん幅の広い行を測ってください。左そろえで、折り返しを字下げして組む詩については、詩を1行ずつ組むを参照してください。
- 段落の先頭のスペースは取り除かれるので、スペースを詰めて行の形を作ることはできません。各行の幅は語だけで決まります。翻訳では、スペイン語版がしているように、原文とほぼ同じ幅の行を見つける必要があります。
- 中央そろえでは各行が自然な幅を保つので、『The Altar』の天板と台座は四角くなりません。天板の3行目と4行目は最初の2行より狭くなります。両端そろえにしても四角にはなりません。1.4.1は両端そろえの段落の最終行を自然な幅のまま残し、ここではどの行もそれだけで1段落だからです。
- 扉ページと最後のページは、グリッドの最後の行まで埋まっています。扉ページに1行増えると(エピグラフが長くなる、行が折り返す、
:::spaceを大きくするなど)、地のラベルとその下の2行が次のページに送られます。最後のページに1行増えると、奥付の最後の1行だけが5ページ目に送られます。どちらかのページに行を足すときは、その上の:::spaceから1行減らしてください。 - Postextは、1633年の刷り手たちが『Easter Wings』を組んだように文字を横向きに組むことはできません。回転できるのは図と表の全体だけです。ここでは連を正立させて組み、4ページの注記にもそう書いています。
クレジット
- 本文
- “The Altar” and “Easter wings”, the title, subtitle, byline and epigraph of the title page, and Herbert’s motto with a clause of the printers’ preface, from the first edition of The Temple (Cambridge, 1633) in the EEBO-TCP transcription (CC0 1.0); the letters and stops it marks as illegible or unknown at six line ends (four in The Altar, two in Easter wings) are supplied · George Herbert; Text Creation Partnership · パブリックドメイン
- Psalm 29:9 in the Spanish edition, from the Reina-Valera Bible of 1909 · Reina-Valera 1909 · パブリックドメイン
- The Spanish versions of both poems, of the subtitle, the motto and the printers’ clause, and the title-page imprint, the note on the text and the colophon of both editions, written for this recipe · Ignacio Ferro · CC BY 4.0
- フォント
- IM Fell English (SIL OFL 1.1) · IM Fell Great Primer (SIL OFL 1.1) · IM Fell English SC (SIL OFL 1.1)


