Aller au contenu principal
Recette numéro 62

Recettes · Chapitre 2 · Caractères et texte

Vers figurés : Easter Wings de Herbert, centré vers à vers

Easter Wings et The Altar de George Herbert composés en texte courant centré, un paragraphe par vers : la longueur des lignes dessine les deux figures.

Sur cette page
Genre
Poésie
Sortie
Canvas
Niveau
Débutant
Postext
Testé avec Postext 1.31.0
Nécessite ≥ 1.25.0
Licence
Mis à jour le 11 oct. 2026
Code MIT · Texte CC BY 4.0

p. 2–3 sur 4

  • Exemple en anglais : pas encore d’édition française
  • Format 148 × 210 mm
  • 1 colonne
  • IM Fell English 12,4/17
  • IM Fell English SC
  • IM Fell Great Primer
  • 4 pages
  • Niveau
  • Postext 1.29.1
  • Mis en page en 42 ms
  • 99 lignes de code

En bref

Deux vieux poèmes anglais dont les vers sont imprimés pour que chaque poème ressemble à un dessin : un autel et une paire d’ailes. Chaque vers est simplement centré, et sa longueur dessine la forme.

Ce que vous allez composer

Un livret de quatre pages A5, une feuille A4 pliée en deux, qui contient deux poèmes figurés de The Temple de George Herbert dans l’orthographe de la première édition de 1633. La page de titre suit celle de 1633, jusqu’à son épigraphe tirée du psaume 29. À l’intérieur, The Altar fait face à Easter Wings. Aucun vers de l’un ou l’autre poème n’est en retrait ni complété par des espaces ; chacun est centré, si bien que l’autel repose sur un pilier étroit et que chaque strophe des ailes se resserre jusqu’à deux mots puis s’élargit à nouveau. Sur la dernière page, sous la devise de Herbert, une note raconte comment les imprimeurs de 1633 composèrent les ailes de côté, et le colophon raccourcit ligne après ligne. Tout est composé en texte courant ou dans l’un des trois niveaux de titre, avec les caractères IM Fell, sans style nommé ni maquette de page. L’édition espagnole compose une version vers à vers qui garde les deux formes.

Cette recette répond à

  • Comment ajouter de l’espace vertical entre deux blocs, quand les lignes vides n’y font rien ?
  • Comment composer de la poésie : un vers par ligne, blancs entre les strophes, retraits suspendus pour les vers renvoyés, sans césure ?
  • Comment composer une épigraphe, une dédicace, une signature ou une citation en exergue avec un grand guillemet ?
  • Comment forcer un saut de page ou de colonne, et faire commencer chaque chapitre sur une page de droite ?

La réponse courte

script.js · lignes 26–38dans le code complet
// Postext joins the lines of a Markdown paragraph, so every line of the poem is a paragraph
// of its own, with a blank line after it. Centred, each line keeps its natural width on the
// axis of the measure, and the lengths Herbert gave his lines draw the figure:
//   Lord, who createdst man in wealth and store,
//
//   Though foolishly he lost the same,
//
//   Decaying more and more,
const verse = {
  fontFamily: 'IM Fell English', fontSize: pt(12.4), lineHeight: pt(LEAD), color: col('ink'),
  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
  firstLineIndent: pt(0), // the default 1.5 em moves every line 3.3 mm off the titles' axis
};

Ingrédients

Typographie
IM Fell English, IM Fell Great Primer, IM Fell English SC (SIL OFL 1.1)
Ressources
Aucune : chaque image est dessinée en code

Préparation

#1 · Un vers est un paragraphe centré

Le code est dans la réponse courte plus haut. Postext réunit les lignes d’un paragraphe Markdown en une seule : chaque vers est donc un paragraphe, suivi d’une ligne vide. textAlign: 'center' compose chaque vers à sa largeur naturelle sur l’axe de la justification, et comme seul le texte justifié est coupé tant que hyphenation.ragged n’est pas activé, aucun mot n’est coupé (texte courant). Chaque vers est la première ligne de son paragraphe : le retrait de première ligne par défaut, 1,5 em, le décalerait de 3,3 mm à droite de l’axe des titres ; firstLineIndent: pt(0) garde poèmes et titres sur un même axe.

