En bref
Un petit livre de trois lettres échangées entre le roi de Prusse et Voltaire. Il montre comment composer la date, la formule d’appel et la signature de chaque lettre, les lettres se suivant sans changer de page.
Ce que vous allez composer
Cinq pages de Mon sort est changé, une édition au format 14 × 21 cm de trois lettres en français : les deux que Frédéric II écrivit à Voltaire six et douze jours après être devenu roi de Prusse, en juin 1740, et la lettre de Voltaire du 1er avril 1778, deux mois avant sa mort. Sur la couverture, une photographie de deux lettres pliées et d’un sceau de cire rouge, générée par des modèles de diffusion, repose sur un maroquin vert dessiné en code dans un double filet doré. À l’intérieur, les lettres se suivent sans saut de page. Chaque en-tête donne le numéro en petites capitales rouges et les deux correspondants en italique Fell, avec le lieu et la date au fer à droite en dessous. L’appel a sa propre ligne, et la signature, en petites capitales, termine la lettre contre la marge de droite ; le post-scriptum de Frédéric suit sa signature dans un corps plus petit. Le titre courant de chaque recto donne la date de la lettre.
Cette recette répond à
- Comment composer une épigraphe, une dédicace, une signature ou une citation en exergue avec un grand guillemet ?
- Comment obtenir une bonne justification et une bonne césure pour un texte en espagnol, en français ou en allemand ?
- Comment régler les titres courants : titre du livre sur la page de gauche, titre du chapitre sur celle de droite, numéro de page à l’extérieur ?
La réponse courte
// 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 the count as numberingTemplate '{1:I}' writes it.
{ 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;
// inlineMarks reads the attribute's marks, so date="1^er^ avril 1778" raises its ordinal.
{ kind: 'text', id: 'dateline', content: '{attr.place}, le {attr.date}.', ...crimson,
italic: true, fontSize: pt(10.4), color: col('ink'), align: 'right', inlineMarks: true,
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.
Ingrédients
- Fonctionnalités
- Attributs de titreAncrer les éléments de maquetteStyles de paragrapheOuvertures composéesTitres numérotésChapitres qui commencent en belle pageStyles de titreCouvertures, pages de titre et colophonsImages dans les maquettes de pageSauts de ligne dans les titresTitres courants et foliosVers ligne à ligneMarges en miroirCouleur du papierCésure et langue du documentExposants et indicesMétadonnées du documentPalette de couleurs sémantiquePages sur un canvas
- Utilise aussi
- Titres courants selon le rôle de la pageFigures et tableaux comme ressourcesChapitres non numérotés
- Typographie
- Crimson Pro, IM Fell French Canon, IM Fell DW Pica SC (SIL OFL 1.1)
- Ressources
letters-1160.jpg- The cover: two folded letters and a wax seal, a photograph (Generated With Diffusion Models, original)
- The cover’s green morocco and gilt fillet, drawn in code (Ignacio Ferro, CC BY 4.0)
Préparation
#1 · Lire la date dans le titre
Le code est dans la réponse courte plus haut. Chaque lettre est un titre de niveau 1 dont les attributs contiennent le lieu et la date, et l’en-tête les imprime sous la forme {attr.place}, le {attr.date}. (attributs de titre). La boîte de la date atteint le bord droit de la colonne (width: 'fill'), et align: 'right' cale les mots contre ce bord ; sans cela, le texte de maquette est centré. L’appel, la signature et le post-scriptum sont des styles de paragraphe appliqués avec :::paragraphs{style="…"}. La signature prend IM Fell DW Pica SC, une famille dont les bas-de-casse sont dessinés en petites capitales. inlineMarks: true fait lire à la date les marques de son attribut : date="1^er^ avril 1778" met l’ordinal en exposant.
#2 · Faire se suivre les lettres
const letters = { level: 1, numberingTemplate: '{1:I}', advancedDesign: letterHead,
// No break: left at the level's default, each letter would open 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. Upright, it would need the roman, which FONTS
// leaves out; the layout would change only for a title long enough to wrap.
italic: true };
Avec breakBefore désactivé, une lettre commence deux lignes de grille (marginTop) sous la ligne de grille où finit la précédente. Sur la page 4, le post-scriptum, composé sur un interlignage de 12,6 pt, se termine entre deux lignes de grille, si bien qu’environ trois lignes de blanc se trouvent au-dessus de la lettre III. En page 2, la lettre I court jusqu’au pied de la page et la lettre II ouvre la page 3. Laissé à sa valeur par défaut, le niveau 1 ouvrirait chaque lettre sur un recto (saut avant). numberingTemplate: '{1:I}' numérote les lettres de I à III, et l’en-tête imprime le chiffre par {number}.
#3 · Dater le recto avec la lettre
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), inlineMarks: true }),
head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD_Y),
folio),
] };
Dans un en-tête de page, {attr.date} prend l’attribut du dernier titre de niveau 1 qui commence sur la page ou avant. La page 3 imprime « 12 juin 1740 », et la page 5, où se poursuit la lettre de Voltaire commencée en page 4, imprime « 1er avril 1778 » avec son ordinal en exposant (éléments de texte). Les titres courants se placent 12 mm sous le bord supérieur de la page, le folio sur la marge extérieure et les mots 8 mm plus à l’intérieur. pages: 'body' les tient à l’écart de la couverture, qui compte comme une ouverture parce que son titre s’étend sur toute la page. La date est composée dans l’italique Crimson Pro de la date de lieu qu’elle répète, et les noms du verso dans les petites capitales des numéros de lettre.
#4 · Faire de la couverture un titre
// The Markdown: # Mon sort \\ est changé {style="cover"}. The binding fills the page, so the
// design reserves all of it and the headnote starts on page 2.
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 stops at
// the column's foot (a band of paper under the leather), and page 1 counts as a 'body' page
// and prints 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.
{ 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) },
] } } };
La couverture est le premier titre, dans le style de titre cover : un élément image ancré au fond perdu porte la reliure dessinée, un second place la photographie 72 mm plus bas, et le titre, les noms et le sous-titre viennent du titre et de l’en-tête du document. numbered: false la tient hors du compte, si bien que la première lettre de Frédéric est la I. L’image de la reliure remplit la page, et une ouverture réserve la place jusqu’à son élément le plus bas : la couverture occupe donc toute la page 1, et la note liminaire commence en page 2 sans :::pagebreak.
#5 · Couper le français, garder les espaces ouvertes
// 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 !; a narrow no-break space
// (U+202F) typed before each would give French spacing and never end a line.
const bodyText = { fontFamily: 'Crimson Pro', fontSize: pt(10), lineHeight: pt(LEAD),
color: col('ink'), firstLineIndent: mm(5),
// No :ref here: written out with the other colours, for the default-skin check.
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 };
locale: 'fr' coupe avec les motifs français (ou-vrage en page 2, représen-tation en page 4). La justification, la coupure des lignes sur tout le paragraphe et les règles contre les veuves, les orphelines et les fins de paragraphe trop courtes sont actives par défaut. minWordSpacing: 0.75 empêche une espace de se réduire sous les trois quarts de celle de Crimson Pro ; à la valeur par défaut de 0,6, la ligne justifiée la plus serrée de la page 5 composerait ses espaces à 0,70 de cette largeur. Avec un interlignage de 14,4 pt, la page contient 33 lignes, et la marge du bas est ce qui reste sous elles, 20,4 mm.
#6 · Lier chaque couleur à la palette
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
};
// col(id): a colour linked to its palette entry, in text styles and design elements alike.
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() lie chaque couleur à son entrée de palette, et ce lien vaut aussi bien dans les styles de texte que dans les éléments de maquette : les en-têtes de lettre, les titres courants et la couverture. L’italique de la note liminaire et du colophon prend la couleur d’italique par défaut, liée à main-color ; avec cette entrée réglée sur l’encre, il s’imprime à l’encre et non dans le bleu du moteur, #295AA3.
La recette complète
// ═══ 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.25.0 import { buildDocumentWithFonts, renderPageToCanvas, 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 }; // col(id): a colour linked to its palette entry, in text styles and design elements alike. 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 the count as numberingTemplate '{1:I}' writes it. { 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; // inlineMarks reads the attribute's marks, so date="1^er^ avril 1778" raises its ordinal. { kind: 'text', id: 'dateline', content: '{attr.place}, le {attr.date}.', ...crimson, italic: true, fontSize: pt(10.4), color: col('ink'), align: 'right', inlineMarks: true, 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, // No break: left at the level's default, each letter would open 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. 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), inlineMarks: true }), 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 // The Markdown: # Mon sort \\ est changé {style="cover"}. The binding fills the page, so the // design reserves all of it and the headnote starts on page 2. 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 stops at // the column's foot (a band of paper under the leather), and page 1 counts as a 'body' page // and prints 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. { 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 !; a narrow no-break space // (U+202F) typed before each would give French spacing and never end a line. const bodyText = { fontFamily: 'Crimson Pro', fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'), firstLineIndent: mm(5), // No :ref here: written out with the other colours, for the default-skin check. 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 }; // #endregion const config = () => ({ locale: 'fr', // French hyphenation patterns 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, a :::verse block 7 mm in with a line of space above and below; the // octosyllables start two spaces (indentStep=3.5mm on the fence) further in, 14 mm, and the // two closing alexandrines at 7 mm. { id: 'verse', indent: mm(7), marginTop: pt(LEAD), marginBottom: pt(LEAD) }, // The editor's headnote at 9.6 on 13 pt, italic through *…* in the Markdown. { 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`---Exemple Markdown · 99 lignes · 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"} :::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"} :::verse{style="verse" align=start indentStep=3.5mm} 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, 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="1^er^ 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 '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')); const doc = await buildDocumentWithFonts({ markdown, resources }, config(), kitFonts(FONTS)); showPages(doc, { title: t({ en: 'Letters edition', es: 'Edición de cartas' }) });Kit · core, fonts, viewer, images : identique dans toutes les recettes · 270 lignes
// ─── 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 v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { 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', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}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().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes 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; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), 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 v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { 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, { ...render, 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 v3 ── 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 as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25). * `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */ async function loadSvg(fileId, svg, options) { if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces); const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img, options); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** A face's Fontsource files: nearest weight; latin + what the SVG sets. */ async function svgFaces(family, weight, style, { codePoints = [] } = {}) { const [id, m] = [fontsourceId(family), await fontsourceMeta(family)]; const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight; const s = m?.styles.includes(style) === false ? 'normal' : style; return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array( await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer()))); } /** 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 ───────────────────────────────────────────────────────────────────────
Le script.js composé s’exécute tel quel : collez-le dans le script de module de n’importe quelle page, ou ouvrez la recette sur CodePen. Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)
Variantes
#Nommer le lieu dans le titre courant
Les mêmes attributs peuvent donner au recto la date de lieu complète, À 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),Erreurs fréquentes
Erreur fréquente
Valeurs d’attribut : pas de { ni de } ; une valeur avec " entre apostrophes simples
Une valeur d’attribut s’arrête à l’accolade fermante : elle ne peut donc contenir ni { ni }. Une valeur qui contient un guillemet double se met entre apostrophes simples ; un signe dollar ne pose pas de problème. Attributs de titre →
- Le recto prend la date de la dernière lettre qui commence sur la page ou avant. Une page qui finit une lettre et ouvre la suivante porte la date de la nouvelle lettre, comme le titre courant d’un dictionnaire nomme la dernière entrée de la page.
- Un titre sans l’attribut imprime une chaîne vide, sans avertissement : une lettre sans
place="…"reçoit une date de lieu qui commence par une virgule. Vérifiez le titre de chaque lettre. - Les lettres gardent les signes ; : ? et ! sans espace de la transcription. L’usage français place devant eux une espace fine insécable (U+202F) ; tapée à cet endroit, elle garde chaque signe sur la ligne de son mot.
- Crimson Pro n’a pas les lettres modificatives ᵉ (U+1D49) ni ʳ (U+02B3) : un 1ᵉʳ écrit en Unicode s’imprimerait dans une police de secours. Écrivez plutôt
1^er^: la note liminaire, qui est du texte courant, le compose en exposant, tout comme la date de lieu et le titre courant du recto, dont les éléments de texte portentinlineMarks: true.
Crédits
- Recette
- Ignacio Ferro
- Texte
- 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 · domaine public
- The headnote and the colophon · Ignacio Ferro · CC BY 4.0
- Images
- The cover: two folded letters and a wax seal, a photograph · Generated With Diffusion Models · original
- The cover’s green morocco and gilt fillet, drawn in code · Ignacio Ferro · CC BY 4.0
- Polices
- Crimson Pro (SIL OFL 1.1) · IM Fell French Canon (SIL OFL 1.1) · IM Fell DW Pica SC (SIL OFL 1.1)
Modifier ce texte ↗ (s’ouvre dans un nouvel onglet)Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)


