In breve
Un piccolo libro di quattro poesie sulla natura di Gerard Manley Hopkins. Mostra come tenere ogni verso a sé, con i rientri originali del poeta e una riga vuota tra le strofe.
Che cosa comporrai
Una plaquette di quattro poesie sulla natura di Gerard Manley Hopkins, 140 × 216 mm, nel testo della prima edizione del 1918. Dopo l’occhiello, un fotogramma di felce e sorbo come tavola di antiporta sta di fronte a un frontespizio composto in Italiana. L’indice elenca ogni poesia con il numero romano e il titolo in corsivo, con puntini spaziati fino al numero di pagina. Ogni poesia apre una pagina sotto un numero romano verde salvia e il suo titolo in corsivo. Ogni poesia è un blocco :::verse, rientrato come nel 1918: i versi di Pied Beauty rientrano secondo la rima, e i versi lunghi di The Windhover vanno a capo e sporgono di 4 em, il doppio dei suoi rientri. Le strofe sono separate da una riga, e nessuna parola si spezza tranne dove l’ha spezzata Hopkins. La data va sotto ogni poesia, allineata a destra, e un rametto di sorbo segue l’ultima. Le pagine delle poesie portano un folio centrato al piede.
Questa ricetta risponde a
- Come compongo poesia: una riga per verso, spazi tra le strofe, rientri sporgenti per i versi che vanno a capo, niente sillabazione?
- Come aggiungo spazio verticale tra due blocchi, se le righe vuote non hanno effetto?
- Come aggiungo un indice che si aggiorna da solo (puntini di guida, numeri di pagina, autori, righe di parte)?
- Come compongo un’epigrafe, una dedica, una firma o una citazione in evidenza con una grande virgoletta?
- Come compongo illustrazioni non numerate: fregi, vignette, loghi?
La risposta breve
// A poem is one :::verse block written as it is printed: a line of verse a line, a blank line
// between stanzas (one line of the grid), and spaces before an indented line, two to an em:
// :::verse{style="verse" align=start}
// The world is charged with the grandeur of God.
// It will flame out, like shining from shook foil;
//
// And for all this, nature is never spent;
// :::
// align=start sets the poem flush with the margin (the default centres it on its longest line).
const verse = {
id: 'verse',
hangingIndent: em(2), // a turnover hangs 2 em past its line's start: 4 em under a 2 em indent
// Verse keeps its words whole, even where bodyText.hyphenation.ragged is switched on.
hyphenation: false,
};
// Hook-up: paragraphStyles: [verse, …]; the Markdown goes to buildDocument as it is written.
Ingredienti
- Funzionalità
- Versi riga per rigaStili di paragrafoIndiceSpazio verticale esplicitoRientri, allineamento e spaziatura dei paragrafiEscape e caratteri letteraliTitoli numeratiAperture progettateStili di titoloCapitoli non numeratiAttributi dei titoliImmagini nei progetti di paginaCopertine, frontespizi e colophonMetadati del documentoTestatine per sezioneTestatine e folioTipi di risorsa personalizzatiFigure proprio quiTavolozza di colori semanticaTestatine secondo il ruolo della pagina
- Tipo
- Sorts Mill Goudy, Italiana, Marcellus SC (SIL OFL 1.1)
- Risorse
plate-1120.jpg- The frontispiece photogram of fern and rowan (Generated With Diffusion Models, originale)
- The rowan ornament, drawn in code in the page’s palette (Ignacio Ferro, CC BY 4.0)
Preparazione
#1 · Una poesia è un blocco :::verse
Un blocco :::verse mantiene ogni riga del Markdown come un verso, quindi la poesia si scrive come si stampa (:::verse). I suoi spazi iniziali sono i suoi rientri, due per em, così i versi che rimano fra loro cominciano alla stessa distanza dal margine, come nel 1918. Una riga vuota tra le strofe lascia una riga della griglia, e cade in testa alla pagina. Un verso troppo lungo per la giustezza va a capo su uno spazio tra parole e sporge del hangingIndent dello stile dei versi, contato dall’inizio del verso stesso: 2 em oltre un verso non rientrato, 4 em sotto un verso rientrato di 2 em, così un verso a capo non si legge mai come un verso a sé. align=start allinea ogni poesia al margine; senza, una poesia si centra sul suo verso più lungo. I versi non si sillabano, e un verso non si separa mai dalla sua continuazione a un’interruzione di pagina.