#2 · La justification prend le vers le plus long

script.js · lignes 42–47dans le code complet
// A line too long for the measure wraps, and both halves are centred, which adds a step to
// the outline.
// The widest line, 'Lord, who createdst man in wealth and store,', is 80.7 mm in 12.4 pt
// Fell, so the 110 mm measure takes every line of both poems whole.
const TRIM = { w: 148, h: 210 }; // mm: A5, one A4 sheet folded once
const MARGIN = { top: 22, bottom: 24, inner: 17, outer: 21 }; // mm: the measure is 110 mm

Un vers qui ne tient pas passe à la ligne, et les deux moitiés sont centrées : le contour reçoit alors une marche qui n’est pas dans la figure de Herbert. Avec une justification de 75 mm, trois vers de The Altar et deux d’Easter Wings passent à la ligne, et le livret s’étend sur six pages. Le vers le plus large ici, le premier d’Easter Wings, mesure 80,7 mm en anglais et 80,0 mm en espagnol : la justification de 110 mm de la page A5 prend donc chaque vers entier.

#3 · Des titres sobres, centrés

script.js · lignes 51–71dans le code complet
// headings.textAlign centres every level. The Fell faces ship weight 400 only, so no level
// asks for bold.
const titles = {
  fontFamily: 'IM Fell Great Primer', fontWeight: 400, color: col('violet'), textAlign: 'center',
  levels: [
    // The keepsake's title opens page 1, where a break changes nothing; a second # title
    // would open on a recto.
    { level: 1, fontSize: pt(40), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      marginBottom: pt(LEAD), breakBefore: { enabled: true, parity: 'odd' } },
    // A poem opens the next page, recto or verso: The Altar (page 2) faces Easter Wings.
    // A heading that opens a page drops its top margin, so the title is lowered by its
    // line box instead: three lines deep, it sets the title about 5 mm down. The default
    // half-em margin under it rounds up to a whole line, and each poem starts on line 5.
    { level: 2, fontSize: pt(24), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      breakBefore: { enabled: true, parity: 'any' } },
    // The small-capital labels on pages 1 and 4: the sister face at the body size, with no
    // margins, since the :::space lines place them.
    { level: 3, fontFamily: 'IM Fell English SC', fontSize: pt(12.4), lineHeight: pt(LEAD),
      marginTop: pt(0), marginBottom: pt(0) },
  ],
};

Aucun niveau n’a de maquette, seulement des réglages typographiques, et headings.textAlign centre les trois (titres). La parité 'any' ouvre chaque poème à la page suivante, recto ou verso : l’autel de la page 2 fait donc face aux ailes de la page 3. Un marginTop ne peut pas abaisser ces titres, car l’espace au-dessus d’un titre qui ouvre une page est supprimé. C’est la boîte de ligne du niveau 2, profonde de trois lignes, qui abaisse chaque titre d’environ 5 mm, et les deux poèmes commencent à la ligne 5 du bloc de texte. Le niveau 1 ouvre le document et n’a pas besoin de saut ici ; il en porte un quand même, pour qu’un second titre # s’ouvre sur un recto. Le niveau 3 compose les quatre étiquettes des pages 1 et 4 dans les petites capitales d’IM Fell English SC, au corps du texte.

#4 · Le Markdown espace et découpe les pages

script.js · lignes 75–92dans le code complet
// A blank line adds no space, so every gap on these pages is a :::space{lines=N}, which adds
// N lines of the 17 pt grid (one without the attribute). ## opens a page for each poem, and
// :::pagebreak opens the last page.
const config = () => ({
  colorPalette,
  locale: t({ en: 'en-us', es: 'es' }), // the language of each edition, and of its bundle
  page: { // mirror: left is the inner margin; 150 dpi is for the screen
    sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
      right: mm(MARGIN.outer), mirror: true },
  },
  layout: { layoutType: 'single' }, // the default is two columns
  bodyText: verse, // a :ref added to these pages takes main-color, the ink, like bold and italic
  headings: titles,
  header: { elements: [] }, // a folded keepsake of four pages: no running heads,
  footer: { elements: [] }, // and no folios
});

