Vai al contenuto principale
Ricetta numero 30

Ricettario · Capitolo 5 · Struttura del libro

Un’antologia con le firme degli autori

Tre saggi sul camminare i cui titoli portano autore, anno e rivista: l’apertura li stampa tutti e tre; le testatine del verso e l’indice stampano l’autore.

In questa pagina

pp. 2–3 di 9

  • Esempio in inglese: non c’è ancora un’edizione italiana
  • Formato 135 × 180 mm
  • 1 colonna
  • Spectral 10/14
  • Gloock
  • Hanken Grotesk
  • 9 pagine
  • Livello
  • Postext 1.31.0
  • Impaginato in 76 ms
  • 175 righe di codice

In breve

Un piccolo libro di tre saggi sul camminare. L’autore e l’anno di ogni saggio si scrivono una volta sola e poi compaiono dovunque il libro ne abbia bisogno, come nella pagina dell’indice.

Che cosa comporrai

Nove pagine di Afoot, un’antologia tascabile di tre saggi sul camminare, di Hazlitt (1822), Thoreau (1862) e Stevenson (1876). Un dipinto di una brughiera al tramonto riempie la copertina, un JPEG generato con modelli di diffusione. L’indice dà a ogni saggio un numero color ruggine, un titolo in Gloock, puntini di guida spaziati fino alla sua pagina e il nome dell’autore in corsivo color erica sotto il titolo. Ogni saggio si apre con il suo numero e il suo titolo, una firma in maiuscole spaziate e una riga di data con la rivista e l’anno della prima pubblicazione. I versi portano il nome dell’autore, i recto il titolo del saggio. Il titolo di ogni saggio porta autore, anno e fonte come attributi, e l’apertura, le testatine e l’indice li stampano da lì, quindi un quarto saggio non richiede alcuna modifica alla configurazione.

Questa ricetta risponde a

  • Come aggiungo un indice che si aggiorna da solo (puntini di guida, numeri di pagina, autori, righe di parte)?
  • Come aggiungo a un’apertura una riga d’autore, un sommario o un attacco con capolettera?
  • Come imposto le testatine: titolo del libro sulla pagina sinistra, titolo del capitolo sulla destra, numero di pagina all’esterno?

La risposta breve

script.js · righe 44–64nel codice completo
// Each essay's heading carries its credits, and every {attr.…} below reads them:
//   # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"}
// (a value cannot hold { or }, and one with " goes in single quotes; gotcha: attr-values)
// 1 · The opener: inside a heading's design, {attr.author} is that heading's attribute.
const byline = [
  text('byline', '{attr.author}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
    color: col('heather') }, below('title', 5)),
  text('dateline', '{attr.source}, {attr.year}', { fontFamily: 'Spectral', italic: true,
    fontSize: pt(9.5), color: col('muted'), align: 'left' }, below('byline', 1.2)),
];
// 2 · The verso running head (head() is in the running-heads region): in the page header,
//     {attr.author} is the attribute of the essay the page belongs to.
const versoAuthor = head('verso-author', '{attr.author}', 'even',
  at('page', 'top-left', MARGIN.outer + HEAD.gap, HEAD.y));
// 3 · The contents: toc.subtitle prints the attribute it names as a line under each title.
//     'author' and italic are the defaults; attr is written out so it can become 'source'.
const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
  color: col('heather') };
// Hooked up below: byline → the opener, versoAuthor → header, authorLine → toc.subtitle.
// A heading without author="…" gets an empty byline and running head and no line in the
// contents, and the build gives no warning: check every heading.

Ingredienti

Tipo
Spectral, Gloock, Hanken Grotesk (SIL OFL 1.1)
Risorse
  • cover-1200.jpg
  • The cover: a heath and hills at dusk, a painting (Generated With Diffusion Models, originale)

Preparazione

#1 · Numera i saggi e abbassa il loro testo

