Vai al contenuto principale
Ricetta numero 32

Ricettario · Capitolo 6 · Riquadri e note

Un classico annotato con glosse a margine

Il tè dei matti di Alice in edizione annotata: glosse verdi e rosse nel margine esterno, accanto alle righe che spiegano, richiamate da una lettera colorata.

In questa pagina
Output
Canvas
Livello
Avanzato
Postext
Provata con Postext 1.31.0
Richiede ≥ 1.28.0
Licenza
Aggiornata il 11 ott 2026
Codice MIT · Testo CC BY 4.0

pp. 82–83 · 2–3 di 3

  • Esempio in inglese: non c’è ancora un’edizione italiana
  • Formato 156 × 234 mm
  • Colonna e mezza, Intercolonna di 4,5 mm
  • Unna 9,5/14
  • Cormorant SC
  • Rozha One
  • 3 pagine
  • Livello
  • Postext 1.31.0
  • Impaginato in 91 ms
  • 207 righe di codice

In breve

Alcune pagine di Alice nel Paese delle Meraviglie con le note di un curatore nel margine laterale. Ogni nota sta accanto alle righe che spiega ed è segnalata da una lettera colorata.

Che cosa comporrai

L’inizio del capitolo VII di Alice’s Adventures in Wonderland, il tè dei matti, composto come tre pagine di un’edizione annotata su una pagina editoriale di 156 × 234 mm. Il testo di Carroll scorre in una sola colonna giustificata di Unna, circa 66 caratteri per riga. Un canale di 33 mm sul lato del taglio accoglie le otto glosse del curatore, ciascuna accanto al paragrafo che porta la sua lettera, così che scendono a destra di un recto e a sinistra di un verso. Le glosse su parole e giochi di parole sono verdi, quelle sulla storia rosse, e ogni lettera nel testo prende il colore della sua glossa. Il capitolo si apre sotto un acquerello della tavola del tè visto dall’alto, seguito dal numerale, dal titolo in Rozha One e da un cappello in corsivo con un capolettera verde; la nota su questa edizione sta nel margine accanto al cappello.

Questa ricetta risponde a

  • Come compongo note a margine o glosse accanto al paragrafo che spiegano?
  • Come faccio le note a piè di pagina?
  • Come creo un layout a colonna e mezza, con una colonna di testo larga e una colonna laterale stretta?
  • Come aggiungo a un’apertura una riga d’autore, un sommario o un attacco con capolettera?

La risposta breve

script.js · righe 36–61nel codice completo
const layout = {
  layoutType: 'oneAndHalf', // one text column and a narrower side column
  sideColumnPercent: 26, // of the 127 mm between the margins: a 33 mm channel
  sideColumnRole: 'floats', // no text runs in it: it holds side boxes and figures
  sideColumnSide: 'outer', // at the fore-edge, which mirrored margins move page by page
  gutterWidth: mm(4.5), // the text column keeps 127 − 33 − 4.5 = 89.5 mm
};
// A gloss is :::callout{type="note" span="side" title="a · …"} in the Markdown.
// span="side" moves it out of the flow into the channel, level with the block after
// the fence, so the fence goes just before the paragraph that carries its letter.
// Glosses never float. One that meets the gloss above stacks under it. One that would
// run past the column's foot slides up, as far as the gloss above allows, until its
// foot sits on the foot; if it still does not fit, it waits for the next page
// (gotcha: side-box-starts-at-fence).
const gloss = (id, hue) => ({ id,
  backgroundEnabled: false, // one device: a hairline over the gloss, in its colour
  stripe: { enabled: true, side: 'top', width: pt(0.5), color: col(hue) },
  padding: { top: mm(1.5), right: pt(0), bottom: pt(0), left: pt(0) },
  // Cormorant SC draws lower case as small capitals: the title needs no textTransform.
  titleStyle: { fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5),
    letterSpacing: pt(0.3), color: col(hue), gap: mm(0.8) },
  // Face and colours come from bodyText. A box body also inherits its 4 mm first-line
  // indent, which a gloss sets back to 0.
  body: { fontSize: pt(7.8), lineHeight: pt(10), textAlign: 'left',
    firstLineIndent: pt(0) } });
const calloutStyles = [gloss('note', 'lawn'), gloss('context', 'jam')]; // words, history

Ingredienti

Tipo
Unna, Rozha One, Cormorant SC (SIL OFL 1.1)
Risorse
  • tea-table-1560.jpg
  • The tea-table seen from above, a watercolour (Generated With Diffusion Models, originale)

Preparazione

#1 · Dai il lato del taglio alle glosse