Une ligne vide entre deux paragraphes n’ajoute aucun blanc : chaque espace de ces pages vient donc de :::space, une ligne entre les strophes d’Easter Wings et, sur la page de titre, cinq lignes au-dessus de l’étiquette du psaume et dix sous l’épigraphe (:::space). Avec ces valeurs, la mention d’éditeur de la page 1 se termine sur la dernière ligne du bloc de texte, et les étiquettes au-dessus du psaume et de la note se tiennent toutes deux sur la ligne 13, à 94 mm du haut de la page. :::pagebreak ouvre la dernière page (:::pagebreak). Le colophon est lui aussi composé à raison d’un paragraphe par ligne, coupé là où finit une expression : ses quatre lignes s’effilent ainsi en cul-de-lampe.

La recette complète

Sandbox
// ═══ Postext Cookbook · Nº 062 · Shaped verse: Herbert's Easter Wings, centred line by line ═
// https://postext.dev/en/cookbook/shaped-verse
// Code: MIT · Text: G. Herbert, The Temple, 1633 (PD; EEBO-TCP, CC0) · Spanish version: CC BY 4.0
// Fonts: IM Fell English, Great Primer and English SC (SIL OFL 1.1) · Needs postext ≥ 1.25.0
import { buildDocumentWithFonts, renderPageToCanvas } from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'shaped-verse';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // every colour in the config links to one of these
  ink: '#241d29', // the text: a violet-black
  violet: '#5c2a6f', // the one accent: the titles and the small-capital labels
  paper: '#fbf7ee', // the sheet: an uncoated cream
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // Italic and bold take their colour from 'main-color' unless set. With 'main-color' in ink,
  // the italic subtitle, epigraph and motto print in ink instead of the default blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
const LEAD = 17; // pt: the leading of every line, verse and prose alike

// #region answer: centred text, one line of verse to a paragraph
// Postext joins the lines of a Markdown paragraph, so every line of the poem is a paragraph
// of its own, with a blank line after it. Centred, each line keeps its natural width on the
// axis of the measure, and the lengths Herbert gave his lines draw the figure:
//   Lord, who createdst man in wealth and store,
//
//   Though foolishly he lost the same,
//
//   Decaying more and more,
const verse = {
  fontFamily: 'IM Fell English', fontSize: pt(12.4), lineHeight: pt(LEAD), color: col('ink'),
  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
  firstLineIndent: pt(0), // the default 1.5 em moves every line 3.3 mm off the titles' axis
};
// #endregion

// #region measure: an A5 leaf whose measure holds the longest line whole
// A line too long for the measure wraps, and both halves are centred, which adds a step to
// the outline.
// The widest line, 'Lord, who createdst man in wealth and store,', is 80.7 mm in 12.4 pt
// Fell, so the 110 mm measure takes every line of both poems whole.
const TRIM = { w: 148, h: 210 }; // mm: A5, one A4 sheet folded once
const MARGIN = { top: 22, bottom: 24, inner: 17, outer: 21 }; // mm: the measure is 110 mm
// #endregion

