かんたんな説明
河口の鳥を2部に分けたポケット図鑑です。泥の茶色の部と、ヨシの緑の部があります。部ごとに色をひとつ決めるだけで、扉のページ、つめ見出し、ノンブル、目次の行に色がつく様子を見せます。
できあがり
『Birds of the Estuary』は、生息環境ごとに1部を立てた2部構成のポケット判の野外図鑑です。干潟の部は泥の茶色、ヨシ原の部はヨシの緑で刷り、それぞれの色は、その環境を開く:::partに一度だけ書きます。部扉は裁ち落としまでの色面で、150ptのローマ数字と、その環境の種の一覧をクリーム色で置きます。扉の裏も同じ色で塗り、小口に部のつめ見出しを白抜きで入れます。種のページでは、つめ見出し、ノンブル、キッカー、学名の下の罫、太字の識別点、行頭記号、図版のラベルが部の色になります。目次は河口の風景の帯の下から始まり、部ごとに、その色の帯と部扉のページ番号を置きます。表紙、帯、4点の図版は拡散モデルで生成した絵で、JPEGのビットマップとして読み込んでいます。部のパレットは画像には届かないので、絵の色は絵のままです。
このレシピが答える質問
- 本を部や節に分け、それぞれに専用の色と中扉を付けるには?
- 章扉や白ページで柱を隠したり、空白の偶数ページを部の色で塗ったりするには?
- 自動で更新される目次(リーダー、ノンブル、著者名、部の行)を追加するには?
- 本文や囲みの中で、キーワード(太字やイタリック)に色を付けるには?
手短な答え
// In the Markdown: :::part{number="I" title="The \\ Mudflats" palette="band=#8c5e24"}
// Every colour below that is linked to 'band' takes #8c5e24 until the next part.
const parts = { // passed to the config as `parts`
// A divider opens on a recto by default. The break after it goes to the next recto too, so
// the back of the leaf stays blank and versoDesign paints it (gotcha: verso-design-breakafter).
breakAfter: { parity: 'odd' },
margins: { top: mm(125) }, // the fence's text starts low, under the title
design: { elements: [ // the divider: its container is the whole trim
box('field', col('band'), { ...at('top-left', 0, 0, 'bleed'), ...fill }),
text('part', t({ en: 'Part', es: 'Parte' }), { ...label, fontSize: pt(10),
letterSpacing: pt(2.4), color: col('paper') },
at('top-left', MARGIN.inner, MARGIN.top)), // a recto: the inner margin is on the left
// Roman for the parts, Arabic for the species. {numberRoman} re-formats number="I" (or
// "1"), on part pages only (gotcha: heading-number-placeholders).
text('numeral', '{numberRoman}', { ...display, fontSize: pt(150), lineHeight: 0.9,
color: col('paper') }, below('part', 0)),
// The \\ in the title breaks the line here; the contents and the heads get one line.
text('title', '{titleText}', { ...display, fontSize: pt(46), lineHeight: 0.98,
color: col('paper') }, below('numeral', 2, { width: mm(MEASURE) })),
rule('rule', col('paper'), 1, below('title', 7, { width: mm(14) })),
] },
// The back of the leaf: the same band, edge to edge, the part's tab in reverse at the
// fore-edge (a verso's is on the left) and its name at the foot.
versoDesign: { elements: [
box('field', col('band'), { ...at('top-left', 0, 0, 'bleed'), ...fill }),
text('tab', '{partNumber}', { ...label, fontSize: pt(9), color: col('band'), align: 'center',
box: { backgroundColor: col('paper') } }, { ...at('top-left', 0, TAB.y), size: TAB.size }),
text('name', '{partTitle}', { ...label, fontSize: pt(9), letterSpacing: pt(2.4),
color: col('paper') }, at('bottom-left', MARGIN.outer, -MARGIN.bottom)),
] },
// The fence's list of species, in the paper colour on the band.
bodyStyle: { fontSize: pt(11), color: col('paper'), textAlign: 'left', numberColor: col('paper'),
orderedLists: { fontFamily: 'Barlow Condensed', separator: '', gap: mm(4) } },
};
材料
- 機能
- 部ごとの色部扉目次セマンティックカラーパレット太字・イタリックとその色裁ち落としの帯とツメページの役割ごとの柱柱とノンブルデザインした章扉見出しの属性番号付き見出し見出しスタイル番号のない章タイトル内の改行表紙、扉、奥付ページデザインの画像独自のリソースの種類その場に置く図固定位置の囲みとバッジ改ページと改段
- 併用する機能
- 囲み用紙の色段落スタイルリソースとしての図と表
- 種類
- Alegreya, Zilla Slab, Barlow Condensed (SIL OFL 1.1)
- 素材
cover-1152.jpgcurlew-1200.jpgredshank-1200.jpgreedling-1200.jpgstrip-1536.jpgwarbler-1200.jpg- The cover: a curlew on the estuary mud, reeds in front (Generated With Diffusion Models, オリジナル)
- The contents strip: a curlew and a redshank on the mud, the reedbed beyond (Generated With Diffusion Models, オリジナル)
- Plate 1: Eurasian curlew (Generated With Diffusion Models, オリジナル)
- Plate 2: common redshank (Generated With Diffusion Models, オリジナル)
- Plate 3: bearded reedling (Generated With Diffusion Models, オリジナル)
- Plate 4: common reed warbler (Generated With Diffusion Models, オリジナル)
作り方
#1 · 部が上書きするパレットの項目をひとつ用意する
const palette = {
ink: '#1f2624', // text: a green-tinted near-black
band: '#3c4b4f', // the house slate, before any part; each :::part brings its own
paper: '#f6f3ea', // the page, and the type set on a band
rule: '#d5d1c4', // hairlines
muted: '#61675f', // running heads, Latin names, the colophon
};
// The paletteId is the link a part's palette="band=#…" follows; the hex is written out too,
// as the palette alone would not reach design elements (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' } },
];
部はパレットの項目をidで上書きします。そこで、環境とともに変わるべきものはすべてひとつの項目bandに結びつけます。この項目自身の値はスレート色で、部の外(表紙のキッカーと、目次の下の注記の題)にだけ刷られます。設定の中にほかに同じ16進値の色はありません。col()はidの横に16進値を書きます。Postext 1.4.1では文書のパレットがデザイン要素に届かないからです。部の上書きはidを通じて届きます。
#2 · 部扉、その裏、種の一覧
この手順のコードは上の手短な答えにあります。:::partは奇数ページに部扉を開き、parts.designがそれを仕上がり全体にレイアウトします。フェンスのあいだのMarkdownは、表題の下、紙の上端から125 mmのところから始まります。habitat段落スタイルでその環境について数行、続いてbodyStyleで種の一覧です。versoDesignが扉の裏(色面と白抜きのつめ見出し)を塗るのはそのページが白ページのときだけで、breakAfter: { parity: 'odd' }が最初の種を次の奇数ページへ送ることで、裏を白ページのまま保ちます。{numberRoman}は部のnumber="I"をローマ数字で印字し("1"でも同じIが出ます)、種のほうは1から4までアラビア数字で番号を振ります。表題の中の\\は部扉の上だけで改行します。目次と柱では表題を1行で組みます。
#3 · 本文のアクセントは部に従う
const bodyText = {
fontFamily: 'Alegreya', fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('band'), // the field marks: **Bill**, **Voice.**
italicColor: col('ink'), referenceColor: col('ink'),
firstLineIndent: mm(4), indentAfterHeading: false,
// Tighter than the 0.6–2 defaults. runtMinCharacters counts word spaces, not letters:
// 45 are about 20 letters of Alegreya (the default 20, about 9); a shorter last line is a runt.
minWordSpacing: 0.75, maxWordSpacing: 1.7, runtMinCharacters: 45,
// A runt is fixed with word spacing only: the default also tightens the tracking, which
// 1.4.1 measures but never paints (gotcha: runt-tracking-unpainted).
maxRuntTracking: 0,
};
const unorderedLists = { bulletChar: '▪', color: col('band'), // the field marks' bullets
marginTop: pt(0), marginBottom: pt(0) };
// The plates are numbered with the species, and their label is in the part's colour.
const resourceTypes = [{ id: 'plate', numberingTemplate: '{n}', resetOn: 'never',
counterFormat: 'decimal', ...t({
en: { name: 'Plate', namePlural: 'Plates', shortLabel: 'Pl.', captionPrefix: 'Plate' },
es: { name: 'Lámina', namePlural: 'Láminas', shortLabel: 'Lám.', captionPrefix: 'Lámina' },
}) }];
const captionStyle = { fontSize: pt(8.5), labelColor: col('band'), descriptionItalic: true,
gap: mm(1.8) };
ある:::partから次の部までのあいだ、band自身の値と等しい文字色はすべて部の値に置き換わります(:::partコンテナー)。ここでは太字の識別点、行頭記号、図版のラベルがそれにあたります。本文とイタリックは墨のままです。図版は専用のリソースの種類で、ラベルはPlate、番号はリセットしないので、種と歩調を合わせて1から4まで数えます。
#4 · 種の扉
const species = { enabled: true, slot: { elements: [
text('kicker', '{number} · {attr.status}', { ...label, fontSize: pt(8.5),
letterSpacing: pt(1.7), color: col('band') }, at('top-left', 0, 1, 'container')),
// 'cm' is a unit symbol: it keeps its lower case, so the size is not set in capitals.
text('size', '{attr.size}', { ...label, textTransform: 'none', fontSize: pt(8.5),
letterSpacing: pt(0.5), color: col('muted') }, at('top-right', 0, 1, 'container')),
text('name', '{titleText}', { ...display, fontSize: pt(22), lineHeight: 1.05,
color: col('ink') }, below('kicker', 1.5, { width: 'fill' })),
text('latin', '{attr.latin}', { fontFamily: 'Alegreya', italic: true, fontSize: pt(11.5),
color: col('ink') }, below('name', 0.8)),
rule('rule', col('band'), 0.75, below('latin', 2, { width: mm(MEASURE) })),
] } };
種の扉は見出しの属性を3つ印字します。statusはキッカーの番号の後、sizeはキッカーの右、latinは名前の下です。キッカーと学名の下の罫はbandに結びついているので、ひとつのデザインで両方の環境をまかなえます。見出し自体は改ページを設定しません。各種は:::pagebreakの後か、部の後の改ページの後から始まるので、そのページは本文のページとして数えられ、柱が入ります。大きさはラベルの大文字にせずに組みます。cmは単位記号だからです。
#5 · 目次に部ごとの帯を置く
const contents = {
levels: [{ level: 1, fontFamily: 'Zilla Slab', fontSize: pt(12), fontWeight: 600,
numberFontFamily: 'Barlow Condensed', numberFontWeight: 600, numberColor: col('muted'),
numberWidth: mm(5), numberGap: mm(3), marginTop: pt(4) }],
pageNumber: { fontFamily: 'Barlow Condensed', fontSize: pt(10), fontWeight: 600, width: mm(7) },
leader: { char: '. ' },
subtitle: { enabled: true, attr: 'latin', fontFamily: 'Alegreya', fontSize: pt(9.5),
color: col('muted') }, // the Latin name, in italic by default
// A part row lays out this design with the part's number, title, page and palette.
parts: { height: mm(8.5), marginTop: pt(LEAD), design: { elements: [
box('row', col('band'), { ...at('top-left', 0, 0, 'container'), ...fill }),
text('part', t({ en: 'Part {number}', es: 'Parte {number}' }), { ...label, fontSize: pt(8),
letterSpacing: pt(1.6), color: col('paper') }, at('left', 3, 0, 'container')),
text('title', '{titleText}', { ...display, fontSize: pt(12), color: col('paper') },
at('left', 20, 0, 'container')),
text('page', '{pageNumber}', { ...label, fontSize: pt(10), color: col('paper'),
align: 'right' }, at('right', -2.5, 0, 'container')),
] } },
};
:::tocは:::partごとに1行を設け、その中に、部の番号、表題、部扉のページ、パレットを使ってtoc.parts.designをレイアウトします(目次)。そのため同じ囲みが、干潟では茶色、ヨシ原では緑で刷られます。帯の下の種の行には、番号、点線のリーダー、ページが並び、その下の行にlatin属性の学名が入ります。
#6 · 柱とつめ見出しに部を出す
const head = (id, content, parity, placement, style = {}) => ({ ...text(id, content, { ...label,
fontSize: pt(8), letterSpacing: pt(1.4), color: col('muted'), overflow: 'clip', ...style },
placement), parity, pages: 'body' }); // dividers are 'part' pages, their versos 'blank'
const folio = { fontSize: pt(9), fontWeight: 700, color: col('band') };
const tab = (parity, edge) => head(`tab-${parity}`, '{partNumber}', parity,
{ ...at(edge, 0, TAB.y), size: TAB.size },
{ fontSize: pt(9), color: col('paper'), align: 'center', box: { backgroundColor: col('band') } });
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', at('top-left', MARGIN.outer, HEAD.y), folio),
head('verso-title', BOOK, 'even', at('top-left', MARGIN.outer + HEAD.gap, HEAD.y)),
head('recto-title', '{partTitle}', 'odd', at('top-right', -(MARGIN.outer + HEAD.gap), HEAD.y)),
head('recto-folio', '{pageNumber}', 'odd', at('top-right', -MARGIN.outer, HEAD.y), folio),
tab('even', 'top-left'), tab('odd', 'top-right'), // on the fore-edge, left on a verso
] };
pages: 'body'にすると、柱は本文のページにだけ刷られます。部扉はpartページ、色を敷いたその裏はblankページです。表紙と目次はどちらもページ幅の見出しで始まるので、扉として扱われます。奇数ページの柱は{partTitle}を1行で印字し、小口のつめ見出しは、背景色がbandに結びついた囲みに{partNumber}を印字します。
レシピの全体
// ═══ Postext Cookbook · Nº 019 · Parts in colour from one attribute ═══════════════════════ // https://postext.dev/en/cookbook/parts-in-colour // Code: MIT · Text: original (CC BY 4.0) · Pictures: diffusion models // Fonts: Alegreya, Zilla Slab, Barlow Condensed (SIL OFL 1.1) · Needs postext ≥ 1.7.0 // A pocket field guide to two habitats. Each :::part names its own 'band' colour, and every // colour linked to 'band' takes it: the divider and its verso, the tab, the field marks. import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage, } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'parts-in-colour'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: 'band' is the entry the parts override; the others keep their value const palette = { ink: '#1f2624', // text: a green-tinted near-black band: '#3c4b4f', // the house slate, before any part; each :::part brings its own paper: '#f6f3ea', // the page, and the type set on a band rule: '#d5d1c4', // hairlines muted: '#61675f', // running heads, Latin names, the colophon }; // The paletteId is the link a part's palette="band=#…" follows; the hex is written out too, // as the palette alone would not reach design elements (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: 150, height: 200 }; // a pocket guide const MARGIN = { top: 22, bottom: 20, inner: 19, outer: 21 }; // mirrored; room for a thumb const MEASURE = TRIM.width - MARGIN.inner - MARGIN.outer; // 110 mm, about 70 characters const LEAD = 14.5; // body leading in pt: the baseline grid const STRIP = 44; // the contents' picture strip, from the top edge (mm) const HEAD = { y: 12, gap: 8 }; // running heads from the top edge; folio to title (mm) const TAB = { y: 26, size: { width: mm(8), height: mm(24) } }; // the thumb tab, at the fore-edge const TITLE_DROP = 6; // mm from the foot of the contents strip to the title's box const BOOK = t({ en: 'Birds of the Estuary', es: 'Aves del estuario' }); const label = { fontFamily: 'Barlow Condensed', fontWeight: 600, textTransform: 'uppercase' }; const display = { fontFamily: 'Zilla Slab', fontWeight: 700 }; const at = (edge, x, y, to = 'page') => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); const below = (id, y, size) => ({ ...at('below', 0, y, `#${id}`), ...(size && { size }) }); const fill = { size: { width: 'fill', height: 'fill' } }; const text = (id, content, style, placement) => ({ kind: 'text', id, content, overflow: 'wrap', align: 'left', ...style, placement }); const box = (id, color, placement) => ({ kind: 'box', id, style: { backgroundColor: color }, placement }); const rule = (id, color, w, placement) => ({ kind: 'rule', id, color, thickness: pt(w), placement }); // #region answer: a divider, its painted verso and its list, all in the part's own 'band' // In the Markdown: :::part{number="I" title="The \\ Mudflats" palette="band=#8c5e24"} // Every colour below that is linked to 'band' takes #8c5e24 until the next part. const parts = { // passed to the config as `parts` // A divider opens on a recto by default. The break after it goes to the next recto too, so // the back of the leaf stays blank and versoDesign paints it (gotcha: verso-design-breakafter). breakAfter: { parity: 'odd' }, margins: { top: mm(125) }, // the fence's text starts low, under the title design: { elements: [ // the divider: its container is the whole trim box('field', col('band'), { ...at('top-left', 0, 0, 'bleed'), ...fill }), text('part', t({ en: 'Part', es: 'Parte' }), { ...label, fontSize: pt(10), letterSpacing: pt(2.4), color: col('paper') }, at('top-left', MARGIN.inner, MARGIN.top)), // a recto: the inner margin is on the left // Roman for the parts, Arabic for the species. {numberRoman} re-formats number="I" (or // "1"), on part pages only (gotcha: heading-number-placeholders). text('numeral', '{numberRoman}', { ...display, fontSize: pt(150), lineHeight: 0.9, color: col('paper') }, below('part', 0)), // The \\ in the title breaks the line here; the contents and the heads get one line. text('title', '{titleText}', { ...display, fontSize: pt(46), lineHeight: 0.98, color: col('paper') }, below('numeral', 2, { width: mm(MEASURE) })), rule('rule', col('paper'), 1, below('title', 7, { width: mm(14) })), ] }, // The back of the leaf: the same band, edge to edge, the part's tab in reverse at the // fore-edge (a verso's is on the left) and its name at the foot. versoDesign: { elements: [ box('field', col('band'), { ...at('top-left', 0, 0, 'bleed'), ...fill }), text('tab', '{partNumber}', { ...label, fontSize: pt(9), color: col('band'), align: 'center', box: { backgroundColor: col('paper') } }, { ...at('top-left', 0, TAB.y), size: TAB.size }), text('name', '{partTitle}', { ...label, fontSize: pt(9), letterSpacing: pt(2.4), color: col('paper') }, at('bottom-left', MARGIN.outer, -MARGIN.bottom)), ] }, // The fence's list of species, in the paper colour on the band. bodyStyle: { fontSize: pt(11), color: col('paper'), textAlign: 'left', numberColor: col('paper'), orderedLists: { fontFamily: 'Barlow Condensed', separator: '', gap: mm(4) } }, }; // #endregion // #region flow: the accents of the text, linked to 'band' so that each part retints them const bodyText = { fontFamily: 'Alegreya', fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('band'), // the field marks: **Bill**, **Voice.** italicColor: col('ink'), referenceColor: col('ink'), firstLineIndent: mm(4), indentAfterHeading: false, // Tighter than the 0.6–2 defaults. runtMinCharacters counts word spaces, not letters: // 45 are about 20 letters of Alegreya (the default 20, about 9); a shorter last line is a runt. minWordSpacing: 0.75, maxWordSpacing: 1.7, runtMinCharacters: 45, // A runt is fixed with word spacing only: the default also tightens the tracking, which // 1.4.1 measures but never paints (gotcha: runt-tracking-unpainted). maxRuntTracking: 0, }; const unorderedLists = { bulletChar: '▪', color: col('band'), // the field marks' bullets marginTop: pt(0), marginBottom: pt(0) }; // The plates are numbered with the species, and their label is in the part's colour. const resourceTypes = [{ id: 'plate', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal', ...t({ en: { name: 'Plate', namePlural: 'Plates', shortLabel: 'Pl.', captionPrefix: 'Plate' }, es: { name: 'Lámina', namePlural: 'Láminas', shortLabel: 'Lám.', captionPrefix: 'Lámina' }, }) }]; const captionStyle = { fontSize: pt(8.5), labelColor: col('band'), descriptionItalic: true, gap: mm(1.8) }; // #endregion // #region species: each entry opens with its number and status, name, Latin and a 'band' rule const species = { enabled: true, slot: { elements: [ text('kicker', '{number} · {attr.status}', { ...label, fontSize: pt(8.5), letterSpacing: pt(1.7), color: col('band') }, at('top-left', 0, 1, 'container')), // 'cm' is a unit symbol: it keeps its lower case, so the size is not set in capitals. text('size', '{attr.size}', { ...label, textTransform: 'none', fontSize: pt(8.5), letterSpacing: pt(0.5), color: col('muted') }, at('top-right', 0, 1, 'container')), text('name', '{titleText}', { ...display, fontSize: pt(22), lineHeight: 1.05, color: col('ink') }, below('kicker', 1.5, { width: 'fill' })), text('latin', '{attr.latin}', { fontFamily: 'Alegreya', italic: true, fontSize: pt(11.5), color: col('ink') }, below('name', 0.8)), rule('rule', col('band'), 0.75, below('latin', 2, { width: mm(MEASURE) })), ] } }; // #endregion // #region contents: a band per part in that part's colour, then its species with leaders const contents = { levels: [{ level: 1, fontFamily: 'Zilla Slab', fontSize: pt(12), fontWeight: 600, numberFontFamily: 'Barlow Condensed', numberFontWeight: 600, numberColor: col('muted'), numberWidth: mm(5), numberGap: mm(3), marginTop: pt(4) }], pageNumber: { fontFamily: 'Barlow Condensed', fontSize: pt(10), fontWeight: 600, width: mm(7) }, leader: { char: '. ' }, subtitle: { enabled: true, attr: 'latin', fontFamily: 'Alegreya', fontSize: pt(9.5), color: col('muted') }, // the Latin name, in italic by default // A part row lays out this design with the part's number, title, page and palette. parts: { height: mm(8.5), marginTop: pt(LEAD), design: { elements: [ box('row', col('band'), { ...at('top-left', 0, 0, 'container'), ...fill }), text('part', t({ en: 'Part {number}', es: 'Parte {number}' }), { ...label, fontSize: pt(8), letterSpacing: pt(1.6), color: col('paper') }, at('left', 3, 0, 'container')), text('title', '{titleText}', { ...display, fontSize: pt(12), color: col('paper') }, at('left', 20, 0, 'container')), text('page', '{pageNumber}', { ...label, fontSize: pt(10), color: col('paper'), align: 'right' }, at('right', -2.5, 0, 'container')), ] } }, }; // #endregion // #region running-heads: on body pages only, never on a divider or its verso; a 'band' tab const head = (id, content, parity, placement, style = {}) => ({ ...text(id, content, { ...label, fontSize: pt(8), letterSpacing: pt(1.4), color: col('muted'), overflow: 'clip', ...style }, placement), parity, pages: 'body' }); // dividers are 'part' pages, their versos 'blank' const folio = { fontSize: pt(9), fontWeight: 700, color: col('band') }; const tab = (parity, edge) => head(`tab-${parity}`, '{partNumber}', parity, { ...at(edge, 0, TAB.y), size: TAB.size }, { fontSize: pt(9), color: col('paper'), align: 'center', box: { backgroundColor: col('band') } }); const header = { elements: [ head('verso-folio', '{pageNumber}', 'even', at('top-left', MARGIN.outer, HEAD.y), folio), head('verso-title', BOOK, 'even', at('top-left', MARGIN.outer + HEAD.gap, HEAD.y)), head('recto-title', '{partTitle}', 'odd', at('top-right', -(MARGIN.outer + HEAD.gap), HEAD.y)), head('recto-folio', '{pageNumber}', 'odd', at('top-right', -MARGIN.outer, HEAD.y), folio), tab('even', 'top-left'), tab('odd', 'top-right'), // on the fore-edge, left on a verso ] }; // #endregion // The cover and the contents are headings with no number and no contents entry. They get // no running heads either: a page-wide heading makes its page an 'opener', not 'body'. const unlisted = { numbered: false, toc: false, span: 'page' }; const cover = { enabled: true, slot: { elements: [ { kind: 'image', id: 'art', resourceId: 'cover', placement: { ...at('top-left', 0, 0, 'bleed'), size: { width: 'fill' } } }, text('kicker', '{subtitle}', { ...label, fontSize: pt(9), letterSpacing: pt(1.8), color: col('band') }, at('top-left', MARGIN.inner, MARGIN.top)), // slate: no part yet text('title', '{titleText}', { ...display, fontSize: pt(50), lineHeight: 0.95, color: col('ink') }, below('kicker', 3, { width: mm(120) })), ] } }; // The contents open under a strip of the estuary: mud and waders, then the reeds. const contentsOpener = { enabled: true, minHeight: mm(STRIP), slot: { elements: [ { kind: 'image', id: 'strip', resourceId: 'strip', placement: { ...at('top-left', 0, 0, 'bleed'), size: { width: 'fill' } } }, text('title', '{titleText}', { ...display, fontSize: pt(26), color: col('ink') }, at('top-left', 0, STRIP - MARGIN.top + TITLE_DROP, 'container')), ] } }; const config = () => ({ // a factory: the engine caches resolved configs per object locale: t({ en: 'en-us', es: 'es' }), // hyphenation, by exact code (gotcha: hyphenation-locales) 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 } }, layout: { layoutType: 'single' }, bodyText, unorderedLists, resourceTypes, captionStyle, headings: { ...display, levels: [ // breakBefore stated: the documented H1 page break would make each species page an // 'opener', with no running heads (gotcha: headings-drop-h1-break). :::pagebreak instead. { level: 1, fontSize: pt(22), breakBefore: { enabled: false }, numberingTemplate: '{1}', marginBottom: pt(0), advancedDesign: species }, ] }, headingStyles: [ { id: 'cover', ...unlisted, advancedDesign: cover }, { id: 'contents', ...unlisted, advancedDesign: contentsOpener }, ], toc: contents, parts, paragraphStyles: [ // The habitat's few lines on a divider: no indent, in the paper colour. { id: 'habitat', fontSize: pt(11), color: col('paper'), textAlign: 'left', firstLineIndent: pt(0), marginBottom: pt(LEAD / 2) }, // In the box its margins do not count; the leading adds air (gotcha: box-paragraph-margins). { id: 'colophon', fontFamily: 'Barlow Condensed', fontSize: pt(8), lineHeight: pt(15), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0) }, ], // The note under the contents is pinned to the foot of the text block. calloutStyles: [{ id: 'about', placement: 'fixed', backgroundEnabled: false, 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(8), letterSpacing: pt(1.6), color: col('band') }, body: { fontSize: pt(9.5), lineHeight: pt(13), firstLineIndent: pt(0), textAlign: 'left' } }], header, footer: { elements: [] }, // the default footer would centre a folio in Open Sans }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Markdownの見本 · 95行 · content.en.md
title: "Birds of the Estuary" subtitle: "A pocket guide to the mudflats and the reedbeds" --- # Birds of \\ the Estuary {style="cover"} :::pagebreak # Contents {style="contents"} :::toc :::callout{type="about" title="How to use this guide"} The birds are grouped by habitat: the open mud of the lower estuary, and the reedbeds along its upper reaches. Each habitat has a colour, printed on its tab at the edge of the page, on the field marks in its species texts and on its row in the list above. Sizes are total length, from bill tip to tail tip. :::paragraphs{style="colophon"} Set in Alegreya, Zilla Slab and Barlow Condensed (SIL OFL) · Text: CC BY 4.0 · Plates: diffusion models. ::: ::: :::part{number="I" title="The \\ Mudflats" palette="band=#8c5e24"} :::paragraphs{style="habitat"} Twice a day the tide drains out of the estuary and leaves a mile of shining mud. It looks empty, but every square metre hides thousands of worms, snails and shrimps, and waders fly in from half of Europe to feed on them. ::: 1. Eurasian Curlew 2. Common Redshank ::: # Eurasian Curlew {latin="Numenius arquata" size="50–60 cm" status="Winter visitor"} ::resource{id="curlew"} Our largest wader, and the easiest to name: no other bird on the mud carries so long and so curved a bill. Curlews work the soft mud at low water, walking slowly and probing to the hilt for worms and small crabs. - **Bill** very long and down-curved, longest in the female. - **Plumage** grey-brown, finely streaked; legs grey-blue. - **In flight** a white wedge runs up the back from the tail. **Voice.** A rising, far-carrying *cur-lee*, the sound of an estuary in winter; in spring, a bubbling song on the moors where it breeds. **Where and when.** On the mud from August to March, roosting in flocks on the saltmarsh at high tide. The curlew is declining across Europe and listed as Near Threatened: give a roost a wide berth. :::pagebreak # Common Redshank {latin="Tringa totanus" size="27–29 cm" status="Resident"} ::resource{id="redshank"} The noisiest bird on the marsh. A redshank sees you first and tells everything within half a mile, bobbing nervously and piping as it flies off; it has long been called the warden of the marshes. - **Legs** bright orange-red; the bill red at the base, dark at the tip. - **Plumage** plain grey-brown in winter, mottled browner in summer. - **In flight** a broad white trailing edge to the wing, and white up the back. **Voice.** A ringing *tew-hu-hu*, often the first sound you hear on the marsh, and a frantic *teuk-teuk-teuk* when it is alarmed. **Where and when.** All year round on the creeks and the tideline, picking shrimps and small snails from the mud. It nests in tussocks of saltmarsh grass, often in loose colonies. :::part{number="II" title="The \\ Reedbeds" palette="band=#51702f"} :::paragraphs{style="habitat"} Where the river meets the tide, the common reed grows in beds taller than a person, green in summer and gold in winter. Few birds live in them, and most of those stay hidden among the stems. ::: 3. Bearded Reedling 4. Eurasian Reed Warbler ::: # Bearded Reedling {latin="Panurus biarmicus" size="14.5–17 cm" status="Resident"} ::resource{id="reedling"} A small, long-tailed bird that seldom leaves the reeds, and you will usually hear it first: a pinging call from deep in the reedbed, then a party of birds whirring low over the plumes on short, rounded wings. - **Male** head blue-grey, with a drooping black moustache (and no beard). - **Body** warm tawny; the long, graduated tail is half the bird. - **Female** brown-headed and without the moustache. **Voice.** A metallic *ping*, like a tiny bell, repeated as the party moves. **Where and when.** All year in large reedbeds. It eats insects in summer and reed seeds in winter, and swallows grit in autumn to grind them. Once classed with the tits (Linnaeus named it *Parus biarmicus*), it now has a family of its own, the Panuridae. :::pagebreak # Eurasian Reed Warbler {latin="Acrocephalus scirpaceus" size="13 cm" status="Summer visitor"} ::resource{id="warbler"} A plain brown warbler, heard far more often than seen. Its song runs on from inside the reeds all through a summer day: a slow, rhythmic chatter, *jit-jit-jit, churr-churr*, with phrases borrowed from other birds. - **Upperparts** unstreaked warm brown; underparts buff, throat whitish. - **Head** a flat forehead and a strong, pointed bill. - **Movement** sidles up the stems, often gripping two at once. **Where and when.** A summer visitor from late April to September, wintering in Africa south of the Sahara. Its nest is a deep cup woven round three or four reed stems, and it is one of the cuckoo’s commonest hosts.`; // content.<lang>.md, inlined by the Cookbook // The plates' captions, and the alt text of every drawing. const CAPTIONS = t({ en: { cover: 'A curlew on the mud, with reeds in front.', strip: 'A curlew and a redshank on the mud, with the reedbed beyond.', curlew: 'Adult at low water. The female’s bill is the longer.', redshank: 'Adult by a saltmarsh creek, on the red legs that give it its name.', reedling: 'Male on a reed stem. The female has a plain brown head.', warbler: 'Singing from the reeds, one foot on each stem.', }, es: { cover: 'Un zarapito en el fango, con carrizos delante.', strip: 'Un zarapito y un archibebe en el fango, y el carrizal al fondo.', curlew: 'Adulto en bajamar. La hembra tiene el pico más largo.', redshank: 'Adulto junto a un caño de la marisma, sobre las patas rojas que lo delatan.', reedling: 'Macho en un tallo de carrizo. La hembra tiene la cabeza parda.', warbler: 'Cantando en el carrizal, con una pata en cada tallo.', } }); // Every picture is a JPEG in assets/, cut to the shape of its frame, declared at its pixels. const drawing = (id, [w, h], more) => ({ id, typeId: 'plate', kind: 'bitmap', bitmap: { fileId: `${id}-${w}.jpg`, format: 'jpeg', width: w, height: h }, createdAt: 0, updatedAt: 0, altText: CAPTIONS[id], ...more }); // Plates stand where ::resource{id="…"} is; since 1.5 an inline figure keeps a line of space // below it too. const resources = [drawing('cover', [1152, 1536]), drawing('strip', [1536, 451]), ...['curlew', 'redshank', 'reedling', 'warbler'].map((id) => drawing(id, [1200, 600], { caption: CAPTIONS[id], placement: { position: 'here' } }))]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses, loaded before the first build (gotcha: fonts-first). const FONTS = { Alegreya: ['400', '400i', '700'], 'Zilla Slab': ['600', '700'], // text, display 'Barlow Condensed': ['400', '600', '700'] }; // and labels // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); await Promise.all(resources.map(({ bitmap: b }) => loadImage(b.fileId, asset(b.fileId)))); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: BOOK });キット · 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上のレシピのフォルダー ↗ (新しいタブで開きます)
アレンジ
#3つ目の環境を加える
塩性湿地の最初の種の前にフェンスをひとつ置くと、部扉、色を敷いた裏、目次の行とともに3つ目の色が加わり、設定はそのままで済みます。目次の下の注記は外してください。3本目の帯とその種に、その場所が要ります。
+:::part{number="III" title="The \\ Saltmarsh" palette="band=#6b4a7a"}
+:::
+
+# Common Shelduck {latin="Tadorna tadorna" size="58–67 cm" status="Resident"}#種を部扉に向かい合わせる
次の奇数ページへの改ページをなくすと、最初の種は部扉の裏から始まるので、versoDesignが塗る白い偶数ページは残りません。
- breakAfter: { parity: 'odd' },よくあるつまずき
つまずき
parts.versoDesignにはparts.breakAfterのparity 'odd'が必要
部扉の裏が描かれるのは、parts.breakAfter { enabled: true, parity: 'odd' }によってその偶数ページが白のまま残るときだけです。 部扉 →
つまずき
{number}/{chapterNumber}はH1の番号を出す。{numberRoman}は部だけ
{number}と{chapterNumber}は見出しの書式付きの番号を出力しますが、{numberRoman}、{numberDecimal}などの数値のバリエーションが埋まるのは部扉だけです。章番号の書式はnumberingTemplate({1:I})で指定するか、属性として渡してください。 番号付き見出し →
つまずき
1ページは奇数ページ。ページは物理的な番号で計画する
1ページは右ページで、2ページが最初の偶数ページです。見開きは物理的なページ番号で計画してください。偶数ページの章扉は、その次の奇数ページと向かい合います。 改ページと改段 →
つまずき
パレットを差し替えても、デザイン要素と参照色は変わらない
postext 1.4.1はcolorPaletteをテキストのスタイル(本文、見出し、リスト、キャプション、表、囲み)には反映しますが、ヘッダー、フッター、章扉、部扉の要素と、bodyText.referenceColorには反映しません。これらはpaletteIdの横に書いた16進の色のままです。画面用のダーク版や色替えのためにパレットを差し替えるときは、ビルドの前に、リンクしたすべての色をcolorPaletteから書き直してください。 セマンティックカラーパレット →
つまずき
headingsオブジェクトを渡すとH1の改ページが消える
既定ではH1は奇数ページへ改ページします(always-odd)。ところがheadingsオブジェクトを渡すと中身にかかわらずこの既定がリセットされ、章は改ページせずに続けて組まれ、span: 'page'も効かなくなります。どの設定でもheadings.levels[0].breakBefore: { enabled: true, parity }を書き直してください。 奇数ページから始まる章 →
つまずき
段落スタイルの前後の空きは囲みの中では効かない
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を設定すれば語間による修正は残るので、それでも出るラントは文を書き換えてください。 ウィドウ、オーファン、ラント →
つまずき
ハイフネーションできるのは8つのロケールだけで、コードは完全一致
ハイフネーションが用意されているのはen-us、es、fr、de、it、pt、ca、nlで、コードは完全一致で照合されます。'es-ES'やほかの言語は、何の知らせもなくアメリカ英語にフォールバックします。 ハイフネーションと文書の言語 →
つまずき
レイアウトの前にすべてのフォントを読み込む
レイアウトはブラウザーが読み込んだフォントで文字を計測し、その幅をキャッシュします。最初のビルドのあとに届いたフォントがあると改行位置が狂い、PDFも画面と一致しなくなります。すべてのウェイトとスタイルを先に読み込み、遅れて届いたときは再ビルドの前にclearMeasurementCache()を呼んでください。 レイアウト前のフォント読み込み →
- 部は値を比べて文字の色を変えます。
band自身の値と等しい文字色は、別の項目に結びついていても、どこにも結びついていなくても、部とともに切り替わります。その16進値はband専用にしてください。 - つめ見出しは
{partNumber}を印字しますが、これは部の外では空です。そのため最初の:::partより前の本文ページには、空のスレート色のつめ見出しが出ます。本文のページはすべて部の中に入れてください。 - 画像とインラインのスウォッチは、そこに書かれた色を保ちます。ここでの図版は環境の色をコードの
HABITATから取っているので、:::partに新しい色を書いたら、そちらにも書き加えてください。
クレジット
- 本文
- 書き下ろしの文章, CC BY 4.0
- 画像
- The cover: a curlew on the estuary mud, reeds in front · Generated With Diffusion Models · オリジナル
- The contents strip: a curlew and a redshank on the mud, the reedbed beyond · Generated With Diffusion Models · オリジナル
- Plate 1: Eurasian curlew · Generated With Diffusion Models · オリジナル
- Plate 2: common redshank · Generated With Diffusion Models · オリジナル
- Plate 3: bearded reedling · Generated With Diffusion Models · オリジナル
- Plate 4: common reed warbler · Generated With Diffusion Models · オリジナル
- フォント
- Alegreya (SIL OFL 1.1) · Zilla Slab (SIL OFL 1.1) · Barlow Condensed (SIL OFL 1.1)