Il codice è la risposta breve qui sopra. In un layout a colonna e mezza, sideColumnPercent: 26 dà alla colonna laterale 33 mm dei 127 mm tra i margini, e il testo conserva 89,5 mm dopo l’intercolonna di 4,5 mm. sideColumnRole: 'floats' tiene Carroll fuori dalla colonna laterale, che contiene solo ciò che vi viene collocato con span: 'side'. Una nota a piè di pagina aspetta al piede della colonna, sotto un filetto; nel margine ogni glossa sta all’altezza della riga che spiega, e le glosse non tolgono righe alla colonna di testo. I due stili di glossa differiscono solo nel colore del filetto sottile e del titolo, così il lettore distingue una glossa su una parola da una glossa sulla storia prima di leggerla.

#2 · Specchia la pagina perché il canale segua il taglio

script.js · righe 75–86nel codice completo
const PT = 25.4 / 72; // mm in a point
const [TRIM_W, TRIM_H] = [156, 234]; // mm
const [TOP, BOTTOM, INNER, OUTER] = [22, 22, 15, 14]; // mm: 38 lines of text
const page = { width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
  backgroundColor: col('paper'),
  // left is a recto's inner margin; mirror swaps the sides on a verso, and 'outer' moves
  // the channel with them: right on a recto, left on a verso.
  margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
    mirror: true } };
const CONTENT_W = TRIM_W - INNER - OUTER; // 127 mm
const SIDE_W = CONTENT_W * layout.sideColumnPercent / 100; // 33 mm: the glosses' measure
const TEXT_W = CONTENT_W - SIDE_W - layout.gutterWidth.value;

Con mirror: true i margini si scambiano su ogni verso, e sideColumnSide: 'outer' sposta il canale con loro, così le glosse scendono a destra di pagina 81 e di pagina 83 e a sinistra di pagina 82. Margini di testa e di piede di 22 mm lasciano 190 mm, lo spazio per 38 righe da 14 pt, e ogni glossa comincia su una riga della stessa griglia.

#3 · Delimita ogni glossa prima del paragrafo che spiega

script.js · righe 90–104nel codice completo
const bodyText = { // justified and hyphenated (en-us) by default
  // Copy-fitted to the 89.5 mm column: at 9.5 pt '“Have some wine,” the March Hare said in
  // an encouraging tone.' fits one line. At 10.4 pt every measure from 77 to 93 mm left
  // it a short last line: 'couraging tone.', 'aging tone.', 'ing tone.' or 'tone.'.
  fontFamily: TEXT, fontSize: pt(9.5), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(4),
  // The chapter's first paragraph is set flush, though the fences of glosses a and b stand
  // between the heading and it: a box that leaves the flow is not the block after a heading.
  indentAfterHeading: false,
  // Unna's space is narrow, 0.22 em. At the default 0.6 the line breaker set 'Alice felt
  // dreadfully puzzled. The Hatter’s remark seemed to have' with its spaces at 0.69.
  minWordSpacing: 0.75,
  // The dictionary divides the heroine's name, 'said Al-' | 'ice indignantly' on page 81.
  // The list keeps it whole wherever it stands; every other word is divided as before.
  hyphenation: { exceptions: ['Alice'] } };

Un riquadro con span="side" sul suo blocco :::callout lascia il flusso e si colloca nel canale all’altezza del blocco che lo segue, quindi ogni glossa va aperta subito prima del paragrafo che porta la sua lettera, come qui sotto. Le glosse a e b vanno accanto al primo paragrafo, perciò i loro blocchi stanno tra il titolo e quel paragrafo. Un riquadro che lascia il flusso non conta come blocco dopo il titolo, così indentAfterHeading: false compone comunque quel paragrafo senza rientro. Il corpo è adattato alla colonna: a 9,5 pt il paragrafo che si apre con «Have some wine,» sta in una riga di 89,5 mm, mentre a 10,4 pt ogni giustezza tra 77 e 93 mm gli lasciava una riga finale troppo corta come «ing tone.» o «tone.». Lo spazio tra parole di Unna è stretto, 0,22 em, e con il minWordSpacing predefinito di 0,6 una riga di pagina 83 aveva gli spazi compressi a 0,69 di quella larghezza; minWordSpacing: 0.75 esclude quella riga. A pagina 81 il dizionario divideva il nome dell’eroina, «Al-» e «ice», perciò quel paragrafo è racchiuso in :::paragraphs{style="whole"}, uno stile con hyphenation: false; la sua unica divisione è un trattino morbido (U+00AD) digitato in «together».

:::callout{type="context" span="side" title="c · Hair"}
Tenniel drew Alice with long, loose hair. …
:::
 
“Your hair wants cutting,”:chip[^c^]{style="context"} said the Hatter. …

#4 · Stampa ogni lettera nel colore della sua glossa