// #region titles: plain headings, centred, in the display face and the accent
// headings.textAlign centres every level. The Fell faces ship weight 400 only, so no level
// asks for bold.
const titles = {
  fontFamily: 'IM Fell Great Primer', fontWeight: 400, color: col('violet'), textAlign: 'center',
  levels: [
    // The keepsake's title opens page 1, where a break changes nothing; a second # title
    // would open on a recto.
    { level: 1, fontSize: pt(40), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      marginBottom: pt(LEAD), breakBefore: { enabled: true, parity: 'odd' } },
    // A poem opens the next page, recto or verso: The Altar (page 2) faces Easter Wings.
    // A heading that opens a page drops its top margin, so the title is lowered by its
    // line box instead: three lines deep, it sets the title about 5 mm down. The default
    // half-em margin under it rounds up to a whole line, and each poem starts on line 5.
    { level: 2, fontSize: pt(24), lineHeight: pt(LEAD * 3), textTransform: 'uppercase',
      breakBefore: { enabled: true, parity: 'any' } },
    // The small-capital labels on pages 1 and 4: the sister face at the body size, with no
    // margins, since the :::space lines place them.
    { level: 3, fontFamily: 'IM Fell English SC', fontSize: pt(12.4), lineHeight: pt(LEAD),
      marginTop: pt(0), marginBottom: pt(0) },
  ],
};
// #endregion