script.js · righe 68–85nel codice completo
const opener = { enabled: true,
  // The hairline ends 7 mm above the foot of this reserve, so the text starts on the same grid
  // line under every opener whose title fits on one line (SINK = 14 holds a two-line title).
  minHeight: pt(SINK * LEAD),
  slot: { elements: [
    // {number} prints the essay's number as numberingTemplate '{1:I}' writes it: I, II, III.
    text('number', '{number}', { ...gloock, fontSize: pt(34), lineHeight: 1, color: col('rust') },
      at('container', 'top-left', 0, 8)),
    text('title', '{titleText}', { ...gloock, fontSize: pt(26), lineHeight: 1.08,
      color: col('ink') }, below('number', 3, { width: 'fill' })),
    ...byline,
    { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'),
      placement: below('dateline', 6) }, // a horizontal rule runs to the column's edge
  ] } };
const essays = { level: 1, numberingTemplate: '{1:I}', advancedDesign: opener,
  marginBottom: pt(0), // the heading's default margin would add to minHeight
  // The cover and contents styles inherit this break (gotcha: style-inherits-break).
  breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page

Il numberingTemplate: '{1:I}' del livello numera i saggi I, II e III, e {number} stampa il numero sopra il titolo (span e design avanzato). minHeight riserva 12 righe da 14 pt e il filetto finisce 7 mm sopra quella riserva, così sotto un titolo di una riga il testo comincia a 79 mm dal bordo superiore in ogni apertura. Una seconda riga di titolo da 26 pt aggiunge 9,9 mm e spinge il testo giù di una riga della griglia; con SINK = 14, i titoli di una e di due righe fanno partire il testo sulla stessa riga. marginBottom: pt(0) impedisce al margine predefinito del titolo di sommarsi alla riserva.

#2 · Lascia che l’indice si compili da solo

script.js · righe 89–99nel codice completo
const ENTRY = 15; // pt: the essay titles in the contents
const contents = { // passed to the config as `toc`
  // An entry's numeral takes the title's face and size and stands on its baseline.
  levels: [{ level: 1, fontFamily: 'Gloock', fontSize: pt(ENTRY), color: col('ink'),
    numberFontWeight: 400, // Gloock has one weight
    numberColor: col('rust'), numberWidth: mm(8), numberGap: mm(3), marginBottom: pt(LEAD) }],
  pageNumber: { fontFamily: 'Hanken Grotesk', fontSize: pt(8.5), fontWeight: 600,
    color: col('muted'), width: mm(6) }, // the leader dots take this face and colour too
  leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up
  subtitle: authorLine,
};

:::toc nel Markdown stampa una voce per ogni titolo elencato: numero, titolo, puntini di guida, etichetta di pagina e, tramite subtitle, l’autore. buildDocument impagina di nuovo il libro finché le etichette di pagina non smettono di cambiare, così un saggio rinominato o allungato aggiorna l’indice (indice). Il numero di una voce prende il carattere e il corpo del titolo, a meno che numberFontFamily o numberFontSize non dicano altro, e poggia sulla linea di base della prima riga del titolo a qualunque corpo. Un libro diviso in parti riceve anche una riga per ogni :::part, disegnata da toc.parts.design, come in Parti a colori.

#3 · Autore sul verso, saggio sul recto

script.js · righe 103–119nel codice completo
// Each head: the label face, the pages of its parity, never an opener (pages: 'body'), where
// the byline names the author. A function declaration, so the answer above can call it.
function head(id, content, parity, placement, look = {}) {
  return { ...text(id, content, { ...label, fontSize: pt(7.5), letterSpacing: pt(1.3),
    color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD.y), folio),
  versoAuthor,
  head('recto-title', '{chapterTitle}', 'odd',
    at('page', 'top-right', -(MARGIN.outer + HEAD.gap), HEAD.y), { align: 'right' }),
  head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD.y),
    { ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', // an opener's folio
  at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };

Ogni testatina è ancorata alla pagina, a 11 mm dal bordo superiore, e parity sceglie il lato: il folio si allinea al bordo esterno del testo e il nome o il titolo sta 7 mm più all’interno. Il pages: 'body' in head() tiene le testatine lontane dalle aperture, dove la firma nomina già l’autore, e il pages: 'opener' del piè di pagina vi stampa invece un folio al piede (elementi di testo). {chapterTitle} stampa il titolo del saggio senza il suo numero.

#4 · Tieni la copertina e l’indice fuori dal conteggio

script.js · righe 123–146nel codice completo
// Unnumbered, so the first essay is I; unlisted; and with no running heads or folio. They
// inherit the level's page break, 'any' (gotcha: style-inherits-break).
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
  { kind: 'image', id: 'art', resourceId: 'cover',
    placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
  text('title', '{titleText}', { ...gloock, fontSize: pt(80), lineHeight: 1, color: col('ink') },
    at('page', 'top-left', MARGIN.inner, 22)), // page 1 is a recto: the inner margin is left
  text('subtitle', '{subtitle}', { fontFamily: 'Spectral', italic: true, fontSize: pt(14),
    color: col('ink'), align: 'left' }, below('title', 1)),
  text('authors', '{attr.authors}', { ...label, fontSize: pt(8.5), letterSpacing: pt(2),
    color: col('heather') }, below('subtitle', 5)),
  text('imprint', '{attr.imprint}', { ...label, fontSize: pt(7.5), letterSpacing: pt(1.8),
    color: col('paper') }, at('page', 'bottom-left', MARGIN.inner, -12)),
] } };
// span: 'page' in a one-column book: a design kept in its column is cut at the column's foot,
// which would leave a band of paper under the dusk.
const coverStyle = { id: 'cover', ...bare, span: 'page', advancedDesign: cover };
const contentsOpener = { enabled: true, slot: { elements: [ // {title}, {subtitle}: frontmatter
  text('kicker', '{title} · {subtitle}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
    color: col('heather') }, at('container', 'top-left', 0, 8)),
  text('title', '{titleText}', { ...gloock, fontSize: pt(26), color: col('ink') },
    below('kicker', 2.5)),
] } };