script.js · righe 65–71nel codice completo
// A letter in the text is a chip named after its gloss's type, :chip[^a^]{style="note"}:
// the Markdown has no mark for coloured text, and a chip style sets a colour. With no
// fill, outline, padding or gap the chip adds no width, so the lines break as they would
// with a plain ^a^.
const letter = (id, hue) => ({ id, backgroundEnabled: false, borderWidth: pt(0),
  paddingX: pt(0), gap: pt(0), color: col(hue), bold: true });
const chipStyles = [letter('note', 'lawn'), letter('context', 'jam')];

Il Markdown non ha un segno per il testo colorato, ma uno stile di chip imposta un colore del testo, quindi ogni lettera è un chip il cui stile ha lo stesso nome del tipo della sua glossa. :chip[^c^]{style="context"} stampa una c rossa, il rosso del filetto e del titolo della glossa c. Senza riempimento, contorno, padding né distanza il chip non aggiunge larghezza, e ogni riga va a capo dove andava con un semplice ^c^.

#5 · Apri il capitolo sotto la tavola del tè

script.js · righe 108–155nel codice completo
const HEADPIECE = 72; // mm: the depth of the painting at the head of the page
const HEADNOTE = { size: 9.8, lead: LEAD }; // pt: the headnote keeps the body's leading
const CAPS = { text: 0.597, initial: 0.56 }; // cap heights, em: Unna and Rozha One
// The initial's size: its capital runs from the first line's cap height down to the
// last baseline it spans. At the default size it stops short of the first line's capitals.
const dropSize = (lines) => pt(((lines - 1) * HEADNOTE.lead + CAPS.text * HEADNOTE.size)
  / CAPS.initial);
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' },
  offset: { y: mm(y) }, size: { width } });
// The opener reserves the height of its lowest element, rounded up to the grid, and the
// heading's default bottom margin adds one blank line: with a four-line headnote the
// text starts on line 23. The kicker, the title and the headnote hang below the painting.
const opener = { enabled: true, slot: { elements: [
  // An image element draws a resource without number or caption. It hangs from the
  // trim's corner, over the top margin and both side margins.
  { kind: 'image', id: 'headpiece', resourceId: 'tea-table',
    placement: { anchor: { to: 'page', edge: 'top-left' },
      size: { width: mm(TRIM_W) } } },
  // The numeral comes from the heading line,
  // # A Mad Tea-Party {num="VII" headnote="…" …}: the excerpt stands alone, so
  // {chapterNumber} would count its one heading and print 1.
  { kind: 'text', id: 'kicker', content: 'Chapter {attr.num}', fontFamily: LABEL,
    fontWeight: 600, fontSize: pt(9), letterSpacing: pt(2), textTransform: 'uppercase',
    color: col('jam'), align: 'left',
    placement: { anchor: { to: 'container', edge: 'top-left' },
      offset: { y: mm(HEADPIECE - TOP + 8) } } }, // 8 mm under the painting's foot
  { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY,
    fontSize: pt(40), lineHeight: 1.05, color: col('ink'), align: 'left', // wraps when longer
    placement: below('kicker', 2, 'fill') },
  // The drop cap opens the headnote, the editor's voice, italic and ragged; Carroll's
  // text opens in the flow, justified.
  { kind: 'text', id: 'headnote', content: '{attr.headnote}', fontFamily: TEXT,
    italic: true, fontSize: pt(HEADNOTE.size),
    lineHeight: pt(HEADNOTE.lead), color: col('ink'),
    align: 'left',
    dropCap: { lines: 2, fontFamily: DISPLAY, fontSize: dropSize(2), color: col('lawn'),
      gap: mm(1.5) },
    placement: below('title', 5, mm(TEXT_W)) },
  // The note on this edition stands in the channel, level with the headnote's top.
  { kind: 'text', id: 'edition-label', content: 'This edition', fontFamily: LABEL,
    fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(0.3), color: col('muted'),
    align: 'left', placement: { anchor: { to: '#headnote', edge: 'right-of' },
      offset: { x: layout.gutterWidth }, size: { width: mm(SIDE_W) } } },
  { kind: 'text', id: 'edition', content: '{attr.source}', fontFamily: TEXT,
    fontSize: pt(7.5), lineHeight: pt(10), color: col('muted'), align: 'left',
    placement: { anchor: { to: '#edition-label', edge: 'below' }, offset: { y: mm(0.8) },
      size: { width: mm(SIDE_W) } } },
] } };

Lo spazio grafico del titolo occupa tutta la larghezza della pagina perché il dipinto possa arrivare al rifilo. L’occhiello, il titolo e il cappello pendono sotto il dipinto, e l’apertura riserva lo spazio fino al più basso di essi, arrotondato alla griglia; il margine inferiore predefinito del titolo aggiunge una riga bianca. Con un cappello di quattro righe il testo comincia alla riga 23, e una quinta riga lo sposterebbe alla 24. Il numerale, il cappello e la nota su questa edizione vengono dagli attributi del titolo, {num="VII" headnote="…" source="…"}. Il numerale viene da lì perché {chapterNumber} stamperebbe 1 in un estratto che è un documento a sé. Il capolettera apre il cappello del curatore, composto in corsivo a bandiera, e il testo di Carroll comincia giustificato nel flusso sottostante. dropSize() dimensiona l’iniziale in modo che la sua sommità si allinei con le maiuscole della prima riga del cappello.