// #region pages: four pages, spaced and broken from the Markdown
// A blank line adds no space, so every gap on these pages is a :::space{lines=N}, which adds
// N lines of the 17 pt grid (one without the attribute). ## opens a page for each poem, and
// :::pagebreak opens the last page.
const config = () => ({
  colorPalette,
  locale: t({ en: 'en-us', es: 'es' }), // the language of each edition, and of its bundle
  page: { // mirror: left is the inner margin; 150 dpi is for the screen
    sizePreset: 'custom', width: mm(TRIM.w), height: mm(TRIM.h), dpi: 150,
    backgroundColor: col('paper'),
    margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner),
      right: mm(MARGIN.outer), mirror: true },
  },
  layout: { layoutType: 'single' }, // the default is two columns
  bodyText: verse, // a :ref added to these pages takes main-color, the ink, like bold and italic
  headings: titles,
  header: { elements: [] }, // a folded keepsake of four pages: no running heads,
  footer: { elements: [] }, // and no folios
});
// #endregion

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Exemple Markdown · 129 lignes · content.en.mdtitle: "The Temple" author: "George Herbert" --- # The Temple *Sacred Poems and Private Ejaculations* :::space By Mr. George Herbert :::space{lines=5} ### Psal. 29. *In his Temple doth every man speak of his honour.* :::space{lines=10} ### Two shaped poems in the text of the first edition Cambridge, 1633 ## The Altar A broken ALTAR, Lord, thy servant reares, Made of a heart, and cemented with teares: Whose parts are as thy hand did frame; No workmans tool hath touch’d the same. A HEART alone Is such a stone, As nothing but Thy pow’r doth cut. Wherefore each part Of my hard heart Meets in this frame, To praise thy name. That if I chance to hold my peace, These stones to praise thee may not cease. O let thy blessed SACRIFICE be mine, And sanctifie this ALTAR to be thine. ## Easter Wings Lord, who createdst man in wealth and store, Though foolishly he lost the same, Decaying more and more, Till he became Most poore: With thee O let me rise As larks, harmoniously, And sing this day thy victories: Then shall the fall further the flight in me. :::space My tender age in sorrow did beginne: And still with sicknesses and shame Thou didst so punish sinne, That I became Most thinne. With thee Let me combine, And feel this day thy victorie: For, if I imp my wing on thine, Affliction shall advance the flight in me. :::pagebreak ### Herbert’s motto *Lesse then the least of Gods mercies.* :::space With it, his printers wrote, he used to conclude “all things that might seem to tend any way to his own honour”. :::space{lines=7} ### A note on the text The poems keep the spelling of the first edition, printed at Cambridge in 1633. There “The Altar” stands upright on page 18, and the two stanzas of “Easter Wings” lie sideways on pages 34 and 35. Here they stand upright; give the booklet a quarter turn and each opens into a pair of wings. :::space{lines=5} Set in IM Fell English, English SC and Great Primer, Igino Marini’s revivals of the types John Fell gathered at Oxford. Text from EEBO-TCP.
`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses. Layout measures with the browser's fonts, so the // kit loads them from Fontsource before the first build. const FONTS = { 'IM Fell English': ['400', '400i'], 'IM Fell Great Primer': ['400'], 'IM Fell English SC': ['400'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── const doc = await buildDocumentWithFonts({ markdown }, config(), kitFonts(FONTS)); showPages(doc, { title: t({ en: 'Easter Wings and The Altar', es: 'Alas de Pascua y El altar' }) });
Kit · core, fonts, viewer : identique dans toutes les recettes · 224 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 ───────────────────────────────────────────────────────────────────────

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

#Ouvrir chaque poème sur une page de droite

Avec la parité 'odd', The Altar et Easter Wings s’ouvrent aux pages 3 et 5, deux rectos, et le livret passe à six pages, les pages 2 et 4 restant blanches.

-      breakBefore: { enabled: true, parity: 'any' } },
+      breakBefore: { enabled: true, parity: 'odd' } },

#Composer les vers au fer à gauche

Avec textAlign: 'left', chaque vers commence à la marge et les deux poèmes ne gardent que leur contour droit ; le reste du texte courant, du sous-titre au colophon, passe aussi au fer à gauche, et seuls les titres restent centrés.

-  textAlign: 'center', // ragged on both sides; only justified text is hyphenated by default
+  textAlign: 'left', // ragged right; only justified text is hyphenated by default

Erreurs fréquentes

  • Un vers trop long pour la justification passe à la ligne, et ses deux moitiés sont centrées chacune de leur côté. Mesurez le vers le plus large avant de choisir le corps et les marges. Pour des vers au fer à gauche, avec des rejets en retrait, voyez Des poèmes composés vers à vers.
  • Les espaces en début de paragraphe sont supprimées : vous ne pouvez pas donner sa forme à une ligne en la complétant par des espaces. La largeur de chaque vers vient de ses seuls mots, et une traduction doit trouver des vers à peu près aussi larges que ceux de l’original, comme le fait l’édition espagnole.
  • Le centrage garde chaque vers à sa largeur naturelle : la table et la base de The Altar ne sortent donc pas d’équerre, les troisième et quatrième vers de la table étant plus étroits que les deux premiers. La justification ne peut pas les équarrir non plus, car la dernière ligne d’un paragraphe justifié garde sa largeur naturelle et chacun de ces vers est un paragraphe à lui seul.
  • La page de titre et la dernière page sont remplies jusqu’à la dernière ligne de la grille. Une ligne de plus sur la page de titre, venue d’une épigraphe plus longue, d’un vers qui passe à la ligne ou d’un :::space plus grand, envoie l’étiquette du pied et les deux lignes qui la suivent sur une page à part ; une ligne de plus sur la dernière page envoie la dernière ligne du colophon, seule, sur une cinquième page. Pour ajouter une ligne à l’une ou l’autre page, retirez-en une d’un :::space placé au-dessus.
  • Postext ne compose pas de texte de côté, comme les imprimeurs de 1633 l’ont fait pour Easter Wings : seuls les figures et les tableaux entiers pivotent. Ici, les strophes restent droites, et la note de la page 4 le précise.

Crédits

Texte
  • “The Altar” and “Easter wings”, the title, subtitle, byline and epigraph of the title page, and Herbert’s motto with a clause of the printers’ preface, from the first edition of The Temple (Cambridge, 1633) in the EEBO-TCP transcription (CC0 1.0); the letters and stops it marks as illegible or unknown at six line ends (four in The Altar, two in Easter wings) are supplied · George Herbert; Text Creation Partnership · domaine public
  • Psalm 29:9 in the Spanish edition, from the Reina-Valera Bible of 1909 · Reina-Valera 1909 · domaine public
  • The Spanish versions of both poems, of the subtitle, the motto and the printers’ clause, and the title-page imprint, the note on the text and the colophon of both editions, written for this recipe · Ignacio Ferro · CC BY 4.0
Polices
IM Fell English (SIL OFL 1.1) · IM Fell Great Primer (SIL OFL 1.1) · IM Fell English SC (SIL OFL 1.1)
Sandbox