Con numbered: false la copertina e l’indice non fanno avanzare il contatore, quindi Hazlitt è I, e toc: false li tiene fuori dall’elenco (stili dei titoli). Entrambi gli stili ereditano il breakBefore del livello, con parità 'any', ed è per questo che l’indice segue la copertina a pagina 2 senza un :::pagebreak. Lo stile della copertina occupa tutta la pagina in un libro a una colonna perché un design tenuto nella sua colonna viene tagliato al piede della colonna, cosa che lascerebbe 22 mm di carta sotto il paesaggio.

#5 · Scrivi ogni colore con il suo collegamento alla palette

script.js · righe 14–28nel codice completo
const palette = {
  ink: '#241f26', // text: a plum-tinted near-black
  paper: '#f7f2e8', // the page
  heather: '#6b4468', // the bylines and the authors in the contents
  rust: '#a4502a', // the essay numbers
  rule: '#d5cabd', // hairlines
  muted: '#6d6570', // running heads, datelines, page numbers in the contents
};
// col() links a colour to its palette entry; the hex is the value the entry holds.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];

col() scrive l’esadecimale di ogni colore accanto al suo paletteId, e il motore segue quel collegamento: le firme, le testatine e la copertina prendono ciò che colorPalette contiene sotto quell’id, quindi una seconda palette con gli stessi id ricolora l’intero libro. Puntare main-color sull’inchiostro copre tutti i valori predefiniti che la configurazione non ridichiara, che altrimenti uscirebbero nel blu del motore, #295AA3.

La ricetta completa