#6 · Colora ogni glossa secondo ciò che spiega

script.js · righe 16–30nel codice completo
const palette = {
  ink: '#1f1b1a', // text: a warm near-black
  lawn: '#1e6f6b', // glosses on words and jokes, the drop cap
  jam: '#b23a48', // glosses on history, the kicker
  muted: '#6e645b', // running heads, the note on this edition
  paper: '#f8f3e6', // the page, and the tablecloth
};
// col(id): a colour linked to its palette entry, with the entry's hex beside the id.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const entry = (id, hex, name = id) => ({ id, name, value: { hex, model: 'hex' } });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => entry(id, hex)),
  // The engine's defaults link to 'main-color': aimed at the green, nothing prints blue.
  entry('main-color', palette.lawn, 'lawn (defaults)'),
];

Il verde prato contrassegna le glosse su parole, modi di dire e giochi di parole, il rosso marmellata le persone, le date e la storia del testo. Un nuovo valore per jam nello stesso oggetto palette ricolora in un’unica modifica le glosse sulla storia, le loro lettere e l’occhiello; l’acquerello sopra di loro è stato scelto per lo stesso verde prato e lo stesso rosso marmellata.

La ricetta completa

Sandbox
// ═══ Postext Cookbook · Nº 032 · Annotated classic with margin glosses ════════════
// https://postext.dev/en/cookbook/annotated-classic-glosses
// Code: MIT · Text: Lewis Carroll (PD), glosses (CC BY 4.0) · Headpiece: diffusion models
// Fonts: Unna, Rozha One, Cormorant SC (SIL OFL 1.1) · Needs postext ≥ 1.28.0
// The mad tea-party as an annotated edition: the text keeps to one column, and its glosses stand
// in the outer margin beside the lines they explain, changing sides with the spread.
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en')
const RECIPE = 'annotated-classic-glosses';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a lawn green and a jam red on cream paper
const palette = {
  ink: '#1f1b1a', // text: a warm near-black
  lawn: '#1e6f6b', // glosses on words and jokes, the drop cap
  jam: '#b23a48', // glosses on history, the kicker
  muted: '#6e645b', // running heads, the note on this edition
  paper: '#f8f3e6', // the page, and the tablecloth
};
// col(id): a colour linked to its palette entry, with the entry's hex beside the id.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const entry = (id, hex, name = id) => ({ id, name, value: { hex, model: 'hex' } });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => entry(id, hex)),
  // The engine's defaults link to 'main-color': aimed at the green, nothing prints blue.
  entry('main-color', palette.lawn, 'lawn (defaults)'),
];
// #endregion
const [TEXT, DISPLAY, LABEL] = ['Unna', 'Rozha One', 'Cormorant SC'];
const LEAD = 14; // body leading in pt: the grid of every page

// #region answer: a float-only channel at the fore-edge, and two gloss styles for it
const layout = {
  layoutType: 'oneAndHalf', // one text column and a narrower side column
  sideColumnPercent: 26, // of the 127 mm between the margins: a 33 mm channel
  sideColumnRole: 'floats', // no text runs in it: it holds side boxes and figures
  sideColumnSide: 'outer', // at the fore-edge, which mirrored margins move page by page
  gutterWidth: mm(4.5), // the text column keeps 127 − 33 − 4.5 = 89.5 mm
};
// A gloss is :::callout{type="note" span="side" title="a · …"} in the Markdown.
// span="side" moves it out of the flow into the channel, level with the block after
// the fence, so the fence goes just before the paragraph that carries its letter.
// Glosses never float. One that meets the gloss above stacks under it. One that would
// run past the column's foot slides up, as far as the gloss above allows, until its
// foot sits on the foot; if it still does not fit, it waits for the next page
// (gotcha: side-box-starts-at-fence).
const gloss = (id, hue) => ({ id,
  backgroundEnabled: false, // one device: a hairline over the gloss, in its colour
  stripe: { enabled: true, side: 'top', width: pt(0.5), color: col(hue) },
  padding: { top: mm(1.5), right: pt(0), bottom: pt(0), left: pt(0) },
  // Cormorant SC draws lower case as small capitals: the title needs no textTransform.
  titleStyle: { fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5),
    letterSpacing: pt(0.3), color: col(hue), gap: mm(0.8) },
  // Face and colours come from bodyText. A box body also inherits its 4 mm first-line
  // indent, which a gloss sets back to 0.
  body: { fontSize: pt(7.8), lineHeight: pt(10), textAlign: 'left',
    firstLineIndent: pt(0) } });
