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
// 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
- Fonctionnalités
- Retraits, alignement et espacement des paragraphesNiveaux de titreEspace vertical expliciteSauts de page et de colonneChapitres qui commencent en belle pageTexte courantGras, italique et leurs couleursFormat de la pageMarges en miroirUne ou deux colonnesCouleur du papierPalette de couleurs sémantiquePages sur un canvas
- 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
// 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
// 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
// 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
// ═══ 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.md
title: "The Temple" author: "George Herbert" --- # The Temple *Sacred Poems and Private Ejaculations* :::space By Mr. George Herbert :::space{lines=5} ### Psal. 29. *In his Temple doth every man speak of his honour.* :::space{lines=10} ### Two shaped poems in the text of the first edition Cambridge, 1633 ## The Altar A broken ALTAR, Lord, thy servant reares, Made of a heart, and cemented with teares: Whose parts are as thy hand did frame; No workmans tool hath touch’d the same. A HEART alone Is such a stone, As nothing but Thy pow’r doth cut. Wherefore each part Of my hard heart Meets in this frame, To praise thy name. That if I chance to hold my peace, These stones to praise thee may not cease. O let thy blessed SACRIFICE be mine, And sanctifie this ALTAR to be thine. ## Easter Wings Lord, who createdst man in wealth and store, Though foolishly he lost the same, Decaying more and more, Till he became Most poore: With thee O let me rise As larks, harmoniously, And sing this day thy victories: Then shall the fall further the flight in me. :::space My tender age in sorrow did beginne: And still with sicknesses and shame Thou didst so punish sinne, That I became Most thinne. With thee Let me combine, And feel this day thy victorie: For, if I imp my wing on thine, Affliction shall advance the flight in me. :::pagebreak ### Herbert’s motto *Lesse then the least of Gods mercies.* :::space With it, his printers wrote, he used to conclude “all things that might seem to tend any way to his own honour”. :::space{lines=7} ### A note on the text The poems keep the spelling of the first edition, printed at Cambridge in 1633. There “The Altar” stands upright on page 18, and the two stanzas of “Easter Wings” lie sideways on pages 34 and 35. Here they stand upright; give the booklet a quarter turn and each opens into a pair of wings. :::space{lines=5} Set in IM Fell English, English SC and Great Primer, Igino Marini’s revivals of the types John Fell gathered at Oxford. Text from EEBO-TCP.`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses. Layout measures with the browser's fonts, so the // kit loads them from Fontsource before the first build. 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 defaultErreurs 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
:::spaceplus 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:::spaceplacé 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
- Recette
- Ignacio Ferro
- 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)
Modifier ce texte ↗ (s’ouvre dans un nouvel onglet)Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)