Sandbox
// ═══ Postext Cookbook · Nº 030 · Anthology with bylines ═══════════════════════════════
// https://postext.dev/en/cookbook/anthology-with-bylines
// Code: MIT · Text: Hazlitt, Thoreau, Stevenson (public domain) · Cover: diffusion models
// Fonts: Spectral, Gloock, Hanken Grotesk (SIL OFL 1.1) · 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 = 'anthology-with-bylines';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: heather for the credits, rust for the numbers, plum ink on warm paper
const palette = {
  ink: '#241f26', // text: a plum-tinted near-black
  paper: '#f7f2e8', // the page
  heather: '#6b4468', // the bylines and the authors in the contents
  rust: '#a4502a', // the essay numbers
  rule: '#d5cabd', // hairlines
  muted: '#6d6570', // running heads, datelines, page numbers in the contents
};
// col() links a colour to its palette entry; the hex is the value the entry holds.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  // The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue.
  { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
// #endregion
const TRIM = { width: 135, height: 180 }; // a pocket book, 3 : 4
const MARGIN = { top: 20, bottom: 22, inner: 19, outer: 15 }; // mirrored
const LEAD = 14; // body leading in pt: the baseline grid
const SINK = 12; // grid lines an essay opener reserves above its first line of text
const HEAD = { y: 11, gap: 7 }; // running heads: mm from the top edge, folio to words
const label = { fontFamily: 'Hanken Grotesk', fontWeight: 600, textTransform: 'uppercase',
  align: 'left' }; // design text is centred by default
// One weight, no italic; 'wrap' breaks a long title onto a second line.
const gloock = { fontFamily: 'Gloock', overflow: 'wrap', align: 'left' };
const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } });
const below = (id, y, size) => ({ ...at(`#${id}`, 'below', 0, y), ...(size && { size }) });
const text = (id, content, look, placement) => ({ kind: 'text', id, content, ...look, placement });

// #region answer: one set of heading attributes, read in three places
// Each essay's heading carries its credits, and every {attr.…} below reads them:
//   # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"}
// (a value cannot hold { or }, and one with " goes in single quotes; gotcha: attr-values)
// 1 · The opener: inside a heading's design, {attr.author} is that heading's attribute.
const byline = [
  text('byline', '{attr.author}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
    color: col('heather') }, below('title', 5)),
  text('dateline', '{attr.source}, {attr.year}', { fontFamily: 'Spectral', italic: true,
    fontSize: pt(9.5), color: col('muted'), align: 'left' }, below('byline', 1.2)),
];
// 2 · The verso running head (head() is in the running-heads region): in the page header,
//     {attr.author} is the attribute of the essay the page belongs to.
const versoAuthor = head('verso-author', '{attr.author}', 'even',
  at('page', 'top-left', MARGIN.outer + HEAD.gap, HEAD.y));
// 3 · The contents: toc.subtitle prints the attribute it names as a line under each title.
//     'author' and italic are the defaults; attr is written out so it can become 'source'.
const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
  color: col('heather') };
// Hooked up below: byline → the opener, versoAuthor → header, authorLine → toc.subtitle.
// A heading without author="…" gets an empty byline and running head and no line in the
// contents, and the build gives no warning: check every heading.
// #endregion

// #region opener: the essay's number and title, then the byline, over a sunk first line
const opener = { enabled: true,
  // The hairline ends 7 mm above the foot of this reserve, so the text starts on the same grid
  // line under every opener whose title fits on one line (SINK = 14 holds a two-line title).
  minHeight: pt(SINK * LEAD),
  slot: { elements: [
    // {number} prints the essay's number as numberingTemplate '{1:I}' writes it: I, II, III.
    text('number', '{number}', { ...gloock, fontSize: pt(34), lineHeight: 1, color: col('rust') },
      at('container', 'top-left', 0, 8)),
    text('title', '{titleText}', { ...gloock, fontSize: pt(26), lineHeight: 1.08,
      color: col('ink') }, below('number', 3, { width: 'fill' })),
    ...byline,
    { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'),
      placement: below('dateline', 6) }, // a horizontal rule runs to the column's edge
  ] } };