const calloutStyles = [gloss('note', 'lawn'), gloss('context', 'jam')]; // words, history
// #endregion

// #region letters: the letter that calls each gloss, in the gloss's colour
// A letter in the text is a chip named after its gloss's type, :chip[^a^]{style="note"}:
// the Markdown has no mark for coloured text, and a chip style sets a colour. With no
// fill, outline, padding or gap the chip adds no width, so the lines break as they would
// with a plain ^a^.
const letter = (id, hue) => ({ id, backgroundEnabled: false, borderWidth: pt(0),
  paddingX: pt(0), gap: pt(0), color: col(hue), bold: true });
const chipStyles = [letter('note', 'lawn'), letter('context', 'jam')];
// #endregion

// #region page: a trade page with mirrored margins, so the channel is always at the fore-edge
const PT = 25.4 / 72; // mm in a point
const [TRIM_W, TRIM_H] = [156, 234]; // mm
const [TOP, BOTTOM, INNER, OUTER] = [22, 22, 15, 14]; // mm: 38 lines of text
const page = { width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
  backgroundColor: col('paper'),
  // left is a recto's inner margin; mirror swaps the sides on a verso, and 'outer' moves
  // the channel with them: right on a recto, left on a verso.
  margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
    mirror: true } };
const CONTENT_W = TRIM_W - INNER - OUTER; // 127 mm
const SIDE_W = CONTENT_W * layout.sideColumnPercent / 100; // 33 mm: the glosses' measure
const TEXT_W = CONTENT_W - SIDE_W - layout.gutterWidth.value;
// #endregion

// #region text: book texture, and a first paragraph set flush
const bodyText = { // justified and hyphenated (en-us) by default
  // Copy-fitted to the 89.5 mm column: at 9.5 pt '“Have some wine,” the March Hare said in
  // an encouraging tone.' fits one line. At 10.4 pt every measure from 77 to 93 mm left
  // it a short last line: 'couraging tone.', 'aging tone.', 'ing tone.' or 'tone.'.
  fontFamily: TEXT, fontSize: pt(9.5), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(4),
  // The chapter's first paragraph is set flush, though the fences of glosses a and b stand
  // between the heading and it: a box that leaves the flow is not the block after a heading.
  indentAfterHeading: false,
  // Unna's space is narrow, 0.22 em. At the default 0.6 the line breaker set 'Alice felt
  // dreadfully puzzled. The Hatter’s remark seemed to have' with its spaces at 0.69.
  minWordSpacing: 0.75,
  // The dictionary divides the heroine's name, 'said Al-' | 'ice indignantly' on page 81.
  // The list keeps it whole wherever it stands; every other word is divided as before.
  hyphenation: { exceptions: ['Alice'] } };
// #endregion

// #region opener: a headpiece, the chapter's numeral and title, a headnote with a drop cap
const HEADPIECE = 72; // mm: the depth of the painting at the head of the page
const HEADNOTE = { size: 9.8, lead: LEAD }; // pt: the headnote keeps the body's leading
const CAPS = { text: 0.597, initial: 0.56 }; // cap heights, em: Unna and Rozha One
// The initial's size: its capital runs from the first line's cap height down to the
// last baseline it spans. At the default size it stops short of the first line's capitals.
const dropSize = (lines) => pt(((lines - 1) * HEADNOTE.lead + CAPS.text * HEADNOTE.size)
  / CAPS.initial);
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' },
  offset: { y: mm(y) }, size: { width } });