#2 · Anche le righe attorno a una poesia hanno i loro stili
const lineStyles = [
{ id: 'dedication', fontSize: pt(9.5), marginBottom: pt(LEAD) }, // under The Windhover
{ id: 'date', fontFamily: 'Marcellus SC', fontSize: pt(8), color: col('muted'),
textAlign: 'right', marginTop: pt(LEAD) }, // a line of space above, flush right
];
La dedica sopra The Windhover e il luogo e la data sotto ogni poesia non sono versi, quindi ciascuno è un contenitore :::paragraphs di un solo paragrafo, in uno stile suo. Nessuno dei due stili imposta un’interlinea, e i loro margini sono di una riga (15 pt), così i versi restano sulla griglia del testo. La data si compone una riga sotto la poesia, allineata a destra, in Marcellus SC da 8 pt, il carattere dei folio.
#3 · Ogni poesia apre una pagina sotto il suo numero
// '{1:I}' numbers the poems I to IV: {number} prints the numeral here, :::toc in the contents.
// The head is HEAD_LINES grid lines (minHeight, no bottom margin), so each poem starts on the same
// line; numeral and title fill 4, or 5 if the title wraps.
const HEAD_LINES = 6;
const poemHead = { enabled: true, minHeight: pt(LEAD * HEAD_LINES), slot: { elements: [
{ kind: 'text', id: 'numeral', content: '{number}', fontFamily: 'Italiana', fontSize: pt(22),
color: col('sage'), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Sorts Mill Goudy',
italic: true, fontSize: pt(17), color: col('ink'), align: 'left',
placement: { anchor: { to: '#numeral', edge: 'below' }, offset: { y: mm(2) },
size: { width: 'fill' } } },
] } };
// Parity 'any': the next page, recto or verso, with no blank page between two poems.
const poems = { level: 1, numberingTemplate: '{1:I}', advancedDesign: poemHead,
marginBottom: pt(0), breakBefore: { enabled: true, parity: 'any' } };
Invece del suo testo, il titolo stampa un progetto grafico nella colonna. In quel progetto, {number} è il conteggio come lo formatta numberingTemplate: '{1:I}', da I a IV, e {titleText} è il titolo (progetto grafico a tutta larghezza e avanzato). Il livello non ha margine inferiore, e minHeight rende ogni titolo alto sei righe di griglia (31,75 mm). Il numero romano e un titolo di una riga ne riempiono quattro, e un titolo che va a capo ne riempie cinque, così ogni poesia comincia sulla stessa riga della sua pagina. A pagina 6 quella riga accoglie la dedica. La parità 'any' passa alla pagina successiva, recto o verso, quindi non ci sono pagine bianche tra le poesie.
#4 · Le pagine preliminari sono fatte di titoli
// Headings too, each on a page of its own, with no number, contents entry or folio.
// A style restates the break, or inherits the poems' (gotcha: style-inherits-break).
const leaf = { numbered: false, toc: false, span: 'page', footer: { elements: [] },
breakBefore: { enabled: true, parity: 'any' } };
const onPage = (y, width) => ({ anchor: { to: 'page', edge: 'top' }, offset: { y: mm(y) },
...(width && { size: { width: mm(width) } }) }); // centred, y mm below the trim
const face = (id, content, font, size, y, extra = {}) => ({ kind: 'text', id, content,
fontFamily: font, fontSize: pt(size), color: col('ink'), align: 'center',
placement: onPage(y), ...extra });
const image = (id, placement) => ({ kind: 'image', id, resourceId: id, placement });
const design = (...elements) => ({ enabled: true, slot: { elements } });
const front = [
{ id: 'half-title', ...leaf,
advancedDesign: design(face('title', '{titleText}', 'Italiana', 20, 60)) },
{ id: 'plate', ...leaf, advancedDesign: design( // span 'page': a column's foot would cut it
image('plate', { anchor: { to: 'bleed', edge: 'top-left' }, size: { width: 'fill' } }),
face('caption', '{attr.caption}', 'Sorts Mill Goudy', 8.5, 203,
{ italic: true, color: col('muted') })) },
{ id: 'title-page', ...leaf, advancedDesign: design(
face('author', '{author}', 'Marcellus SC', 10, 46, // {author}, {title}: the frontmatter
{ letterSpacing: pt(2.4), textTransform: 'uppercase' }),
face('title', '{title}', 'Italiana', 54, 56, { lineHeight: 1 }),
face('subtitle', '{subtitle}', 'Sorts Mill Goudy', 13, 80,
{ italic: true, color: col('sage') }),
image('sprig', onPage(96, 30)),
face('press', 'The Herbarium Press', 'Marcellus SC', 8.5, 182,
{ letterSpacing: pt(1.8), color: col('muted') })) },
{ id: 'contents', ...leaf, span: 'column', advancedDesign: { enabled: false },
fontFamily: 'Italiana', fontSize: pt(24), lineHeight: pt(LEAD * 2), marginBottom: pt(LEAD) },
];
Uno stile di titolo può passare a una nuova pagina e disegnarvi un progetto grafico, quindi l’occhiello, la tavola di antiporta e il frontespizio sono un titolo ciascuno, con uno stile suo. numbered: false li tiene fuori dal conteggio, e la prima poesia è ancora I. toc: false li tiene fuori dall’indice, e un piè di pagina vuoto lascia le loro pagine senza folio (stili di titolo). Gli stili occupano tutta la pagina perché il piede della gabbia taglia un progetto grafico tenuto nella colonna, che lì ritaglierebbe la tavola al vivo e ne perderebbe la didascalia. La didascalia viene dall’attributo caption del titolo; autore, titolo e sottotitolo del frontespizio vengono dal frontmatter.
#5 · L’indice si compila da solo
// The numerals stand on the titles' baseline, in Italiana and sage, at the titles' size.
const contents = {
levels: [{ level: 1, italic: true, marginBottom: pt(LEAD), numberFontFamily: 'Italiana',
numberFontWeight: 400, numberColor: col('sage'), numberWidth: mm(6), numberGap: mm(3) }],
pageNumber: { fontFamily: 'Marcellus SC', fontSize: pt(9), color: col('muted'), width: mm(6) },
leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up
};
:::toc elenca ogni titolo che il suo stile lascia entrare, con il numero romano dallo stesso modello e l’etichetta della pagina su cui cade il titolo. La composizione reimpagina il libro finché quelle etichette di pagina smettono di cambiare (indice). Il titolo della pagina stessa, «Contents», ha uno stile con numbered: false; senza, «Contents» sarebbe la poesia I. Il riempitivo '. ' mette uno spazio dopo ogni punto. I puntini sono allineati a destra, quindi si allineano da una voce all’altra, e si stampano nel carattere e nel colore dei numeri di pagina perché toc.leader non ha un’impostazione di colore.
#6 · Un ornamento è un tipo di risorsa senza etichetta
// No caption prefix and no caption: the sprig prints bare, where ::resource{id="sprig"} puts
// it (double quotes: gotcha resource-double-quotes), a fifth of the measure wide, centred.
const ornament = { id: 'ornament', name: 'Ornament', shortLabel: '', captionPrefix: '',
numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal',
defaultPlacement: { position: 'here', width: 0.2, align: 'center' } };
Un’immagine nel testo è una risorsa, e ogni risorsa ha un tipo. Il tipo ornamento ha un prefisso di didascalia vuoto e il rametto non ha didascalia, quindi sotto non si stampa nessuna etichetta come «Figure 1» (tipi di risorsa). Il defaultPlacement del tipo mette il rametto dove sta ::resource{id="sprig"}, centrato e largo un quinto della giustezza, così il finalino sotto Inversnaid occupa una riga di Markdown. resourceTypes sostituisce l’elenco predefinito, quindi questo libro non ha i tipi figura e tabella. Per tenerli, espandi defaultResourceTypes() davanti all’ornamento.
La ricetta completa
// ═══ Postext Cookbook · Nº 015 · Poems set line by line ═════════════════════════════ // https://postext.dev/en/cookbook/poetry-collection // Code: MIT · Text: G. M. Hopkins, Poems, 1918 (PD) · Plate: diffusion models · Sprig: code // Fonts: Sorts Mill Goudy, Italiana, Marcellus SC (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 = 'poetry-collection'; // ─── 1 · Design ───────────────────────────────────────────────────────────── const palette = { // every colour in the config links to one of these ink: '#26221f', // the text: a warm near-black sage: '#56673f', // the one accent: numerals, the subtitle, the ornament's leaves sepia: '#8c5f3a', // used for the rowan berries only muted: '#746a60', // dates, folios, leaders and the colophon paper: '#fbf8f2', // the page }; // A colour is its hex and the palette entry it follows. 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 accent, so nothing prints blue. { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.sage, model: 'hex' } }, ]; const TRIM_W = 140, TRIM_H = 216; // mm: a poetry trim, tall enough for a sonnet's turnovers const LEAD = 15; // pt: the body's leading, the grid every line of verse sits on // #region answer: verse: a :::verse block keeps each line, its indent, the stanza breaks // A poem is one :::verse block written as it is printed: a line of verse a line, a blank line // between stanzas (one line of the grid), and spaces before an indented line, two to an em: // :::verse{style="verse" align=start} // The world is charged with the grandeur of God. // It will flame out, like shining from shook foil; // // And for all this, nature is never spent; // ::: // align=start sets the poem flush with the margin (the default centres it on its longest line). const verse = { id: 'verse', hangingIndent: em(2), // a turnover hangs 2 em past its line's start: 4 em under a 2 em indent // Verse keeps its words whole, even where bodyText.hyphenation.ragged is switched on. hyphenation: false, }; // Hook-up: paragraphStyles: [verse, …]; the Markdown goes to buildDocument as it is written. // #endregion // #region lines: the lines around a poem: a dedication above it, a place and a date below const lineStyles = [ { id: 'dedication', fontSize: pt(9.5), marginBottom: pt(LEAD) }, // under The Windhover { id: 'date', fontFamily: 'Marcellus SC', fontSize: pt(8), color: col('muted'), textAlign: 'right', marginTop: pt(LEAD) }, // a line of space above, flush right ]; // #endregion // #region poem-head: every poem opens a page under its numeral and its title // '{1:I}' numbers the poems I to IV: {number} prints the numeral here, :::toc in the contents. // The head is HEAD_LINES grid lines (minHeight, no bottom margin), so each poem starts on the same // line; numeral and title fill 4, or 5 if the title wraps. const HEAD_LINES = 6; const poemHead = { enabled: true, minHeight: pt(LEAD * HEAD_LINES), slot: { elements: [ { kind: 'text', id: 'numeral', content: '{number}', fontFamily: 'Italiana', fontSize: pt(22), color: col('sage'), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' } } }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Sorts Mill Goudy', italic: true, fontSize: pt(17), color: col('ink'), align: 'left', placement: { anchor: { to: '#numeral', edge: 'below' }, offset: { y: mm(2) }, size: { width: 'fill' } } }, ] } }; // Parity 'any': the next page, recto or verso, with no blank page between two poems. const poems = { level: 1, numberingTemplate: '{1:I}', advancedDesign: poemHead, marginBottom: pt(0), breakBefore: { enabled: true, parity: 'any' } }; // #endregion // #region front: the half-title, the frontispiece, the title page and the contents // Headings too, each on a page of its own, with no number, contents entry or folio. // A style restates the break, or inherits the poems' (gotcha: style-inherits-break). const leaf = { numbered: false, toc: false, span: 'page', footer: { elements: [] }, breakBefore: { enabled: true, parity: 'any' } }; const onPage = (y, width) => ({ anchor: { to: 'page', edge: 'top' }, offset: { y: mm(y) }, ...(width && { size: { width: mm(width) } }) }); // centred, y mm below the trim const face = (id, content, font, size, y, extra = {}) => ({ kind: 'text', id, content, fontFamily: font, fontSize: pt(size), color: col('ink'), align: 'center', placement: onPage(y), ...extra }); const image = (id, placement) => ({ kind: 'image', id, resourceId: id, placement }); const design = (...elements) => ({ enabled: true, slot: { elements } }); const front = [ { id: 'half-title', ...leaf, advancedDesign: design(face('title', '{titleText}', 'Italiana', 20, 60)) }, { id: 'plate', ...leaf, advancedDesign: design( // span 'page': a column's foot would cut it image('plate', { anchor: { to: 'bleed', edge: 'top-left' }, size: { width: 'fill' } }), face('caption', '{attr.caption}', 'Sorts Mill Goudy', 8.5, 203, { italic: true, color: col('muted') })) }, { id: 'title-page', ...leaf, advancedDesign: design( face('author', '{author}', 'Marcellus SC', 10, 46, // {author}, {title}: the frontmatter { letterSpacing: pt(2.4), textTransform: 'uppercase' }), face('title', '{title}', 'Italiana', 54, 56, { lineHeight: 1 }), face('subtitle', '{subtitle}', 'Sorts Mill Goudy', 13, 80, { italic: true, color: col('sage') }), image('sprig', onPage(96, 30)), face('press', 'The Herbarium Press', 'Marcellus SC', 8.5, 182, { letterSpacing: pt(1.8), color: col('muted') })) }, { id: 'contents', ...leaf, span: 'column', advancedDesign: { enabled: false }, fontFamily: 'Italiana', fontSize: pt(24), lineHeight: pt(LEAD * 2), marginBottom: pt(LEAD) }, ]; // #endregion // #region contents: what :::toc prints for each poem: numeral, italic title, leader, page // The numerals stand on the titles' baseline, in Italiana and sage, at the titles' size. const contents = { levels: [{ level: 1, italic: true, marginBottom: pt(LEAD), numberFontFamily: 'Italiana', numberFontWeight: 400, numberColor: col('sage'), numberWidth: mm(6), numberGap: mm(3) }], pageNumber: { fontFamily: 'Marcellus SC', fontSize: pt(9), color: col('muted'), width: mm(6) }, leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up }; // #endregion // #region ornament: a resource type for artwork that prints no caption and no label // No caption prefix and no caption: the sprig prints bare, where ::resource{id="sprig"} puts // it (double quotes: gotcha resource-double-quotes), a fifth of the measure wide, centred. const ornament = { id: 'ornament', name: 'Ornament', shortLabel: '', captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal', defaultPlacement: { position: 'here', width: 0.2, align: 'center' } }; // #endregion const proseStyles = [ // the note on the text and the colophon, under the contents { id: 'note', fontSize: pt(9.5), lineHeight: pt(13) }, { id: 'colophon', fontSize: pt(8), lineHeight: pt(11), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) }, ]; const folio = (pages) => ({ kind: 'text', id: `folio-${pages}`, content: '{pageNumber}', pages, fontFamily: 'Marcellus SC', fontSize: pt(9), color: col('muted'), align: 'center', placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(10) } } }); const config = () => ({ locale: 'en-us', // the sample is in English colorPalette, // The list replaces Figure and Table; a book with figures spreads defaultResourceTypes() in. resourceTypes: [ornament], page: { // mirror: left is the inner margin; 150 dpi is for the screen sizePreset: 'custom', width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(22), bottom: mm(24), left: mm(22), right: mm(18), mirror: true }, }, layout: { layoutType: 'single' }, bodyText: { // the note's prose: justified, hyphenated (en-us), spaces 0.8–1.6 of normal fontFamily: 'Sorts Mill Goudy', fontSize: pt(11), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(4), indentAfterHeading: false, minWordSpacing: 0.8, maxWordSpacing: 1.6, }, headings: { fontFamily: 'Sorts Mill Goudy', fontWeight: 400, color: col('ink'), levels: [poems, { level: 2, fontFamily: 'Marcellus SC', fontSize: pt(9), lineHeight: pt(LEAD), color: col('sage'), marginTop: pt(0), marginBottom: pt(0) }], }, headingStyles: front, toc: contents, paragraphStyles: [verse, ...lineStyles, ...proseStyles], header: { elements: [] }, // no running heads: each poem starts a page under its own title footer: { elements: [folio('opener'), folio('body')] }, // centred; never on a blank page }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Esempio Markdown · 130 righe · content.en.md
title: "Pied Beauty" subtitle: "Four poems of the natural world" author: "Gerard Manley Hopkins" --- # Pied Beauty {style="half-title"} # Fern and Rowan {style="plate" caption="Fern and rowan: the ‘flitches of fern’ and the ‘beadbonny ash’ of Inversnaid"} # Pied Beauty {style="title-page"} # Contents {style="contents"} :::toc :::space{lines=2} ## A note on the text :::paragraphs{style="note"} This selection follows the first edition of the poems, edited by Robert Bridges in 1918, nearly thirty years after Hopkins’s death. Its spelling, accents and indents are kept, and lines that rhyme with one another start the same distance in. A line too long for the page is turned over, and the turnover is set deeper than the lines around it, so that it reads as part of the line above. No word is broken unless Hopkins broke it, as he did at the end of the first line of *The Windhover*. Under each poem is the date from Bridges’s notes, with the place when he gives one. ::: :::paragraphs{style="colophon"} Set in Sorts Mill Goudy, Italiana and Marcellus SC (SIL Open Font License). Text from Project Gutenberg eBook 22403, accents and indents from Wikisource. Plate made with diffusion models; ornament drawn for this edition. ::: # God’s Grandeur :::verse{style="verse" align=start} The world is charged with the grandeur of God. It will flame out, like shining from shook foil; It gathers to a greatness, like the ooze of oil Crushed. Why do men then now not reck his rod? Generations have trod, have trod, have trod; And all is seared with trade; bleared, smeared with toil; And wears man’s smudge and shares man’s smell: the soil Is bare now, nor can foot feel, being shod. And for all this, nature is never spent; There lives the dearest freshness deep down things; And though the last lights off the black West went Oh, morning, at the brown brink eastward, springs— Because the Holy Ghost over the bent World broods with warm breast and with ah! bright wings. ::: :::paragraphs{style="date"} 23 February 1877 ::: # The Windhover :::paragraphs{style="dedication"} *To Christ our Lord* ::: :::verse{style="verse" align=start} I caught this morning morning’s minion, king- dom of daylight’s dauphin, dapple-dawn-drawn Falcon, in his riding Of the rolling level underneath him steady air, and striding High there, how he rung upon the rein of a wimpling wing In his ecstacy! then off, off forth on swing, As a skate’s heel sweeps smooth on a bow-bend: the hurl and gliding Rebuffed the big wind. My heart in hiding Stirred for a bird,—the achieve of, the mastery of the thing! Brute beauty and valour and act, oh, air, pride, plume, here Buckle! AND the fire that breaks from thee then, a billion Times told lovelier, more dangerous, O my chevalier! No wonder of it: shéer plód makes plough down sillion Shine, and blue-bleak embers, ah my dear, Fall, gall themselves, and gash gold-vermillion. ::: :::paragraphs{style="date"} St Beuno’s, 30 May 1877 ::: # Pied Beauty :::verse{style="verse" align=start} Glory be to God for dappled things— For skies of couple-colour as a brinded cow; For rose-moles all in stipple upon trout that swim: Fresh-firecoal chestnut-falls; finches’ wings; Landscape plotted and pieced—fold, fallow, and plough; And áll trádes, their gear and tackle and trim. All things counter, original, spare, strange; Whatever is fickle, freckled (who knows how?) With swift, slow; sweet, sour; adazzle, dim; He fathers-forth whose beauty is past change: Praise him. ::: :::paragraphs{style="date"} St Beuno’s, Tremeirchion, summer 1877 ::: # Inversnaid :::verse{style="verse" align=start} This darksome burn, horseback brown, His rollrock highroad roaring down, In coop and in comb the fleece of his foam Flutes and low to the lake falls home. A windpuff-bonnet of fáwn-fróth Turns and twindles over the broth Of a pool so pitchblack, féll-frówning, It rounds and rounds Despair to drowning. Degged with dew, dappled with dew Are the groins of the braes that the brook treads through, Wiry heathpacks, flitches of fern, And the beadbonny ash that sits over the burn. What would the world be, once bereft Of wet and of wildness? Let them be left, O let them be left, wildness and wet; Long live the weeds and the wilderness yet. ::: :::paragraphs{style="date"} 28 September 1881 ::: ::resource{id="sprig"}`; // content.<lang>.md, inlined by the Cookbook // #region art: a rowan sprig for the ornament, drawn in code; the plate is a JPEG in assets/ const f = (n) => n.toFixed(1); const path = (d, fill, a = 1) => `<path d="${d}" fill="${fill}" fill-opacity="${a}"/>`; const ring = (pts) => `M${pts.map(([x, y]) => `${f(x)} ${f(y)}`).join('L')}Z`; const disk = (x, y, r, fill, a = 1) => `<circle cx="${f(x)}" cy="${f(y)}" r="${f(r)}" ` + `fill="${fill}" fill-opacity="${a}"/>`; const PX = 10; // a drawing w × h mm has a viewBox in tenths of a millimetre, w·PX × h·PX const svgOf = (w, h, body) => `<svg xmlns="http://www.w3.org/2000/svg" width="${w * PX}" ` + `height="${h * PX}" viewBox="0 0 ${w * PX} ${h * PX}">${body}</svg>`; // A cubic Bézier and its unit normal at t. const bez = ([p0, p1, p2, p3], t) => { const u = 1 - t; const coord = (i) => u * u * u * p0[i] + 3 * u * u * t * p1[i] + 3 * u * t * t * p2[i] + t * t * t * p3[i]; const dt = (i) => 3 * u * u * (p1[i] - p0[i]) + 6 * u * t * (p2[i] - p1[i]) + 3 * t * t * (p3[i] - p2[i]); const [dx, dy] = [dt(0), dt(1)]; const len = Math.hypot(dx, dy) || 1; return { x: coord(0), y: coord(1), a: Math.atan2(dy, dx), nx: -dy / len, ny: dx / len }; }; // A stalk: the curve drawn as a filled band tapering from w0 to w1. const stalk = (curve, w0, w1) => { const left = []; const right = []; for (let i = 0; i <= 40; i++) { const p = bez(curve, i / 40); const w = (w0 + (w1 - w0) * (i / 40)) / 2; left.push([p.x + p.nx * w, p.y + p.ny * w]); right.unshift([p.x - p.nx * w, p.y - p.ny * w]); } return ring([...left, ...right]); }; // A leaf blade from its base (x, y) along angle a: length l, half-width w, `teeth` serrations. const blade = (x, y, a, l, w, teeth = 0) => { const [c, s] = [Math.cos(a), Math.sin(a)]; const at = (u, v) => [x + u * c - v * s, y + u * s + v * c]; const side = (sign) => Array.from({ length: 25 }, (_, i) => { const t = i / 24; let h = w * Math.sin(Math.PI * t ** 0.85) ** 0.9; if (teeth && t > 0.25) h *= 1 - 0.18 * ((t * teeth) % 1); return at(l * t, sign * h); }); return ring([...side(1), ...side(-1).reverse()]); }; // The ornament: a rowan leaf laid flat, with a bunch of berries at its tip. function sprig() { const [w, h] = [36, 12]; const rib = [[16, 66], [90, 56], [170, 56], [236, 60]]; const d = [stalk(rib, 6, 3)]; for (let j = 0; j < 5; j++) { const q = bez(rib, 0.12 + j * 0.19); const size = 50 - j * 3; for (const sgn of [1, -1]) d.push(blade(q.x, q.y, q.a + sgn * 1.1, size, size * 0.22, 8)); } const tip = bez(rib, 1); d.push(blade(tip.x, tip.y, tip.a, 44, 10.5, 8)); const out = [path(d.join(''), palette.sage)]; for (const [dx, dy, r] of [[34, -16, 9], [44, 2, 10], [30, 14, 9], [52, -12, 8.5], [58, 10, 8], [22, -2, 8]]) { const [bx, by] = [262 + dx, 60 + dy]; out.push(path(stalk([[240, 64], [252, 66], [bx - 10, by + 2], [bx, by]], 2.6, 1.8), palette.sage), disk(bx, by, r, palette.sepia)); } return svgOf(w, h, out.join('')); } await loadSvg('sprig.svg', sprig()); // The frontispiece is a photogram: the whole 140 × 216 mm page, its torn coating and the paper // under it in one JPEG, declared at its pixels. await loadImage('plate-1120.jpg', asset('plate-1120.jpg')); // Nothing cites them: designs show the plate and the sprig, ::resource sets the tailpiece. const svgResource = (id, w, h, altText) => ({ id, typeId: 'ornament', kind: 'svg', altText, createdAt: 0, updatedAt: 0, svg: { fileId: `${id}.svg`, width: w * PX, height: h * PX } }); const resources = [ { id: 'plate', typeId: 'ornament', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'plate-1120.jpg', format: 'jpeg', width: 1120, height: 1728 }, altText: 'A photogram of a fern frond and a sprig of rowan in berry, left white on a ' + 'brushed sage ground.' }, svgResource('sprig', 36, 12, 'Ornament: a rowan leaf with a bunch of berries.'), ]; // #endregion // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Loaded before the first build. None of the three ships a bold. const FONTS = { 'Sorts Mill Goudy': ['400', '400i'], Italiana: ['400'], 'Marcellus SC': ['400'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── // :::toc lists the page each poem lands on: the build lays out again until those settle. const doc = await buildDocumentWithFonts({ markdown, resources }, config(), kitFonts(FONTS)); showPages(doc, { title: 'Pied Beauty · four poems by Gerard Manley Hopkins' });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
#Fai sporgere di più i versi a capo
Un rientro a sporgere più profondo allontana dal margine il resto di un verso a capo, per poesie i cui versi vanno spesso a capo.
- hangingIndent: em(2), // a turnover hangs 2 em past its line's start: 4 em under a 2 em indent
+ hangingIndent: em(4), // a turnover hangs 2 em past its line's start: 4 em under a 2 em indent#Allinea a destra i versi a capo
Con turnover: 'right' in bodyText.verse, il resto di un verso troppo lungo per la giustezza va allineato a destra sulla riga sotto, dietro una parentesi quadra aperta, come lo compongono molte edizioni inglesi di poesia.
indentAfterHeading: false, minWordSpacing: 0.8, maxWordSpacing: 1.6,
+ verse: { turnover: 'right' }, // a turnover flush right, behind [
},#Apri ogni poesia su una pagina di destra
Con la parità 'odd' ogni poesia si apre sul recto successivo. Il libro passa da otto a undici pagine, e le tre pagine di sinistra bianche non stampano folio, perché i folio sono filtrati per ruolo della pagina.
const poems = { level: 1, numberingTemplate: '{1:I}', advancedDesign: poemHead,
- marginBottom: pt(0), breakBefore: { enabled: true, parity: 'any' } };
+ marginBottom: pt(0), breakBefore: { enabled: true, parity: 'odd' } };#Numera le poesie in cifre
Lo stile del contatore del modello cambia insieme il numero sulla pagina della poesia e nell’indice.
-const poems = { level: 1, numberingTemplate: '{1:I}', advancedDesign: poemHead,
+const poems = { level: 1, numberingTemplate: '{1}', advancedDesign: poemHead,Errori frequenti
Errore frequente
'1998. ' o '- ' a inizio paragrafo aprono un elenco
Un paragrafo che comincia con un numero, un punto e uno spazio, o con un trattino e uno spazio, diventa una voce di elenco. Metti un word joiner (U+2060) prima del numero, e scrivi i dialoghi con la lineetta. Escape e caratteri letterali →
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 →
Errore frequente
::resource{id="…"} accetta solo virgolette doppie
Un inserimento a blocco viene riconosciuto solo come ::resource{id="…"} con virgolette doppie; qualsiasi altra forma resta nel testo come riga visibile. Figure proprio qui →
- Tieni uniti i versi di una strofa: una riga vuota dentro un blocco
:::versecomincia una nuova strofa, per quante siano le righe vuote. - Una riga che comincia con un segno più e uno spazio è un verso a gradino, composto dove è finito il verso sopra; scrivi
\+per stampare il segno più. - Senza
align=startogni poesia si centra sul suo verso più lungo, e ogni poesia si trova allora a una distanza diversa dal margine. - Per mandare a capo due parole insieme, legale con uno spazio non divisibile (U+00A0), come lo sono the soil e bright wings. a pagina 5.
- Un elemento del piè di pagina con
pageslasciato su'all'si stampa anche sulle pagine bianche. Qui il folio è fatto di due elementi, uno per le aperture e uno per le pagine di testo, così un verso bianco resta bianco.
Crediti
- Ricetta
- Ignacio Ferro
- Testo
- “God’s Grandeur”, “The Windhover”, “Pied Beauty” and “Inversnaid”, with the places and dates from the editor’s notes, in the first edition of Poems of Gerard Manley Hopkins (1918); accents and indents checked against the proofread Wikisource transcription of that edition · Gerard Manley Hopkins; Robert Bridges (editor) · pubblico dominio
- The note on the text, the plate’s caption and the colophon · Ignacio Ferro · CC BY 4.0
- Immagini
- The frontispiece photogram of fern and rowan · Generated With Diffusion Models · originale
- The rowan ornament, drawn in code in the page’s palette · Ignacio Ferro · CC BY 4.0
- Font
- Sorts Mill Goudy (SIL OFL 1.1) · Italiana (SIL OFL 1.1) · Marcellus SC (SIL OFL 1.1)
- Codice
- MIT, come Postext
Modifica questo testo ↗ (si apre in una nuova scheda)Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)


