かんたんな説明
プロイセン王とヴォルテールが交わした3通の手紙を収めた小さな本です。手紙ごとに日付、呼びかけ、署名を組み、新しいページを起こさずに手紙を続けて組む方法を示します。
できあがり
14 × 21cmの版『Mon sort est changé』の5ページで、フランス語の書簡3通を収めます。フリードリヒ2世がプロイセン王になって6日後と12日後、1740年6月にヴォルテールに宛てた2通と、ヴォルテールが死の2か月前、1778年4月1日に書いた手紙です。表紙では、拡散モデルで生成した、折りたたんだ2通の手紙と赤い封蝋の写真が、コードで描いた緑のモロッコ革の上、金の二重罫の内側に置かれています。本文では、手紙は改ページせずに続きます。各手紙の見出しは赤いスモールキャピタルの番号と、Fellのイタリックで組んだ差出人と宛先からなり、その下に場所と日付を右そろえで置きます。呼びかけは独立した1行を取り、スモールキャピタルの署名が右の余白で手紙を締めくくります。フリードリヒの追伸は署名のあとに1段小さく続きます。各奇数ページの柱には手紙の日付が入ります。
このレシピが答える質問
- エピグラフ、献辞、署名、大きな引用符の付いたプルクオートを組むには?
- スペイン語・フランス語・ドイツ語のテキストで、きれいな両端そろえとハイフネーションを得るには?
- 柱を設定するには(左ページに書名、右ページに章タイトル、ノンブルは小口側)?
手短な答え
// Each letter is a level-1 heading that carries its place and date as attributes:
// # Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"}
// (a value holds no { or }, and one with " goes in single quotes: gotcha attr-values)
const letterHead = { enabled: true, slot: { elements: [
// {number} prints numberingTemplate '{1:I}' (gotcha: heading-number-placeholders).
{ kind: 'text', id: 'number', content: 'Lettre {number}', ...sc, fontSize: pt(9),
letterSpacing: pt(1.8), color: col('seal'), placement: at('container', 'top-left') },
{ kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(15),
color: col('ink'),
placement: at('#number', 'below', 0, 1) },
// The dateline spans the measure under the title and sets its words flush right.
{ kind: 'text', id: 'dateline', content: '{attr.place}, le {attr.date}.', ...crimson,
italic: true, fontSize: pt(10.4), color: col('ink'), align: 'right',
placement: { ...at('#title', 'below', 0, 1.5), size: { width: 'fill' } } },
] } };
// The salutation, the signature and the postscript, each a :::paragraphs{style="…"} container:
// :::paragraphs{style="signature"}
// Fédéric.
// :::
const letterParts = [
{ id: 'vedette', firstLineIndent: pt(0) }, // 'Sire,' on a line of its own, flush left
{ id: 'signature', ...sc, fontSize: pt(10.5), textAlign: 'right', marginTop: pt(LEAD / 2) },
{ id: 'postscript', fontSize: pt(9), lineHeight: pt(12.6), marginTop: pt(LEAD / 2) },
];
// Hooked up below: letterHead designs the level-1 heading, letterParts joins paragraphStyles.
材料
- 機能
- 見出しの属性デザイン要素の基準位置段落スタイルデザインした章扉番号付き見出し奇数ページから始まる章見出しスタイル表紙、扉、奥付ページデザインの画像タイトル内の改行柱とノンブルページの役割ごとの柱見開きの余白用紙の色改ページと改段ハイフネーションと文書の言語上付きと下付き文書のメタデータセマンティックカラーパレットキャンバス上のページ
- 併用する機能
- リソースとしての図と表番号のない章
- 種類
- Crimson Pro, IM Fell French Canon, IM Fell DW Pica SC (SIL OFL 1.1)
- 素材
letters-1160.jpg- The cover: two folded letters and a wax seal, a photograph (Generated With Diffusion Models, オリジナル)
- The cover’s green morocco and gilt fillet, drawn in code (Ignacio Ferro, CC BY 4.0)
作り方
#1 · 日付行を見出しから読む
コードは上の手短な答えにあります。手紙はそれぞれレベル1の見出しで、その属性に場所と日付を持ち、見出しのデザインがそれを{attr.place}, le {attr.date}.として印刷します(見出しの属性)。日付行の箱は段の右端まで届き(width: 'fill')、align: 'right'が語をその端にそろえます。これがないと、デザインのテキストは中央にそろいます。呼びかけ、署名、追伸は:::paragraphs{style="…"}で適用する段落スタイルです。1.4.1の段落スタイルにはイタリックや大文字小文字の設定がないので、署名には、小文字をスモールキャピタルとして作ったファミリーIM Fell DW Pica SCを使います。
#2 · 手紙を続けて組む
const letters = { level: 1, numberingTemplate: '{1:I}', advancedDesign: letterHead,
// Written out: 1.4.1 drops the H1 page break for any headings object (gotcha:
// headings-drop-h1-break), and a fixed engine would put each letter on a recto.
breakBefore: { enabled: false }, marginTop: pt(2 * LEAD),
// The hidden heading line is measured in the heading face: italic keeps it the IM Fell cut
// that FONTS loads (gotcha: fonts-first). Upright, it would need the roman, which FONTS
// leaves out; the layout would change only for a title long enough to wrap.
italic: true };
breakBeforeを切ると、手紙は前の手紙が終わるグリッド線の2本下(marginTop)から始まります。4ページでは、12.6ptの行送りで組んだ追伸がグリッド線のあいだで終わるので、第3の手紙の上に約3行分の白が立ちます。2ページでは第1の手紙がページの末尾まで続き、第2の手紙が3ページを始めます。この設定を書き出しているのは、1.4.1では設定にheadingsオブジェクトがあるだけでレベル1の改ページがなくなるためで、修正されたエンジンならどの手紙も奇数ページから始めることになります(前で改ページ)。numberingTemplate: '{1:I}'で手紙にIからIIIの番号が振られ、見出しは{number}でその数字を印刷します。
#3 · 奇数ページに手紙の日付を入れる
const HEAD_Y = 12; // mm from the top edge
const head = (id, content, parity, placement, look = {}) => ({ kind: 'text', id, content,
parity, pages: 'body', ...sc, fontSize: pt(8.5), letterSpacing: pt(0.9), color: col('muted'),
placement, ...look });
const folio = { ...crimson, fontSize: pt(9), letterSpacing: pt(0), color: col('ink') };
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD_Y), folio),
head('verso-names', '{author}', 'even', at('page', 'top-left', MARGIN.outer + 8, HEAD_Y)),
// {attr.date} reads the last letter that starts on or before the page.
head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),
{ ...crimson, italic: true, fontSize: pt(9.5), letterSpacing: pt(0) }),
head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD_Y),
folio),
] };
ページのヘッダーでは、{attr.date}は、そのページかそれより前で始まる最後のレベル1の見出しの属性を取ります。3ページには12 juin 1740が入り、4ページから続くヴォルテールの手紙の5ページには1er avril 1778が入ります(テキスト要素)。柱はページの上端から12mm下に置き、ノンブルは小口側の余白に、語はそこから8mm内側に置きます。pages: 'body'は柱を表紙から外します。表紙は見出しがページ全幅にまたがるので、章扉として数えられます。日付は、それが繰り返す日付行と同じCrimson Proのイタリックで組み、偶数ページの名前は手紙の番号と同じスモールキャピタルで組みます。
#4 · 表紙を見出しにし、そのページを閉じる
// The Markdown: # Mon sort \\ est changé {style="cover"}, then :::pagebreak, or the headnote
// and the first letter start on the cover (gotcha: cover-pagebreak).
const onCover = (y) => at('page', 'top', 0, y); // centred, y mm below the top edge
const LETTERS = { x: 12, y: 72, w: 116, h: 120 }; // mm: the photograph, inside the fillet
// numbered: false keeps the cover out of the count, so the first letter is I.
const cover = { id: 'cover', numbered: false,
// span: 'page' although the book has one column. Kept in the column, the design is clipped
// to the column's top and bottom (paper above and below the leather, no names) and its title
// loses the \\ break; page 1 would also count as a 'body' page and print the running heads.
span: 'page', advancedDesign: { enabled: true, slot: { elements: [
{ kind: 'image', id: 'binding', resourceId: 'binding',
placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
{ kind: 'image', id: 'letters', resourceId: 'letters', placement: { ...at('page', 'top-left',
LETTERS.x, LETTERS.y), size: { width: mm(LETTERS.w), height: mm(LETTERS.h) } } },
{ kind: 'text', id: 'names', content: '{author}', ...sc, fontSize: pt(9.5),
letterSpacing: pt(2), color: col('gilt'), placement: onCover(18) },
// \\ in the heading breaks the title here; lineHeight is a multiple (gotcha:
// design-lineheight-multiple), and 'wrap' keeps the ellipsis off (overflow-ellipsis-default).
{ kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(50),
lineHeight: 1, color: col('paper'), align: 'center', overflow: 'wrap',
placement: onCover(24) },
{ kind: 'text', id: 'subtitle', content: '{subtitle}', ...crimson, italic: true,
fontSize: pt(12), color: col('paper'), placement: onCover(62) },
] } } };
表紙は最初の見出しで、coverの見出しスタイルを使います。裁ち落としにアンカーした画像要素が描いた装丁を載せ、2つ目の画像要素が72mm下に写真を置き、題、名前、副題は見出しとフロントマターから取ります。numbered: falseで表紙は番号の数に入らないので、フリードリヒの最初の手紙がIになります。デザインが確保するのは一番下のテキストである副題までで、画像の分は確保しません。見出しのあとに:::pagebreakがなければ、解題と第1の手紙が1ページで、写真の手紙と封蝋の上から始まってしまいます。
#5 · フランス語をハイフネーションし、語間を開いておく
// Justification, hyphenation, whole-paragraph line breaking and the widow, orphan and runt
// rules are defaults; locale 'fr' (in the config) picks the French patterns.
// The letters keep the transcription's unspaced ; : ? and !, because a narrow no-break
// space is a place to break the line in 1.4.1 (gotcha: nbsp-breaks).
const bodyText = { fontFamily: 'Crimson Pro', fontSize: pt(10), lineHeight: pt(LEAD),
color: col('ink'), firstLineIndent: mm(5),
// No :ref here, but 1.4.1 leaves this one blue whatever main-color says (gotcha:
// palette-skips-designs), and the default-skin check reads it.
referenceColor: col('ink'),
// A word space never shrinks below 75 % of the font's. At the default 60 %, the tightest
// line on page 5 sets its spaces at 0.70 (each VDT line carries its justifiedSpaceRatio).
minWordSpacing: 0.75,
// A runt fix may add tracking 1.4.1 measures but never paints (gotcha:
// runt-tracking-unpainted); no paragraph here needs one, edited text might.
maxRuntTracking: 0 };
locale: 'fr'はフランス語のパターンでハイフネーションします(2ページのou-vrage、4ページのreprésen-tation)。両端そろえ、段落全体での改行、ウィドウ、オーファン、ラントの規則は既定で有効です。minWordSpacing: 0.75は、語間がCrimson Proの語間の4分の3より縮まないようにします。既定の0.6では、5ページで最も詰まった両端そろえの行の語間が、その幅の0.70になります。14.4ptの行送りでページには33行が入り、地の余白はその下に残る20.4mmです。
#6 · すべての色をパレットにつなぐ
const palette = {
ink: '#2a2320', // the text: a warm near-black
paper: '#f6efe2', // the page, and the lettering on the cover
seal: '#9c2b24', // the letter numbers
leather: '#2a4536', // the cover: a green morocco binding
gilt: '#d0b67c', // its tooled border and the names on it
muted: '#75695d', // the running heads
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
// The engine's default colours, the italic of the headnote among them, link to 'main-color';
// here it is the ink, not the default blue.
{ id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
col()は各色のhexをパレットのidと並べて書きます。1.4.1ではパレットはテキストのスタイルには届きますが、デザインの要素には届かず、デザインの要素はそのhexで描かれます。手紙の見出し、柱、表紙がそうです。解題と奥付のイタリックは既定のイタリックの色を使い、これはmain-colorにつながっています。この項目を墨色にすると、イタリックはエンジンの青(#295AA3)ではなく墨色で印刷されます。
レシピの全体
// ═══ Postext Cookbook · Nº 064 · Letters edition: datelines and signatures ════════════ // https://postext.dev/en/cookbook/letters-edition // Code: MIT · Text: Frederick II and Voltaire, 1740 and 1778 (PD) · Cover photo: diffusion models // Fonts: Crimson Pro, IM Fell French Canon, IM Fell DW Pica SC (SIL OFL) · Needs postext ≥ 1.4.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'letters-edition'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: iron-gall ink on cream paper, wax red, green morocco and gilt const palette = { ink: '#2a2320', // the text: a warm near-black paper: '#f6efe2', // the page, and the lettering on the cover seal: '#9c2b24', // the letter numbers leather: '#2a4536', // the cover: a green morocco binding gilt: '#d0b67c', // its tooled border and the names on it muted: '#75695d', // the running heads }; // A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs). const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = [ ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })), // The engine's default colours, the italic of the headnote among them, link to 'main-color'; // here it is the ink, not the default blue. { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } }, ]; // #endregion const TRIM = { width: 140, height: 210 }; // the French 14 × 21 format const LEAD = 14.4; // pt: the body's leading, the grid every letter starts on const LINES = 33; // lines of text on a full page const TOP = 22; // mm: the top margin const MARGIN = { top: TOP, inner: 17.5, outer: 14.5, // mm, mirrored bottom: TRIM.height - TOP - (LINES * LEAD * 25.4) / 72 }; // ends the page on line 33 const sc = { fontFamily: 'IM Fell DW Pica SC' }; // its lower case is cut as small capitals const fell = { fontFamily: 'IM Fell French Canon', italic: true }; // the display italic const crimson = { fontFamily: 'Crimson Pro' }; // the text face const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); // #region answer: a letter head read from the heading, and styles for the letter's parts // Each letter is a level-1 heading that carries its place and date as attributes: // # Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"} // (a value holds no { or }, and one with " goes in single quotes: gotcha attr-values) const letterHead = { enabled: true, slot: { elements: [ // {number} prints numberingTemplate '{1:I}' (gotcha: heading-number-placeholders). { kind: 'text', id: 'number', content: 'Lettre {number}', ...sc, fontSize: pt(9), letterSpacing: pt(1.8), color: col('seal'), placement: at('container', 'top-left') }, { kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(15), color: col('ink'), placement: at('#number', 'below', 0, 1) }, // The dateline spans the measure under the title and sets its words flush right. { kind: 'text', id: 'dateline', content: '{attr.place}, le {attr.date}.', ...crimson, italic: true, fontSize: pt(10.4), color: col('ink'), align: 'right', placement: { ...at('#title', 'below', 0, 1.5), size: { width: 'fill' } } }, ] } }; // The salutation, the signature and the postscript, each a :::paragraphs{style="…"} container: // :::paragraphs{style="signature"} // Fédéric. // ::: const letterParts = [ { id: 'vedette', firstLineIndent: pt(0) }, // 'Sire,' on a line of its own, flush left { id: 'signature', ...sc, fontSize: pt(10.5), textAlign: 'right', marginTop: pt(LEAD / 2) }, { id: 'postscript', fontSize: pt(9), lineHeight: pt(12.6), marginTop: pt(LEAD / 2) }, ]; // Hooked up below: letterHead designs the level-1 heading, letterParts joins paragraphStyles. // #endregion // #region letters: numbered I, II, III and run on, two grid lines apart const letters = { level: 1, numberingTemplate: '{1:I}', advancedDesign: letterHead, // Written out: 1.4.1 drops the H1 page break for any headings object (gotcha: // headings-drop-h1-break), and a fixed engine would put each letter on a recto. breakBefore: { enabled: false }, marginTop: pt(2 * LEAD), // The hidden heading line is measured in the heading face: italic keeps it the IM Fell cut // that FONTS loads (gotcha: fonts-first). Upright, it would need the roman, which FONTS // leaves out; the layout would change only for a title long enough to wrap. italic: true }; // #endregion // #region running-heads: the correspondents on the verso, the date of the letter on the recto const HEAD_Y = 12; // mm from the top edge const head = (id, content, parity, placement, look = {}) => ({ kind: 'text', id, content, parity, pages: 'body', ...sc, fontSize: pt(8.5), letterSpacing: pt(0.9), color: col('muted'), placement, ...look }); const folio = { ...crimson, fontSize: pt(9), letterSpacing: pt(0), color: col('ink') }; const header = { elements: [ head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD_Y), folio), head('verso-names', '{author}', 'even', at('page', 'top-left', MARGIN.outer + 8, HEAD_Y)), // {attr.date} reads the last letter that starts on or before the page. head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y), { ...crimson, italic: true, fontSize: pt(9.5), letterSpacing: pt(0) }), head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD_Y), folio), ] }; // #endregion // #region cover: page 1 is a heading style with the art and the title; :::pagebreak ends it // The Markdown: # Mon sort \\ est changé {style="cover"}, then :::pagebreak, or the headnote // and the first letter start on the cover (gotcha: cover-pagebreak). const onCover = (y) => at('page', 'top', 0, y); // centred, y mm below the top edge const LETTERS = { x: 12, y: 72, w: 116, h: 120 }; // mm: the photograph, inside the fillet // numbered: false keeps the cover out of the count, so the first letter is I. const cover = { id: 'cover', numbered: false, // span: 'page' although the book has one column. Kept in the column, the design is clipped // to the column's top and bottom (paper above and below the leather, no names) and its title // loses the \\ break; page 1 would also count as a 'body' page and print the running heads. span: 'page', advancedDesign: { enabled: true, slot: { elements: [ { kind: 'image', id: 'binding', resourceId: 'binding', placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } }, { kind: 'image', id: 'letters', resourceId: 'letters', placement: { ...at('page', 'top-left', LETTERS.x, LETTERS.y), size: { width: mm(LETTERS.w), height: mm(LETTERS.h) } } }, { kind: 'text', id: 'names', content: '{author}', ...sc, fontSize: pt(9.5), letterSpacing: pt(2), color: col('gilt'), placement: onCover(18) }, // \\ in the heading breaks the title here; lineHeight is a multiple (gotcha: // design-lineheight-multiple), and 'wrap' keeps the ellipsis off (overflow-ellipsis-default). { kind: 'text', id: 'title', content: '{titleText}', ...fell, fontSize: pt(50), lineHeight: 1, color: col('paper'), align: 'center', overflow: 'wrap', placement: onCover(24) }, { kind: 'text', id: 'subtitle', content: '{subtitle}', ...crimson, italic: true, fontSize: pt(12), color: col('paper'), placement: onCover(62) }, ] } } }; // #endregion // #region text: Crimson Pro at 10/14.4 pt, set in French // Justification, hyphenation, whole-paragraph line breaking and the widow, orphan and runt // rules are defaults; locale 'fr' (in the config) picks the French patterns. // The letters keep the transcription's unspaced ; : ? and !, because a narrow no-break // space is a place to break the line in 1.4.1 (gotcha: nbsp-breaks). const bodyText = { fontFamily: 'Crimson Pro', fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'), firstLineIndent: mm(5), // No :ref here, but 1.4.1 leaves this one blue whatever main-color says (gotcha: // palette-skips-designs), and the default-skin check reads it. referenceColor: col('ink'), // A word space never shrinks below 75 % of the font's. At the default 60 %, the tightest // line on page 5 sets its spaces at 0.70 (each VDT line carries its justifiedSpaceRatio). minWordSpacing: 0.75, // A runt fix may add tracking 1.4.1 measures but never paints (gotcha: // runt-tracking-unpainted); no paragraph here needs one, edited text might. maxRuntTracking: 0 }; // #endregion const config = () => ({ // a factory: the engine caches resolved configs per object locale: 'fr', // the exact code of the bundled patterns (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 } }, // left = inner layout: { layoutType: 'single' }, bodyText, // A heading's own line is hidden under its design but still measured, in this face and weight. headings: { fontFamily: 'IM Fell French Canon', fontWeight: 400, levels: [letters] }, headingStyles: [cover], paragraphStyles: [...letterParts, // Frederick's verses, one paragraph per line (a one-line paragraph is never stretched), with // a line of space above and below; the two closing alexandrines start 7 mm further left. { id: 'verse', firstLineIndent: mm(14), marginTop: pt(LEAD) }, { id: 'verse-long', firstLineIndent: mm(7), marginBottom: pt(LEAD) }, // The editor's headnote at 9.6 on 13 pt, italic through *…* in the Markdown, since a // paragraph style has no italic setting. { id: 'headnote', fontSize: pt(9.6), lineHeight: pt(13), firstLineIndent: pt(0) }, { id: 'colophon', fontSize: pt(7.8), lineHeight: pt(10.8), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(3 * LEAD) }], header, footer: { elements: [] }, // the folios ride in the header }); // #region art: the binding drawn in code, and the photograph of the letters laid on it const W = TRIM.width; const H = TRIM.height; // The binding: green leather to the edges, a gilt double fillet and a lozenge at each corner. function bindingSvg() { const tooling = [6, 7.6].map((inset, i) => `<rect x="${inset}" y="${inset}" ` + `width="${W - 2 * inset}" height="${H - 2 * inset}" fill="none" stroke="${palette.gilt}" ` + `stroke-width="${i ? 0.25 : 0.7}"/>`).join('') + [[6, 6], [W - 6, 6], [6, H - 6], [W - 6, H - 6]].map(([x, y]) => `<path d="M${x} ${y - 2.4} L${x + 2.4} ${y} L${x} ${y + 2.4} ` + `L${x - 2.4} ${y} Z" fill="${palette.gilt}"/>`).join(''); return `<svg xmlns="http://www.w3.org/2000/svg" width="${W * 10}" height="${H * 10}" ` + `viewBox="0 0 ${W} ${H}"><rect width="${W}" height="${H}" fill="${palette.leather}"/>` + `${tooling}</svg>`; } // The design's image elements name these by id; loadSvg() and loadImage() below register the // files. The photograph is a JPEG in assets/ cut to LETTERS' 116 × 120 mm, declared at its // pixels; its edges fade into the leather's green, so it sits on the drawn binding unseen. const resources = [{ id: 'binding', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, svg: { fileId: 'binding.svg', width: W * 10, height: H * 10 }, altText: 'A green leather cover with a gilt double fillet and a lozenge at each corner.' }, { id: 'letters', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'letters-1160.jpg', format: 'jpeg', width: 1160, height: 1200 }, altText: 'Two folded letters on the green leather: the upper one lies face down, its flap ' + 'closed by a red wax seal; the lower one shows an address in brown ink.' }]; // #endregion // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Markdownの見本 · 120行 · content.en.md
title: "Mon sort est changé" subtitle: "Trois lettres, 1740 et 1778" author: "Frédéric II et Voltaire" --- # Mon sort \\ est changé {style="cover"} :::pagebreak :::paragraphs{style="headnote"} *Frédéric-Guillaume I^er^, roi de Prusse, meurt à Potsdam le 31 mai 1740. Six jours plus tard, son fils, qui règne désormais sous le nom de Frédéric II, écrit de Charlottembourg à Voltaire, son correspondant depuis août 1736. Les deux premières lettres de ce choix sont de ce mois de juin. La troisième, écrite de Paris le 1^er^ avril 1778, ferme le volume de 1889 d’où viennent ces textes. Voltaire meurt à Paris le 30 mai suivant. On garde l’orthographe de l’édition; la vedette et la signature sont détachées du texte.* ::: # Frédéric à Voltaire {place="À Charlottembourg" date="6 juin 1740"} :::paragraphs{style="vedette"} Mon cher ami, ::: Mon sort est changé, et j’ai assisté aux derniers moments d’un roi, à son agonie, à sa mort. En parvenant à la royauté, je n’avais pas besoin assurément de cette leçon pour être dégoûté de la vanité des grandeurs humaines. J’avais projeté un petit ouvrage de métaphysique; il s’est changé en un ouvrage de politique. Je croyais joûter avec l’aimable Voltaire, et il me faut escrimer avec Machiavel. Enfin, mon cher Voltaire, nous ne sommes point maîtres de notre sort. Le tourbillon des événements nous entraîne, et il faut se laisser entraîner. Ne voyez en moi, je vous prie, qu’un citoyen zélé, un philosophe un peu sceptique, mais un ami véritablement fidèle. Pour dieu, ne m’écrivez qu’en homme, et méprisez avec moi les titres, les noms, et tout l’éclat extérieur. Jusqu’à présent il me reste à peine le temps de me reconnaître; j’ai des occupations infinies: je m’en donne encore de surplus; mais malgré tout ce travail, il me reste toujours du temps assez pour admirer vos ouvrages et pour puiser chez vous des instructions et des délassements. Assurez la marquise de mon estime. Je l’admire autant que ses vastes connaissances et la rare capacité de son esprit le méritent. Adieu, mon cher Voltaire; si je vis, je vous verrai, et même dès cette année. Aimez-moi toujours, et soyez toujours sincère ami avec votre ami :::paragraphs{style="signature"} Fédéric. ::: # Frédéric à Voltaire {place="À Charlottembourg" date="12 juin 1740"} :::paragraphs{style="verse"} Non, ce n’est plus du mont Rémus, Douce et studieuse retraite D’où mes vers vous sont parvenus, Que je date ces vers confus: Car dans ce moment le poète Et le prince sont confondus. Désormais mon peuple que j’aime Est l’unique Dieu que je sers: Adieu les vers et les concerts. Tous les plaisirs. Voltaire même; Mon devoir est mon Dieu suprême. Qu’il entraîne de soins divers! Quel fardeau que le diadème! Quand ce dieu sera satisfait, Alors dans vos bras, cher Voltaire, Je volerai, plus prompt qu’un trait, ::: :::paragraphs{style="verse-long"} Puiser, dans les leçons de mon ami sincère, Quel doit être d’un roi le sacré caractère. ::: Vous voyez, mon cher ami, que le changement du sort ne m’a pas tout à fait guéri de la métromanie, et que peut-être je n’en guérirai jamais. J’estime trop l’art d’Horace et de Voltaire pour y renoncer; et je suis du sentiment que chaque chose de la vie a son temps. J’avais commencé une épître sur les abus de la mode et de la coutume, lors même que la coutume de la primogéniture m’obligeait de monter sur le trône et de quitter mon épître pour quelque temps. J’aurais volontiers changé mon épître en satire contre cette même mode, si je ne savais que la satire doit être bannie de la bouche des princes. Enfin, mon cher Voltaire, je flotte entre vingt occupations, et je ne déplore que la brièveté des jours, qui me paraissent trop courts de vingt-quatre heures. Je vous avoue que la vie d’un homme qui n’existe que pour réfléchir et pour lui-même, me semble infiniment préférable à la vie d’un homme dont l’unique occupation doit être de faire le bonheur des autres. Vos vers sont charmants. Je n’en dirai rien, car ils sont trop flatteurs. Mon cher Voltaire, ne vous refusez pas plus longtemps à l’empressement que j’ai de vous voir. Faites en ma faveur tout ce que vous croyez que votre humanité comporte. J’irai à la fin d’auguste à Vesel, et peut-être plus loin. Promettez-moi de me joindre, car je ne saurais vivre heureux ni mourir tranquille sans vous avoir embrassé. Adieu. :::paragraphs{style="signature"} Fédéric. ::: :::paragraphs{style="postscript"} Mille compliments à la marquise. Je travaille des deux mains; d’un côté à l’armée, de l’autre au peuple et aux beaux-arts. ::: # Voltaire à Frédéric {place="À Paris" date="1er avril 1778"} :::paragraphs{style="vedette"} Sire, ::: Le gentilhomme français qui rendra cette lettre à Votre Majesté, et qui passe pour être digne de paraître devant Elle, pourra vous dire que si je n’ai pas eu l’honneur de vous écrire depuis longtemps, c’est que j’ai été occupé à éviter deux choses qui me poursuivaient dans Paris: les sifflets et la mort. Il est plaisant qu’à quatre-vingt-quatre ans j’aie échappé à deux maladies mortelles. Voilà ce que c’est que de vous être consacré: je me suis renommé de vous, et j’ai été sauvé. J’ai vu avec surprise et avec une satisfaction bien douce, à la représentation d’une tragédie nouvelle, que le public, qui regardait il y a trente ans Constantin et Théodose comme les modèles des princes, et même des saints, a applaudi avec des transports inouïs à des vers qui disent que Constantin et Théodose n’ont été que des tyrans superstitieux. J’ai vu vingt preuves pareilles du progrès que la philosophie a fait enfin dans toutes les conditions. Je ne désespérerais pas de faire prononcer dans un mois le panégyrique de l’empereur Julien: et assurément si les Parisiens se souviennent qu’il a rendu chez eux la justice comme Caton, et qu’il a combattu pour eux comme César, ils lui doivent une éternelle reconnaissance. Il est donc vrai, Sire, qu’à la fin les hommes s’éclairent, et que ceux qui se croient payés pour les aveugler ne sont pas toujours les maîtres de leur crever les yeux! Grâces en soient rendus à Votre Majesté! Vous avez vaincu les préjugés comme vos autres ennemis: vous jouissez de vos établissements en tout genre. Vous êtes le vainqueur de la superstition, ainsi que le soutien de la liberté germanique. Vivez plus longtemps que moi, pour affermir tous les empires que vous avez fondés. Puisse Frédéric le Grand être Frédéric l’immortel! Daignez agréer le profond respect et l’inviolable attachement de :::paragraphs{style="signature"} Voltaire. ::: :::paragraphs{style="colophon"} *Correspondance de Voltaire avec le roi de Prusse* (Paris, Librairie de la Bibliothèque nationale, 1889), texte du Project Gutenberg, nº 25734. Composé en Crimson Pro et IM Fell (licence SIL OFL). :::`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces (gotcha: fonts-first) 'Crimson Pro': ['400', '400i'], 'IM Fell French Canon': ['400i'], 'IM Fell DW Pica SC': ['400'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadSvg('binding.svg', bindingSvg()); await loadImage('letters-1160.jpg', asset('letters-1160.jpg')); await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'Letters edition', es: 'Edición de cartas' }) });キット · 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上のレシピのフォルダー ↗ (新しいタブで開きます)
アレンジ
#柱に場所も入れる
同じ属性を使えば、奇数ページに日付行全体、「À Charlottembourg, le 12 juin 1740」を入れられます。
- head('recto-date', '{attr.date}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),
+ head('recto-date', '{attr.place}, le {attr.date}', 'odd',
+ at('page', 'top-right', -(MARGIN.outer + 8), HEAD_Y),よくあるつまずき
つまずき
属性値には{も}も書けない。"を含む値は一重引用符で囲む
属性値は閉じ波かっこで終わるため、{や}を含められません。二重引用符を含む値は一重引用符で囲みます。ドル記号は問題ありません。 見出しの属性 →
つまずき
headingsオブジェクトを渡すとH1の改ページが消える
既定ではH1は奇数ページへ改ページします(always-odd)。ところがheadingsオブジェクトを渡すと中身にかかわらずこの既定がリセットされ、章は改ページせずに続けて組まれ、span: 'page'も効かなくなります。どの設定でもheadings.levels[0].breakBefore: { enabled: true, parity }を書き直してください。 奇数ページから始まる章 →
つまずき
{number}/{chapterNumber}はH1の番号を出す。{numberRoman}は部だけ
{number}と{chapterNumber}は見出しの書式付きの番号を出力しますが、{numberRoman}、{numberDecimal}などの数値のバリエーションが埋まるのは部扉だけです。章番号の書式はnumberingTemplate({1:I})で指定するか、属性として渡してください。 番号付き見出し →
つまずき
全面の表紙のあとに:::pagebreakを置く
段組みのレイアウトでは、表紙のような全面の章扉のあと、次のブロックが同じページの2段目、つまり絵の上から始まってしまいます。表紙の見出しの直後に:::pagebreakを置けばそのページで終わり、白ページもできません。 表紙、扉、奥付 →
つまずき
ハイフネーションできるのは8つのロケールだけで、コードは完全一致
ハイフネーションが用意されているのはen-us、es、fr、de、it、pt、ca、nlで、コードは完全一致で照合されます。'es-ES'やほかの言語は、何の知らせもなくアメリカ英語にフォールバックします。 ハイフネーションと文書の言語 →
つまずき
ノーブレークスペースでも改行される
postext 1.4.1の改行処理はU+00A0を通常のスペースとして扱うため、0.08 %、2.006 s、Section 2などが2行に分かれることがあります。間を詰める(0.08%)か、文を書き換えてください。 エスケープとそのままの文字 →
つまずき
パレットを差し替えても、デザイン要素と参照色は変わらない
postext 1.4.1はcolorPaletteをテキストのスタイル(本文、見出し、リスト、キャプション、表、囲み)には反映しますが、ヘッダー、フッター、章扉、部扉の要素と、bodyText.referenceColorには反映しません。これらはpaletteIdの横に書いた16進の色のままです。画面用のダーク版や色替えのためにパレットを差し替えるときは、ビルドの前に、リンクしたすべての色をcolorPaletteから書き直してください。 セマンティックカラーパレット →
つまずき
デザインのテキストのlineHeightは倍率で、寸法ではない
デザインのスロットでは、テキスト要素のlineHeightはフォントサイズに掛ける倍率です(lineHeight: 1.05)。postext 1.4.1ではpt(15)のような寸法を指定しても拒否されず、章扉の高さがNaNと計測されて、minHeightを含め確保する高さが警告なしに失われ、本文がタイトルに重なって組まれます。 ページデザインのテキスト・罫・ボックス →
つまずき
デザインのテキストのoverflowの既定値は'ellipsis-end'
幅に収まらないデザインのテキスト要素は、既定では省略記号で終わります。複数行に折り返したいタイトルにはoverflow: 'wrap'を設定してください。 ページデザインのテキスト・罫・ボックス →
つまずき
ラントの修正が、描かれないトラッキングを詰めることがある
postext 1.4.1では、段落がラント(最終行に残った短い一語)で終わると、レイアウトはその段落を1行短く組みます。まず語間を詰め、次にmaxRuntTracking(1000分の1 em単位)までの負のトラッキングを使います。CanvasとPDFのレンダラーは0より大きいトラッキングしか描かないため、トラッキングを詰めた段落はトラッキングなしで印字されます。両端そろえの行はその差の分だけ語間を失って詰まって見え、最終行は行長を超えて段の端で切れることがあります。bodyText.maxRuntTracking: 0を設定すれば語間による修正は残るので、それでも出るラントは文を書き換えてください。 ウィドウ、オーファン、ラント →
つまずき
レイアウトの前にすべてのフォントを読み込む
レイアウトはブラウザーが読み込んだフォントで文字を計測し、その幅をキャッシュします。最初のビルドのあとに届いたフォントがあると改行位置が狂い、PDFも画面と一致しなくなります。すべてのウェイトとスタイルを先に読み込み、遅れて届いたときは再ビルドの前にclearMeasurementCache()を呼んでください。 レイアウト前のフォント読み込み →
つまずき
デザインのテキストではインラインの^sup^や**bold**が使えない
デザインのテキスト要素はプレーンテキストを印字するため、属性内の^1^や**bold**はそのまま出ます。Unicodeの上付き数字(¹ ² ³はlatinサブセットに含まれます)を使うか、別のウェイトの要素をもう1つ置いてください。 ページデザインのテキスト・罫・ボックス →
- 奇数ページは、そのページかそれより前で始まる最後の手紙の日付を取ります。ある手紙が終わり次の手紙が始まるページには、新しい手紙の日付が入ります。辞書の柱がそのページの最後の見出し語を示すのと同じです。
- 属性のない見出しは空文字列を印刷し、警告も出ません。
place="…"のない手紙では、日付行がコンマから始まります。すべての手紙の見出しを確認してください。 - 手紙は翻刻のとおり、; : ? !の前にアキを入れていません。フランス語の慣行ではその前に細いノーブレークスペースを入れますが、1.4.1はそのスペースでもほかのスペースと同じように改行するので、約物が行頭に来るおそれがあります。
- Crimson Proには修飾文字のᵉ(U+1D49)とʳ(U+02B3)がないので、Unicodeで綴った1ᵉʳは代替フォントで印刷されます。本文である解題は
1^er^で上付きにしています。日付行と奇数ページの柱はデザインのテキストなので、序数の部分も本文と同じ大きさのまま1erと印刷されます。
クレジット
- 本文
- Frederick II’s letters to Voltaire of 6 and 12 June 1740 and Voltaire’s letter to him of 1 April 1778, in Correspondance de Voltaire avec le roi de Prusse (Paris, Librairie de la Bibliothèque nationale, 1889), one misprint corrected (ouvragés), the salutations and signatures set apart · Frédéric II · Voltaire · パブリックドメイン
- The headnote and the colophon · Ignacio Ferro · CC BY 4.0
- 画像
- The cover: two folded letters and a wax seal, a photograph · Generated With Diffusion Models · オリジナル
- The cover’s green morocco and gilt fillet, drawn in code · Ignacio Ferro · CC BY 4.0
- フォント
- Crimson Pro (SIL OFL 1.1) · IM Fell French Canon (SIL OFL 1.1) · IM Fell DW Pica SC (SIL OFL 1.1)