// The opener reserves the height of its lowest element, rounded up to the grid, and the
// heading's default bottom margin adds one blank line: with a four-line headnote the
// text starts on line 23. The kicker, the title and the headnote hang below the painting.
const opener = { enabled: true, slot: { elements: [
  // An image element draws a resource without number or caption. It hangs from the
  // trim's corner, over the top margin and both side margins.
  { kind: 'image', id: 'headpiece', resourceId: 'tea-table',
    placement: { anchor: { to: 'page', edge: 'top-left' },
      size: { width: mm(TRIM_W) } } },
  // The numeral comes from the heading line,
  // # A Mad Tea-Party {num="VII" headnote="…" …}: the excerpt stands alone, so
  // {chapterNumber} would count its one heading and print 1.
  { kind: 'text', id: 'kicker', content: 'Chapter {attr.num}', fontFamily: LABEL,
    fontWeight: 600, fontSize: pt(9), letterSpacing: pt(2), textTransform: 'uppercase',
    color: col('jam'), align: 'left',
    placement: { anchor: { to: 'container', edge: 'top-left' },
      offset: { y: mm(HEADPIECE - TOP + 8) } } }, // 8 mm under the painting's foot
  { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY,
    fontSize: pt(40), lineHeight: 1.05, color: col('ink'), align: 'left', // wraps when longer
    placement: below('kicker', 2, 'fill') },
  // The drop cap opens the headnote, the editor's voice, italic and ragged; Carroll's
  // text opens in the flow, justified.
  { kind: 'text', id: 'headnote', content: '{attr.headnote}', fontFamily: TEXT,
    italic: true, fontSize: pt(HEADNOTE.size),
    lineHeight: pt(HEADNOTE.lead), color: col('ink'),
    align: 'left',
    dropCap: { lines: 2, fontFamily: DISPLAY, fontSize: dropSize(2), color: col('lawn'),
      gap: mm(1.5) },
    placement: below('title', 5, mm(TEXT_W)) },
  // The note on this edition stands in the channel, level with the headnote's top.
  { kind: 'text', id: 'edition-label', content: 'This edition', fontFamily: LABEL,
    fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(0.3), color: col('muted'),
    align: 'left', placement: { anchor: { to: '#headnote', edge: 'right-of' },
      offset: { x: layout.gutterWidth }, size: { width: mm(SIDE_W) } } },
  { kind: 'text', id: 'edition', content: '{attr.source}', fontFamily: TEXT,
    fontSize: pt(7.5), lineHeight: pt(10), color: col('muted'), align: 'left',
    placement: { anchor: { to: '#edition-label', edge: 'below' }, offset: { y: mm(0.8) },
      size: { width: mm(SIDE_W) } } },
] } };
// #endregion

// The running heads: the book on the verso, the chapter on the recto, folios at the fore-edge.
const HEAD = 14; // mm from the top trim to the heads' baseline
const INSET = 8; // mm from the folio's outer edge to the running head's
const DROP = 13; // mm from the bottom trim up to the foot of the drop folio's box
// A design text's first baseline sits 0.96 em below the top of its box: the default
// lineHeight, 1.2, times 0.8, where the baseline falls in the line box.
const BASELINE = 1.2 * 0.8;
const baseline = (size) => mm(HEAD - BASELINE * size * PT);
const head = (id, parity, content, x, extra = {}) => ({ kind: 'text', id, parity, content,
  pages: 'body', fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'), ...extra,
  placement: { anchor: { to: 'page', edge: parity === 'even' ? 'top-left' : 'top-right' },
    offset: { x: mm(x), y: baseline(extra.fontSize?.value ?? 8.5) } } });
const folio = { fontFamily: TEXT, fontWeight: 700, fontSize: pt(9), letterSpacing: pt(0),
  color: col('ink') };
const header = { elements: [
  head('verso-folio', 'even', '{pageNumber}', OUTER, folio),
  head('verso-title', 'even', '{title}', OUTER + INSET),
  head('recto-title', 'odd', '{chapterTitle}', -(OUTER + INSET)),
  head('recto-folio', 'odd', '{pageNumber}', -OUTER, folio),
] };
// The opener, a recto, carries a drop folio at the foot of its channel instead.
const footer = { elements: [{ ...head('drop-folio', 'odd', '{pageNumber}', -OUTER, folio),
  pages: 'opener', placement: { anchor: { to: 'page', edge: 'bottom-right' },
    offset: { x: mm(-OUTER), y: mm(-DROP) } } }] };