const essays = { level: 1, numberingTemplate: '{1:I}', advancedDesign: opener,
  marginBottom: pt(0), // the heading's default margin would add to minHeight
  // The cover and contents styles inherit this break (gotcha: style-inherits-break).
  breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page
// #endregion

// #region contents: the essays' numbers, titles, leaders and page labels, from the headings
const ENTRY = 15; // pt: the essay titles in the contents
const contents = { // passed to the config as `toc`
  // An entry's numeral takes the title's face and size and stands on its baseline.
  levels: [{ level: 1, fontFamily: 'Gloock', fontSize: pt(ENTRY), color: col('ink'),
    numberFontWeight: 400, // Gloock has one weight
    numberColor: col('rust'), numberWidth: mm(8), numberGap: mm(3), marginBottom: pt(LEAD) }],
  pageNumber: { fontFamily: 'Hanken Grotesk', fontSize: pt(8.5), fontWeight: 600,
    color: col('muted'), width: mm(6) }, // the leader dots take this face and colour too
  leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up
  subtitle: authorLine,
};
// #endregion

// #region running-heads: author on the verso, essay title on the recto, folios outside
// Each head: the label face, the pages of its parity, never an opener (pages: 'body'), where
// the byline names the author. A function declaration, so the answer above can call it.
function head(id, content, parity, placement, look = {}) {
  return { ...text(id, content, { ...label, fontSize: pt(7.5), letterSpacing: pt(1.3),
    color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { color: col('ink') };
const header = { elements: [
  head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD.y), folio),
  versoAuthor,
  head('recto-title', '{chapterTitle}', 'odd',
    at('page', 'top-right', -(MARGIN.outer + HEAD.gap), HEAD.y), { align: 'right' }),
  head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD.y),
    { ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', // an opener's folio
  at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };
// #endregion

// #region front: the cover and the contents page, two headings kept out of the count
// Unnumbered, so the first essay is I; unlisted; and with no running heads or folio. They
// inherit the level's page break, 'any' (gotcha: style-inherits-break).
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
  { kind: 'image', id: 'art', resourceId: 'cover',
    placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
  text('title', '{titleText}', { ...gloock, fontSize: pt(80), lineHeight: 1, color: col('ink') },
    at('page', 'top-left', MARGIN.inner, 22)), // page 1 is a recto: the inner margin is left
  text('subtitle', '{subtitle}', { fontFamily: 'Spectral', italic: true, fontSize: pt(14),
    color: col('ink'), align: 'left' }, below('title', 1)),
  text('authors', '{attr.authors}', { ...label, fontSize: pt(8.5), letterSpacing: pt(2),
    color: col('heather') }, below('subtitle', 5)),
  text('imprint', '{attr.imprint}', { ...label, fontSize: pt(7.5), letterSpacing: pt(1.8),
    color: col('paper') }, at('page', 'bottom-left', MARGIN.inner, -12)),
] } };
// span: 'page' in a one-column book: a design kept in its column is cut at the column's foot,
// which would leave a band of paper under the dusk.
const coverStyle = { id: 'cover', ...bare, span: 'page', advancedDesign: cover };
const contentsOpener = { enabled: true, slot: { elements: [ // {title}, {subtitle}: frontmatter
  text('kicker', '{title} · {subtitle}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
    color: col('heather') }, at('container', 'top-left', 0, 8)),
  text('title', '{titleText}', { ...gloock, fontSize: pt(26), color: col('ink') },
    below('kicker', 2.5)),
] } };
// #endregion

const config = () => ({ locale: 'en-us', // the sample is in English
  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: { fontFamily: 'Spectral', fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    firstLineIndent: mm(4), indentAfterHeading: false,
    minWordSpacing: 0.7, maxWordSpacing: 1.6 }, // tighter than the 0.6–2 defaults
  // The hidden heading line is still measured, in this face; otherwise the build needs Open Sans.
  headings: { fontFamily: 'Gloock', fontWeight: 400, levels: [essays] },
  headingStyles: [coverStyle, { id: 'contents', ...bare, advancedDesign: contentsOpener }],
  toc: contents,
  // Quoted verse is a :::verse block, a line of verse a line, 8 mm in; 'runon' resumes the
  // sentence after it.
  paragraphStyles: [{ id: 'verse', indent: mm(8) },
    { id: 'runon', firstLineIndent: pt(0) },
    // In the note, under a :::space of one line. It takes the note body's indent, 0.
    { id: 'colophon', fontSize: pt(7.5), lineHeight: pt(10.5), color: col('muted') }],
  calloutStyles: [{ id: 'note', placement: 'fixed', backgroundEnabled: false, // the page foot
    stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') },
    padding: { top: mm(3), right: pt(0), bottom: pt(0), left: pt(0) },
    titleStyle: { ...label, fontSize: pt(7.5), letterSpacing: pt(1.5), color: col('heather') },
    body: { fontSize: pt(9), lineHeight: pt(12.5), firstLineIndent: pt(0) } }],
  header, footer,
});

// #region art: the cover, a painting: a JPEG in assets/ cut to the trim's 3 : 4
// The design's image element names the resource by id; loadImage() below registers the file
// under its fileId. A bitmap is declared at its pixels.
const resources = [{ id: 'cover', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'cover-1200.jpg', format: 'jpeg', width: 1200, height: 1600 },
  altText: 'Heath and hills at dusk under a pale apricot sky, a low sun on the horizon, three '
    + 'trees on a ridge and a footpath winding up from the foot of the page through the '
    + 'heather.' }];