const config = () => ({
  locale: 'en-us', // the chapter is in English: its hyphenation patterns, and the bundle's language
  colorPalette, page, layout, bodyText, calloutStyles, chipStyles, header,
  footer,
  headings: {
    fontFamily: DISPLAY, fontWeight: 400, color: col('ink'), // Rozha One has one weight
    levels: [
      // 'odd' puts the opener on a recto; span 'page' lets its design cross the channel.
      { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
        advancedDesign: opener },
    ],
  },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Esempio Markdown · 119 righe · content.en.mdtitle: "Alice’s Adventures in Wonderland" author: "Lewis Carroll" --- # A Mad Tea-Party {num="VII" headnote="The tea-party is not in the book Carroll wrote out by hand for Alice Liddell in 1864; he added it, with the Cheshire Cat, for the book of 1865. Letters in the text point to the glosses beside it: green for words and jokes, red for history." source="Text after Project Gutenberg eBook 11; notes CC BY 4.0. Set in Unna, Rozha One and Cormorant SC (SIL OFL)."} :::callout{type="note" span="side" title="a · The March Hare"} ‘Mad as a March hare’ is an old saying. Hares are shy, but in early spring they race about the fields and rear up on their hind legs to box. ::: :::callout{type="context" span="side" title="b · The Hatter"} ‘Mad as a hatter’ was a saying before Carroll used it. Hatters made felt from fur treated with mercury, and the fumes gave many of them tremors and fits of shyness and temper. Carroll never calls him the Mad Hatter. ::: There was a table set out under a tree in front of the house, and the March Hare:chip[^a^]{style="note"} and the Hatter:chip[^b^]{style="context"} were having tea at it: a Dormouse was sitting between them, fast asleep, and the other two were using it as a cushion, resting their elbows on it, and talking over its head. “Very uncomfortable for the Dormouse,” thought Alice; “only, as it’s asleep, I suppose it doesn’t mind.” The table was a large one, but the three were all crowded together at one corner of it: “No room! No room!” they cried out when they saw Alice coming. “There’s *plenty* of room!” said Alice indignantly, and she sat down in a large arm-chair at one end of the table. “Have some wine,” the March Hare said in an encouraging tone. Alice looked all round the table, but there was nothing on it but tea. “I don’t see any wine,” she remarked. “There isn’t any,” said the March Hare. “Then it wasn’t very civil of you to offer it,” said Alice angrily. “It wasn’t very civil of you to sit down without being invited,” said the March Hare. “I didn’t know it was *your* table,” said Alice; “it’s laid for a great many more than three.” :::callout{type="context" span="side" title="c · Hair"} Tenniel drew Alice with long, loose hair. Alice Liddell, in Carroll’s photographs of her, wears hers short and dark, with a fringe. ::: “Your hair wants cutting,”:chip[^c^]{style="context"} said the Hatter. He had been looking at Alice for some time with great curiosity, and this was his first speech. “You should learn not to make personal remarks,” Alice said with some severity; “it’s very rude.” :::callout{type="context" span="side" title="d · The riddle"} Carroll made up the riddle without an answer. So many readers asked for one that in a preface of 1896 he offered this: ‘Because it can produce a few notes, tho they are very flat; and it is nevar put with the wrong end in front!’ Later printings changed *nevar*, raven spelt backwards, to *never*, and the joke was lost. ::: The Hatter opened his eyes very wide on hearing this; but all he *said* was, “Why is a raven like a writing-desk?”:chip[^d^]{style="context"} “Come, we shall have some fun now!” thought Alice. “I’m glad they’ve begun asking riddles.—I believe I can guess that,” she added aloud. “Do you mean that you think you can find out the answer to it?” said the March Hare. “Exactly so,” said Alice. “Then you should say what you mean,” the March Hare went on. “I do,” Alice hastily replied; “at least—at least I mean what I say—that’s the same thing, you know.” :::callout{type="note" span="side" title="e · I see what I eat"} The Hatter has the logic right. Turn a statement round and you get its converse, which can be false when the statement is true. Carroll, as Charles Dodgson, taught mathematics at Christ Church, Oxford, and wrote two books on logic. ::: “Not the same thing a bit!” said the Hatter. “You might just as well say that ‘I see what I eat’ is the same thing as ‘I eat what I see’!”:chip[^e^]{style="note"} “You might just as well say,” added the March Hare, “that ‘I like what I get’ is the same thing as ‘I get what I like’!” “You might just as well say,” added the Dormouse, who seemed to be talking in his sleep, “that ‘I breathe when I sleep’ is the same thing as ‘I sleep when I breathe’!” “It *is* the same thing with you,” said the Hatter, and here the conversation dropped, and the party sat silent for a minute, while Alice thought over all she could remember about ravens and writing-desks, which wasn’t much. The Hatter was the first to break the silence. “What day of the month is it?” he said, turning to Alice: he had taken his watch out of his pocket, and was looking at it uneasily, shaking it every now and then, and holding it to his ear. :::callout{type="context" span="side" title="f · The fourth"} Of May: Alice Liddell was born on 4 May 1852. ::: Alice considered a little, and then said “The fourth.”:chip[^f^]{style="context"} “Two days wrong!” sighed the Hatter. “I told you butter wouldn’t suit the works!” he added looking angrily at the March Hare. :::callout{type="note" span="side" title="g · The best butter"} Grocers sold butter by grade, and ‘best’ fetched the highest price. The March Hare defends its quality, which was never the trouble. ::: “It was the *best* butter,”:chip[^g^]{style="note"} the March Hare meekly replied. “Yes, but some crumbs must have got in as well,” the Hatter grumbled: “you shouldn’t have put it in with the bread-knife.” The March Hare took the watch and looked at it gloomily: then he dipped it into his cup of tea, and looked at it again: but he could think of nothing better to say than his first remark, “It was the *best* butter, you know.” Alice had been looking over his shoulder with some curiosity. “What a funny watch!” she remarked. “It tells the day of the month, and doesn’t tell what o’clock it is!” “Why should it?” muttered the Hatter. “Does *your* watch tell you what year it is?” “Of course not,” Alice replied very readily: “but that’s because it stays the same year for such a long time together.” “Which is just the case with *mine*,” said the Hatter. Alice felt dreadfully puzzled. The Hatter’s remark seemed to have no sort of meaning in it, and yet it was certainly English. “I don’t quite understand you,” she said, as politely as she could. “The Dormouse is asleep again,” said the Hatter, and he poured a little hot tea upon its nose. :::callout{type="note" span="side" title="h · The Dormouse"} Hazel dormice sleep through the day and hibernate for half the year, from autumn to spring. The name is often traced to the French *dormir*, to sleep. ::: The Dormouse:chip[^h^]{style="note"} shook its head impatiently, and said, without opening its eyes, “Of course, of course; just what I was going to remark myself.” “Have you guessed the riddle yet?” the Hatter said, turning to Alice again. “No, I give it up,” Alice replied: “what’s the answer?” “I haven’t the slightest idea,” said the Hatter. “Nor I,” said the March Hare. Alice sighed wearily. “I think you might do something better with the time,” she said, “than waste it in asking riddles that have no answers.” “If you knew Time as well as I do,” said the Hatter, “you wouldn’t talk about wasting *it*. It’s *him*.” “I don’t know what you mean,” said Alice. “Of course you don’t!” the Hatter said, tossing his head contemptuously. “I dare say you never even spoke to Time!”
`; // content.<lang>.md, inlined by the Cookbook // The headpiece is a resource that only the opener's design draws: never cited, never placed. // It is a watercolour, a JPEG in assets/ cut to the trim's 156 × 72 mm, declared at its pixels. const resources = [{ id: 'tea-table', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'tea-table-1560.jpg', format: 'jpeg', width: 1560, height: 720 }, altText: 'The tea-table under a tree, seen from above: a long white cloth laid with cups, a red ' + 'teapot, bread and butter and a watch on its chain; the Dormouse asleep in a teapot at one ' + 'end and a red arm-chair at the other.' }]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces, loaded before the build Unna: ['400', '400i', '700'], // 700: the folios and the gloss letters 'Rozha One': ['400'], 'Cormorant SC': ['600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadImage('tea-table-1560.jpg', asset('tea-table-1560.jpg')); const continuation = { pageNumbering: { startAt: 81 } }; // chapter VII of a book: an odd folio const doc = await buildDocumentWithFonts({ markdown, resources, continuation }, config(), kitFonts(FONTS)); showPages(doc, { title: 'Annotated classic with margin glosses' });
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

#Tieni le glosse a destra di ogni pagina

Per un’edizione letta una pagina alla volta sullo schermo, disattiva la specchiatura: 'outer' indica allora il bordo destro di ogni pagina, e le testatine restano al loro posto, perché sono ancorate al rifilo.

-    mirror: true } };
+    mirror: false } };

#Manda le note lunghe al piede della pagina

Scrivi una nota troppo lunga per il margine come nota a piè di pagina, [^1] nel testo e la sua definizione [^1]: sotto il paragrafo: Postext la compone al piede della colonna di testo, larga 89,5 mm sotto un filetto corto, e le glosse conservano il canale. Note al piede della colonna che le cita dà lo stile a quelle note.

Errori frequenti

Errore frequente

Un riquadro laterale parte all’altezza del blocco che segue il suo delimitatore

In postext 1.4.1 un riquadro span: 'side' si colloca nella colonna laterale all’altezza a cui è arrivato il testo nel punto del suo delimitatore, sulla riga successiva della griglia e sotto qualunque riquadro già presente. Metti il delimitatore di una glossa subito prima del paragrafo che spiega: messa dopo, la glossa comincia accanto al paragrafo seguente. Un riquadro che sforerebbe il piede della colonna sale finché il suo piede non coincide con il piede della colonna, per quanto lo permette il riquadro sopra di esso; uno che ancora non ci sta aspetta la colonna laterale della pagina successiva. Note a margine →

  • Le glosse si impilano nell’ordine del Markdown, una sotto l’altra, quindi una glossa lunga spinge in basso la successiva. A pagina 81 entrambe le lettere cadono nella seconda riga del primo paragrafo: la glossa a comincia all’altezza del paragrafo e la glossa b sotto di essa, cinque righe sotto la sua lettera. Dove le lettere sono vicine, tieni le glosse brevi.

Crediti

Testo
  • Alice’s Adventures in Wonderland (1865), the opening of chapter VII, “A Mad Tea-Party” · Lewis Carroll · pubblico dominio
  • The headnote, the eight glosses and the note on this edition · Ignacio Ferro · CC BY 4.0
Immagini
  • The tea-table seen from above, a watercolour · Generated With Diffusion Models · originale
Font
Unna (SIL OFL 1.1) · Rozha One (SIL OFL 1.1) · Cormorant SC (SIL OFL 1.1)
Sandbox