// #endregion

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Esempio Markdown · 64 righe · content.en.mdtitle: "Afoot" subtitle: "Three essays on walking" --- # Afoot {style="cover" authors="Hazlitt · Thoreau · Stevenson" imprint="The Fieldpath Library"} # Contents {style="contents"} :::toc :::callout{type="note" title="A note on the texts"} Each of these essays was first printed in a magazine, two in London and the third in Boston. Hazlitt’s appeared in *The New Monthly Magazine* in January 1822. Thoreau worked his up from a lecture he first gave in 1851, and *The Atlantic Monthly* printed it in June 1862, a month after his death. Stevenson’s came out in *The Cornhill Magazine* in 1876, and in its second paragraph he quotes Hazlitt by name. Each essay starts at its first line and stops well short of its last, in its author’s own spelling; […] marks a cut within a paragraph. :::space{lines=1} :::paragraphs{style="colophon"} Set in Spectral, Gloock and Hanken Grotesk (SIL Open Font License). The essays are in the public domain, from Project Gutenberg eBooks #3020, #1022 and #386; this note is CC BY 4.0; the cover is a painting. ::: ::: # On Going a Journey {author="William Hazlitt" year="1822" source="The New Monthly Magazine"} One of the pleasantest things in the world is going a journey; but I like to go by myself. I can enjoy society in a room; but out of doors, nature is company enough for me. I am then never less alone than when alone. :::verse{style="verse" align=start} *The fields his study, nature was his book.* ::: I cannot see the wit of walking and talking at the same time. When I am in the country I wish to vegetate like the country. I am not for criticising hedge-rows and black cattle. I go out of town in order to forget the town and all that is in it. There are those who for this purpose go to watering-places, and carry the metropolis with them. I like more elbow-room and fewer encumbrances. I like solitude, when I give myself up to it, for the sake of solitude; nor do I ask for :::verse{style="verse" align=start} *A friend in my retreat,* *Whom I may whisper solitude is sweet.* ::: The soul of a journey is liberty, perfect liberty, to think, feel, do, just as one pleases. We go a journey chiefly to be free of all impediments and of all inconveniences; to leave ourselves behind much more to get rid of others. It is because I want a little breathing-space to muse on indifferent matters, where Contemplation :::verse{style="verse" align=start} *May plume her feathers and let grow her wings,* *That in the various bustle of resort* *Were all too ruffled, and sometimes impair’d,* ::: :::paragraphs{style="runon"} that I absent myself from the town for a while, without feeling at a loss the moment I am left by myself. Instead of a friend in a postchaise or in a Tilbury, to exchange good things with, and vary the same stale topics over again, for once let me have a truce with impertinence. Give me the clear blue sky over my head, and the green turf beneath my feet, a winding road before me, and a three hours’ march to dinner—and then to thinking! It is hard if I cannot start some game on these lone heaths. I laugh, I run, I leap, I sing for joy. […] ::: # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"} I wish to speak a word for Nature, for absolute Freedom and Wildness, as contrasted with a freedom and culture merely civil,—to regard man as an inhabitant, or a part and parcel of Nature, rather than a member of society. I wish to make an extreme statement, if so I may make an emphatic one, for there are enough champions of civilization: the minister and the school committee and every one of you will take care of that. I have met with but one or two persons in the course of my life who understood the art of Walking, that is, of taking walks—who had a genius, so to speak, for sauntering, which word is beautifully derived “from idle people who roved about the country, in the Middle Ages, and asked charity, under pretense of going à la Sainte Terre,” to the Holy Land, till the children exclaimed, “There goes a Sainte-Terrer,” a Saunterer, a Holy-Lander. They who never go to the Holy Land in their walks, as they pretend, are indeed mere idlers and vagabonds; but they who do go there are saunterers in the good sense, such as I mean. Some, however, would derive the word from sans terre without land or a home, which, therefore, in the good sense, will mean, having no particular home, but equally at home everywhere. For this is the secret of successful sauntering. He who sits still in a house all the time may be the greatest vagrant of all; but the saunterer, in the good sense, is no more vagrant than the meandering river, which is all the while sedulously seeking the shortest course to the sea. But I prefer the first, which, indeed, is the most probable derivation. For every walk is a sort of crusade, preached by some Peter the Hermit in us, to go forth and reconquer this Holy Land from the hands of the Infidels. It is true, we are but faint-hearted crusaders, even the walkers, nowadays, who undertake no persevering, never-ending enterprises. Our expeditions are but tours, and come round again at evening to the old hearth-side from which we set out. Half the walk is but retracing our steps. We should go forth on the shortest walk, perchance, in the spirit of undying adventure, never to return,—prepared to send back our embalmed hearts only as relics to our desolate kingdoms. If you are ready to leave father and mother, and brother and sister, and wife and child and friends, and never see them again,—if you have paid your debts, and made your will, and settled all your affairs, and are a free man; then you are ready for a walk. To come down to my own experience, my companion and I, for I sometimes have a companion, take pleasure in fancying ourselves knights of a new, or rather an old, order—not Equestrians or Chevaliers, not Ritters or Riders, but Walkers, a still more ancient and honorable class, I trust. The chivalric and heroic spirit which once belonged to the Rider seems now to reside in, or perchance to have subsided into, the Walker—not the Knight, but Walker Errant. He is a sort of fourth estate, outside of Church and State and People. We have felt that we almost alone hereabouts practiced this noble art; though, to tell the truth, at least if their own assertions are to be received, most of my townsmen would fain walk sometimes, as I do, but they cannot. No wealth can buy the requisite leisure, freedom, and independence which are the capital in this profession. It comes only by the grace of God. It requires a direct dispensation from Heaven to become a walker. You must be born into the family of the Walkers. Ambulator nascitur, non fit. Some of my townsmen, it is true, can remember and have described to me some walks which they took ten years ago, in which they were so blessed as to lose themselves for half an hour in the woods; but I know very well that they have confined themselves to the highway ever since, whatever pretensions they may make to belong to this select class. No doubt they were elevated for a moment as by the reminiscence of a previous state of existence, when even they were foresters and outlaws. # Walking Tours {author="Robert Louis Stevenson" year="1876" source="The Cornhill Magazine"} It must not be imagined that a walking tour, as some would have us fancy, is merely a better or worse way of seeing the country. There are many ways of seeing landscape quite as good; and none more vivid, in spite of canting dilettantes, than from a railway train. But landscape on a walking tour is quite accessory. He who is indeed of the brotherhood does not voyage in quest of the picturesque, but of certain jolly humours—of the hope and spirit with which the march begins at morning, and the peace and spiritual repletion of the evening’s rest. He cannot tell whether he puts his knapsack on, or takes it off, with more delight. […] Now, to be properly enjoyed, a walking tour should be gone upon alone. If you go in a company, or even in pairs, it is no longer a walking tour in anything but name; it is something else and more in the nature of a picnic. A walking tour should be gone upon alone, because freedom is of the essence; because you should be able to stop and go on, and follow this way or that, as the freak takes you; and because you must have your own pace, and neither trot alongside a champion walker, nor mince in time with a girl. And then you must be open to all impressions and let your thoughts take colour from what you see. You should be as a pipe for any wind to play upon. “I cannot see the wit,” says Hazlitt, “of walking and talking at the same time. When I am in the country I wish to vegetate like the country,”—which is the gist of all that can be said upon the matter. There should be no cackle of voices at your elbow, to jar on the meditative silence of the morning. And so long as a man is reasoning he cannot surrender himself to that fine intoxication that comes of much motion in the open air, that begins in a sort of dazzle and sluggishness of the brain, and ends in a peace that passes comprehension.
`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces Spectral: ['400', '400i'], Gloock: ['400'], 'Hanken Grotesk': ['600'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadImage('cover-1200.jpg', asset('cover-1200.jpg')); const doc = await buildDocumentWithFonts({ markdown, resources }, config(), kitFonts(FONTS)); showPages(doc, { title: t({ en: 'Anthology with bylines', es: 'Antología con firmas de autor' }) });
Kit · core, fonts, viewer, images: uguale in ogni ricetta · 270 righe// ─── 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 ───────────────────────────────────────────────────────────────────────

Lo script.js composto funziona così com’è: incollalo nello script modulo di qualsiasi pagina, oppure apri la ricetta su CodePen. Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)

Varianti

#Elenca dove è apparso per la prima volta ogni saggio

toc.subtitle stampa qualsiasi attributo del titolo, quindi lo stesso Markdown può elencare sotto il titolo la prima rivista di ogni saggio.

-const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
+const authorLine = { enabled: true, attr: 'source', fontFamily: 'Spectral', fontSize: pt(10),

#Metti il titolo del libro sul verso

Molti libri mettono il proprio titolo sul verso e quello del capitolo sul recto; {title} lo legge dal frontmatter del Markdown e stampa «AFOOT».

-const versoAuthor = head('verso-author', '{attr.author}', 'even',
+const versoAuthor = head('verso-author', '{title}', 'even',

#Apri ogni saggio su un recto

Con la parità 'odd', Stevenson passa a pagina 9 dietro una pagina 8 bianca. I due stili delle pagine preliminari hanno allora bisogno di un loro 'any', altrimenti l’indice eredita 'odd' e si apre a pagina 3 dietro un verso bianco.

-  breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page
+  breakBefore: { enabled: true, parity: 'odd' } };
-const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
+const bare = { numbered: false, toc: false, breakBefore: { enabled: true, parity: 'any' },
+  header: { elements: [] }, footer: { elements: [] } };

Errori frequenti

Errore frequente

Valori degli attributi: niente { o }; virgolette semplici per un valore con "

Il valore di un attributo finisce alla graffa di chiusura, quindi non può contenere { o }. Un valore che contiene una virgoletta doppia va tra virgolette semplici; il simbolo del dollaro va bene. Attributi dei titoli →

Errore frequente

Uno stile di titolo eredita l’interruzione di pagina del suo livello

Una voce di headingStyles prende dal suo livello di titolo ogni campo che omette, breakBefore compreso. Un indice o un colophon con lo stile di un H1 dopo un :::pagebreak eredita parity 'odd' e finisce dietro una pagina bianca. Dai a uno stile del genere breakBefore: { enabled: false }. Stili di titolo →

  • Hazlitt cita versi in mezzo alle sue frasi. I versi vanno in un blocco :::verse{style="verse" align=start}, una riga di Markdown per verso, rientrata di 8 mm, e la frase che riprende dopo va in uno stile runon senza rientro. Una citazione in blocco del Markdown non andrebbe bene: le sue righe si uniscono come quelle di un paragrafo, quindi i versi scorrerebbero come prosa.
  • Un attributo che manca a un titolo non stampa nulla e non genera alcun avviso. Senza author="…", la firma e la testatina del verso di un saggio escono vuote e la sua riga sotto il titolo sparisce dall’indice.

Crediti

Testo
Immagini
  • The cover: a heath and hills at dusk, a painting · Generated With Diffusion Models · originale
Font
Spectral (SIL OFL 1.1) · Gloock (SIL OFL 1.1) · Hanken Grotesk (SIL OFL 1.1)
Sandbox