In breve
Una guida tascabile agli uccelli dell’estuario in due parti, una marrone fango e una verde canneto. Mostra come un’impostazione di colore per parte tinge la pagina d’apertura, le linguette, i numeri di pagina e la riga dell’indice.
Che cosa comporrai
Birds of the Estuary è una guida da campo tascabile in due parti, una per ambiente. Le piane di fango si stampano in marrone fango e i canneti in verde canneto, e ogni colore si scrive una volta sola, nel :::part che apre il suo ambiente. Ogni pagina di parte è un campo al vivo con un numero romano in corpo 150 e le specie dell’ambiente elencate in crema. Il retro del foglio è dipinto in tinta, con la linguetta della parte in negativo sul taglio esterno. Nelle pagine delle specie la linguetta al taglio, il folio, l’occhiello, il filetto sotto il nome latino, i segni distintivi in grassetto, i punti elenco e l’etichetta della tavola prendono il colore della parte. L’indice si apre sotto una striscia dell’estuario e dà a ogni parte una fascia nel suo colore, con il numero di pagina della sua pagina di parte. La copertina, la striscia e le quattro tavole sono dipinti generati con modelli di diffusione, caricati come bitmap JPEG; la palette di una parte non arriva alle immagini, quindi i loro colori restano i loro.
Questa ricetta risponde a
- Come divido un libro in parti o sezioni, ognuna con il suo colore e la sua pagina di parte?
- Come nascondo le testatine nelle aperture e nelle pagine bianche, o coloro un verso bianco con il colore della parte?
- Come aggiungo un indice che si aggiorna da solo (puntini di guida, numeri di pagina, autori, righe di parte)?
- Come coloro i termini chiave (in grassetto o in corsivo) nel testo o dentro i riquadri?
La risposta breve
// In the Markdown: :::part{number="I" title="The \\ Mudflats" palette="band=#8c5e24"}
// Every colour below that is linked to 'band' takes #8c5e24 until the next part.
const parts = { // passed to the config as `parts`
// A divider opens on a recto by default. The break after it goes to the next recto too, so
// the back of the leaf stays blank and versoDesign paints it (gotcha: verso-design-breakafter).
breakAfter: { parity: 'odd' },
margins: { top: mm(125) }, // the fence's text starts low, under the title
design: { elements: [ // the divider: its container is the whole trim
box('field', col('band'), { ...at('top-left', 0, 0, 'bleed'), ...fill }),
text('part', t({ en: 'Part', es: 'Parte' }), { ...label, fontSize: pt(10),
letterSpacing: pt(2.4), color: col('paper') },
at('top-left', MARGIN.inner, MARGIN.top)), // a recto: the inner margin is on the left
// Roman for the parts, Arabic for the species. {numberRoman} re-formats number="I" (or
// "1"); the species design prints {number}, from numberingTemplate '{1}'.
text('numeral', '{numberRoman}', { ...display, fontSize: pt(150), lineHeight: 0.9,
color: col('paper') }, below('part', 0)),
// The \\ in the title breaks the line here; the contents and the heads get one line.
text('title', '{titleText}', { ...display, fontSize: pt(46), lineHeight: 0.98,
color: col('paper') }, below('numeral', 2, { width: mm(MEASURE) })),
rule('rule', col('paper'), 1, below('title', 7, { width: mm(14) })),
] },
// The back of the leaf: the same band, edge to edge, the part's tab in reverse at the
// fore-edge (a verso's is on the left) and its name at the foot.
versoDesign: { elements: [
box('field', col('band'), { ...at('top-left', 0, 0, 'bleed'), ...fill }),
text('tab', '{partNumber}', { ...label, fontSize: pt(9), color: col('band'), align: 'center',
box: { backgroundColor: col('paper') } }, { ...at('top-left', 0, TAB.y), size: TAB.size }),
text('name', '{partTitle}', { ...label, fontSize: pt(9), letterSpacing: pt(2.4),
color: col('paper') }, at('bottom-left', MARGIN.outer, -MARGIN.bottom)),
] },
// The fence's list of species, in the paper colour on the band.
bodyStyle: { fontSize: pt(11), color: col('paper'), textAlign: 'left', numberColor: col('paper'),
orderedLists: { fontFamily: 'Barlow Condensed', separator: '', gap: mm(4) } },
};
Ingredienti
- Funzionalità
- Colori per partePagine di parteIndiceTavolozza di colori semanticaGrassetto, corsivo e i loro coloriFasce al vivo e unghiatureTestatine secondo il ruolo della paginaTestatine e folioAperture progettateAttributi dei titoliTitoli numeratiStili di titoloCapitoli non numeratiA capo nei titoliCopertine, frontespizi e colophonImmagini nei progetti di paginaTipi di risorsa personalizzatiFigure proprio quiRiquadri e badge fissatiInterruzioni di pagina e di colonna
- Tipo
- Alegreya, Zilla Slab, Barlow Condensed (SIL OFL 1.1)
- Risorse
cover-1152.jpgcurlew-1200.jpgredshank-1200.jpgreedling-1200.jpgstrip-1536.jpgwarbler-1200.jpg- The cover: a curlew on the estuary mud, reeds in front (Generated With Diffusion Models, originale)
- The contents strip: a curlew and a redshank on the mud, the reedbed beyond (Generated With Diffusion Models, originale)
- Plate 1: Eurasian curlew (Generated With Diffusion Models, originale)
- Plate 2: common redshank (Generated With Diffusion Models, originale)
- Plate 3: bearded reedling (Generated With Diffusion Models, originale)
- Plate 4: common reed warbler (Generated With Diffusion Models, originale)
Preparazione
#1 · Una voce di palette che le parti sostituiscono
const palette = {
ink: '#1f2624', // text: a green-tinted near-black
band: '#3c4b4f', // the house slate, before any part; each :::part brings its own
paper: '#f6f3ea', // the page, and the type set on a band
rule: '#d5d1c4', // hairlines
muted: '#61675f', // running heads, Latin names, the colophon
};
// The paletteId is the link a part's palette="band=#…" follows, in text and designs alike.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
// The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue.
{ id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
Una parte sostituisce le voci di palette per id, quindi tutto ciò che deve cambiare con l’ambiente si collega a una sola voce, band. Il suo valore proprio, un grigio ardesia, si stampa solo fuori dalle parti (l’occhiello della copertina e il titolo della nota sotto l’indice), e nessun altro colore della configurazione ha lo stesso esadecimale. col() dà a ogni colore il suo paletteId, ed è quell’id che segue la sostituzione di una parte, allo stesso modo negli stili del testo e negli elementi grafici.
#2 · Una pagina di parte, il suo verso e il suo elenco
Il codice di questo passo è nella risposta breve qui sopra. Un :::part apre una pagina di parte su un recto, e parts.design la impagina su tutto il formato rifilato. Il Markdown tra i suoi delimitatori comincia 125 mm sotto il bordo superiore, sotto il titolo: qualche riga sull’ambiente nello stile di paragrafo habitat, poi il suo elenco di specie in bodyStyle. versoDesign dipinge il retro del foglio (il campo e la linguetta in negativo) solo quando quella pagina è bianca, e breakAfter: { parity: 'odd' } la tiene bianca mandando la prima specie al recto successivo. {numberRoman} stampa il number="I" della parte come numero romano ("1" stamperebbe lo stesso I), mentre le specie sono numerate da 1 a 4 in cifre arabe. Il \\ nel titolo va a capo solo nella pagina di parte; l’indice e le testatine compongono il titolo su una riga.
#3 · Gli accenti del testo seguono la parte
const bodyText = {
fontFamily: 'Alegreya', fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('band'), // the field marks: **Bill**, **Voice.**
italicColor: col('ink'), referenceColor: col('ink'),
firstLineIndent: mm(4), indentAfterHeading: false,
// Tighter than the 0.6–2 defaults. runtMinCharacters counts word spaces, not letters:
// 45 are about 20 letters of Alegreya (the default 20, about 9); a shorter last line is a runt.
minWordSpacing: 0.75, maxWordSpacing: 1.7, runtMinCharacters: 45,
};
const unorderedLists = { bulletChar: '•', color: col('band'), // the field marks' bullets
marginTop: pt(0), marginBottom: pt(0) };
// The plates are numbered with the species, and their label is in the part's colour.
const resourceTypes = [{ id: 'plate', numberingTemplate: '{n}', resetOn: 'never',
counterFormat: 'decimal', ...t({
en: { name: 'Plate', namePlural: 'Plates', shortLabel: 'Pl.', captionPrefix: 'Plate' },
es: { name: 'Lámina', namePlural: 'Láminas', shortLabel: 'Lám.', captionPrefix: 'Lámina' },
}) }];
const captionStyle = { fontSize: pt(8.5), labelColor: col('band'), descriptionItalic: true,
gap: mm(1.8) };
Da un :::part al successivo, ogni colore del testo uguale al valore proprio di band prende il valore della parte (il contenitore :::part). Qui sono i segni distintivi in grassetto, i punti elenco e le etichette delle tavole. Il testo corrente e i corsivi restano in nero. Le tavole sono un tipo di risorsa a sé, con l’etichetta «Plate» e senza azzeramento, quindi contano da 1 a 4 al passo con le specie.
#4 · L’apertura della specie
const species = { enabled: true, slot: { elements: [
text('kicker', '{number} · {attr.status}', { ...label, fontSize: pt(8.5),
letterSpacing: pt(1.7), color: col('band') }, at('top-left', 0, 1, 'container')),
// 'cm' is a unit symbol: it keeps its lower case, so the size is not set in capitals.
text('size', '{attr.size}', { ...label, textTransform: 'none', fontSize: pt(8.5),
letterSpacing: pt(0.5), color: col('muted') }, at('top-right', 0, 1, 'container')),
text('name', '{titleText}', { ...display, fontSize: pt(22), lineHeight: 1.05,
color: col('ink') }, below('kicker', 1.5, { width: 'fill' })),
text('latin', '{attr.latin}', { fontFamily: 'Alegreya', italic: true, fontSize: pt(11.5),
color: col('ink') }, below('name', 0.8)),
rule('rule', col('band'), 0.75, below('latin', 2, { width: mm(MEASURE) })),
] } };
L’apertura della specie stampa tre attributi del titolo: status nell’occhiello dopo il numero, size a destra dell’occhiello e latin sotto il nome. L’occhiello e il filetto sotto il nome latino sono collegati a band, così un solo progetto grafico serve entrambi gli ambienti. Il titolo non imposta un’interruzione di pagina propria: ogni specie comincia dopo un :::pagebreak, o dopo l’interruzione che segue la parte, così la sua pagina conta come pagina di testo e stampa le testatine. La misura è composta senza le maiuscole dell’etichetta, perché cm è un simbolo di unità.
#5 · Una fascia per parte nell’indice
const contents = {
levels: [{ level: 1, fontFamily: 'Zilla Slab', fontSize: pt(12), fontWeight: 600,
numberFontFamily: 'Barlow Condensed', numberFontWeight: 600, numberColor: col('muted'),
numberWidth: mm(5), numberGap: mm(3), marginTop: pt(4) }],
pageNumber: { fontFamily: 'Barlow Condensed', fontSize: pt(10), fontWeight: 600, width: mm(7) },
leader: { char: '. ' },
subtitle: { enabled: true, attr: 'latin', fontFamily: 'Alegreya', fontSize: pt(9.5),
color: col('muted') }, // the Latin name, in italic by default
// A part row lays out this design with the part's number, title, page and palette.
parts: { height: mm(8.5), marginTop: pt(LEAD), design: { elements: [
box('row', col('band'), { ...at('top-left', 0, 0, 'container'), ...fill }),
text('part', t({ en: 'Part {number}', es: 'Parte {number}' }), { ...label, fontSize: pt(8),
letterSpacing: pt(1.6), color: col('paper') }, at('left', 3, 0, 'container')),
text('title', '{titleText}', { ...display, fontSize: pt(12), color: col('paper') },
at('left', 20, 0, 'container')),
text('page', '{pageNumber}', { ...label, fontSize: pt(10), color: col('paper'),
align: 'right' }, at('right', -2.5, 0, 'container')),
] } },
};
:::toc dà una riga a ogni :::part e vi impagina toc.parts.design con il numero, il titolo, la pagina di parte e la palette di quella parte (indice), così lo stesso riquadro si stampa marrone per le piane di fango e verde per i canneti. Ogni riga di specie sotto una fascia mostra il suo numero, una linea di puntini e la sua pagina, con il nome latino dell’attributo latin sulla riga sotto.
#6 · La parte nelle testatine e nella linguetta
const head = (id, content, parity, placement, style = {}) => ({ ...text(id, content, { ...label,
fontSize: pt(8), letterSpacing: pt(1.4), color: col('muted'), overflow: 'clip', ...style },
placement), parity, pages: 'body' }); // dividers are 'part' pages, their versos 'blank'
const folio = { fontSize: pt(9), fontWeight: 700, color: col('band') };
const tab = (parity, edge) => head(`tab-${parity}`, '{partNumber}', parity,
{ ...at(edge, 0, TAB.y), size: TAB.size },
{ fontSize: pt(9), color: col('paper'), align: 'center', box: { backgroundColor: col('band') } });
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', at('top-left', MARGIN.outer, HEAD.y), folio),
head('verso-title', BOOK, 'even', at('top-left', MARGIN.outer + HEAD.gap, HEAD.y)),
head('recto-title', '{partTitle}', 'odd', at('top-right', -(MARGIN.outer + HEAD.gap), HEAD.y)),
head('recto-folio', '{pageNumber}', 'odd', at('top-right', -MARGIN.outer, HEAD.y), folio),
tab('even', 'top-left'), tab('odd', 'top-right'), // on the fore-edge, left on a verso
] };
Con pages: 'body' le testatine si stampano solo sulle pagine di testo. Le pagine di parte sono pagine part e i loro versi dipinti pagine blank; la copertina e l’indice si aprono ciascuno con un titolo a tutta pagina, il che li rende aperture. La testatina del recto stampa {partTitle} su una riga, e la linguetta sul taglio esterno stampa {partNumber} su un riquadro il cui sfondo è collegato a band.
La ricetta completa
// ═══ Postext Cookbook · Nº 019 · Parts in colour from one attribute ═══════════════════════ // https://postext.dev/en/cookbook/parts-in-colour // Code: MIT · Text: original (CC BY 4.0) · Pictures: diffusion models // Fonts: Alegreya, Zilla Slab, Barlow Condensed (SIL OFL 1.1) · Needs postext ≥ 1.25.0 // A pocket field guide to two habitats. Each :::part names its own 'band' colour, and every // colour linked to 'band' takes it: the divider and its verso, the tab, the field marks. import { buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'parts-in-colour'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: 'band' is the entry the parts override; the others keep their value const palette = { ink: '#1f2624', // text: a green-tinted near-black band: '#3c4b4f', // the house slate, before any part; each :::part brings its own paper: '#f6f3ea', // the page, and the type set on a band rule: '#d5d1c4', // hairlines muted: '#61675f', // running heads, Latin names, the colophon }; // The paletteId is the link a part's palette="band=#…" follows, in text and designs alike. const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = [ ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })), // The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue. { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } }, ]; // #endregion const TRIM = { width: 150, height: 200 }; // a pocket guide const MARGIN = { top: 22, bottom: 20, inner: 19, outer: 21 }; // mirrored; room for a thumb const MEASURE = TRIM.width - MARGIN.inner - MARGIN.outer; // 110 mm, about 70 characters const LEAD = 14.5; // body leading in pt: the baseline grid const STRIP = 44; // the contents' picture strip, from the top edge (mm) const HEAD = { y: 12, gap: 8 }; // running heads from the top edge; folio to title (mm) const TAB = { y: 26, size: { width: mm(8), height: mm(24) } }; // the thumb tab, at the fore-edge const TITLE_DROP = 6; // mm from the foot of the contents strip to the title's box const BOOK = t({ en: 'Birds of the Estuary', es: 'Aves del estuario' }); const label = { fontFamily: 'Barlow Condensed', fontWeight: 600, textTransform: 'uppercase' }; const display = { fontFamily: 'Zilla Slab', fontWeight: 700 }; const at = (edge, x, y, to = 'page') => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); const below = (id, y, size) => ({ ...at('below', 0, y, `#${id}`), ...(size && { size }) }); const fill = { size: { width: 'fill', height: 'fill' } }; const text = (id, content, style, placement) => ({ kind: 'text', id, content, overflow: 'wrap', align: 'left', ...style, placement }); const box = (id, color, placement) => ({ kind: 'box', id, style: { backgroundColor: color }, placement }); const rule = (id, color, w, placement) => ({ kind: 'rule', id, color, thickness: pt(w), placement }); // #region answer: a divider, its painted verso and its list, all in the part's own 'band' // In the Markdown: :::part{number="I" title="The \\ Mudflats" palette="band=#8c5e24"} // Every colour below that is linked to 'band' takes #8c5e24 until the next part. const parts = { // passed to the config as `parts` // A divider opens on a recto by default. The break after it goes to the next recto too, so // the back of the leaf stays blank and versoDesign paints it (gotcha: verso-design-breakafter). breakAfter: { parity: 'odd' }, margins: { top: mm(125) }, // the fence's text starts low, under the title design: { elements: [ // the divider: its container is the whole trim box('field', col('band'), { ...at('top-left', 0, 0, 'bleed'), ...fill }), text('part', t({ en: 'Part', es: 'Parte' }), { ...label, fontSize: pt(10), letterSpacing: pt(2.4), color: col('paper') }, at('top-left', MARGIN.inner, MARGIN.top)), // a recto: the inner margin is on the left // Roman for the parts, Arabic for the species. {numberRoman} re-formats number="I" (or // "1"); the species design prints {number}, from numberingTemplate '{1}'. text('numeral', '{numberRoman}', { ...display, fontSize: pt(150), lineHeight: 0.9, color: col('paper') }, below('part', 0)), // The \\ in the title breaks the line here; the contents and the heads get one line. text('title', '{titleText}', { ...display, fontSize: pt(46), lineHeight: 0.98, color: col('paper') }, below('numeral', 2, { width: mm(MEASURE) })), rule('rule', col('paper'), 1, below('title', 7, { width: mm(14) })), ] }, // The back of the leaf: the same band, edge to edge, the part's tab in reverse at the // fore-edge (a verso's is on the left) and its name at the foot. versoDesign: { elements: [ box('field', col('band'), { ...at('top-left', 0, 0, 'bleed'), ...fill }), text('tab', '{partNumber}', { ...label, fontSize: pt(9), color: col('band'), align: 'center', box: { backgroundColor: col('paper') } }, { ...at('top-left', 0, TAB.y), size: TAB.size }), text('name', '{partTitle}', { ...label, fontSize: pt(9), letterSpacing: pt(2.4), color: col('paper') }, at('bottom-left', MARGIN.outer, -MARGIN.bottom)), ] }, // The fence's list of species, in the paper colour on the band. bodyStyle: { fontSize: pt(11), color: col('paper'), textAlign: 'left', numberColor: col('paper'), orderedLists: { fontFamily: 'Barlow Condensed', separator: '', gap: mm(4) } }, }; // #endregion // #region flow: the accents of the text, linked to 'band' so that each part retints them const bodyText = { fontFamily: 'Alegreya', fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('band'), // the field marks: **Bill**, **Voice.** italicColor: col('ink'), referenceColor: col('ink'), firstLineIndent: mm(4), indentAfterHeading: false, // Tighter than the 0.6–2 defaults. runtMinCharacters counts word spaces, not letters: // 45 are about 20 letters of Alegreya (the default 20, about 9); a shorter last line is a runt. minWordSpacing: 0.75, maxWordSpacing: 1.7, runtMinCharacters: 45, }; const unorderedLists = { bulletChar: '•', color: col('band'), // the field marks' bullets marginTop: pt(0), marginBottom: pt(0) }; // The plates are numbered with the species, and their label is in the part's colour. const resourceTypes = [{ id: 'plate', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal', ...t({ en: { name: 'Plate', namePlural: 'Plates', shortLabel: 'Pl.', captionPrefix: 'Plate' }, es: { name: 'Lámina', namePlural: 'Láminas', shortLabel: 'Lám.', captionPrefix: 'Lámina' }, }) }]; const captionStyle = { fontSize: pt(8.5), labelColor: col('band'), descriptionItalic: true, gap: mm(1.8) }; // #endregion // #region species: each entry opens with its number and status, name, Latin and a 'band' rule const species = { enabled: true, slot: { elements: [ text('kicker', '{number} · {attr.status}', { ...label, fontSize: pt(8.5), letterSpacing: pt(1.7), color: col('band') }, at('top-left', 0, 1, 'container')), // 'cm' is a unit symbol: it keeps its lower case, so the size is not set in capitals. text('size', '{attr.size}', { ...label, textTransform: 'none', fontSize: pt(8.5), letterSpacing: pt(0.5), color: col('muted') }, at('top-right', 0, 1, 'container')), text('name', '{titleText}', { ...display, fontSize: pt(22), lineHeight: 1.05, color: col('ink') }, below('kicker', 1.5, { width: 'fill' })), text('latin', '{attr.latin}', { fontFamily: 'Alegreya', italic: true, fontSize: pt(11.5), color: col('ink') }, below('name', 0.8)), rule('rule', col('band'), 0.75, below('latin', 2, { width: mm(MEASURE) })), ] } }; // #endregion // #region contents: a band per part in that part's colour, then its species with leaders const contents = { levels: [{ level: 1, fontFamily: 'Zilla Slab', fontSize: pt(12), fontWeight: 600, numberFontFamily: 'Barlow Condensed', numberFontWeight: 600, numberColor: col('muted'), numberWidth: mm(5), numberGap: mm(3), marginTop: pt(4) }], pageNumber: { fontFamily: 'Barlow Condensed', fontSize: pt(10), fontWeight: 600, width: mm(7) }, leader: { char: '. ' }, subtitle: { enabled: true, attr: 'latin', fontFamily: 'Alegreya', fontSize: pt(9.5), color: col('muted') }, // the Latin name, in italic by default // A part row lays out this design with the part's number, title, page and palette. parts: { height: mm(8.5), marginTop: pt(LEAD), design: { elements: [ box('row', col('band'), { ...at('top-left', 0, 0, 'container'), ...fill }), text('part', t({ en: 'Part {number}', es: 'Parte {number}' }), { ...label, fontSize: pt(8), letterSpacing: pt(1.6), color: col('paper') }, at('left', 3, 0, 'container')), text('title', '{titleText}', { ...display, fontSize: pt(12), color: col('paper') }, at('left', 20, 0, 'container')), text('page', '{pageNumber}', { ...label, fontSize: pt(10), color: col('paper'), align: 'right' }, at('right', -2.5, 0, 'container')), ] } }, }; // #endregion // #region running-heads: on body pages only, never on a divider or its verso; a 'band' tab const head = (id, content, parity, placement, style = {}) => ({ ...text(id, content, { ...label, fontSize: pt(8), letterSpacing: pt(1.4), color: col('muted'), overflow: 'clip', ...style }, placement), parity, pages: 'body' }); // dividers are 'part' pages, their versos 'blank' const folio = { fontSize: pt(9), fontWeight: 700, color: col('band') }; const tab = (parity, edge) => head(`tab-${parity}`, '{partNumber}', parity, { ...at(edge, 0, TAB.y), size: TAB.size }, { fontSize: pt(9), color: col('paper'), align: 'center', box: { backgroundColor: col('band') } }); const header = { elements: [ head('verso-folio', '{pageNumber}', 'even', at('top-left', MARGIN.outer, HEAD.y), folio), head('verso-title', BOOK, 'even', at('top-left', MARGIN.outer + HEAD.gap, HEAD.y)), head('recto-title', '{partTitle}', 'odd', at('top-right', -(MARGIN.outer + HEAD.gap), HEAD.y)), head('recto-folio', '{pageNumber}', 'odd', at('top-right', -MARGIN.outer, HEAD.y), folio), tab('even', 'top-left'), tab('odd', 'top-right'), // on the fore-edge, left on a verso ] }; // #endregion // The cover and the contents are headings with no number and no contents entry. They get // no running heads either: a page-wide heading makes its page an 'opener', not 'body'. const unlisted = { numbered: false, toc: false, span: 'page' }; const cover = { enabled: true, slot: { elements: [ { kind: 'image', id: 'art', resourceId: 'cover', placement: { ...at('top-left', 0, 0, 'bleed'), size: { width: 'fill' } } }, text('kicker', '{subtitle}', { ...label, fontSize: pt(9), letterSpacing: pt(1.8), color: col('band') }, at('top-left', MARGIN.inner, MARGIN.top)), // slate: no part yet text('title', '{titleText}', { ...display, fontSize: pt(50), lineHeight: 0.95, color: col('ink') }, below('kicker', 3, { width: mm(120) })), ] } }; // The contents open under a strip of the estuary: mud and waders, then the reeds. const contentsOpener = { enabled: true, minHeight: mm(STRIP), slot: { elements: [ { kind: 'image', id: 'strip', resourceId: 'strip', placement: { ...at('top-left', 0, 0, 'bleed'), size: { width: 'fill' } } }, text('title', '{titleText}', { ...display, fontSize: pt(26), color: col('ink') }, at('top-left', 0, STRIP - MARGIN.top + TITLE_DROP, 'container')), ] } }; const config = () => ({ locale: t({ en: 'en-us', es: 'es' }), // hyphenation patterns and the built-in labels colorPalette, page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true } }, layout: { layoutType: 'single' }, bodyText, unorderedLists, resourceTypes, captionStyle, headings: { ...display, levels: [ // No break of its own: an H1 that breaks the page makes each species page an 'opener', // with no running heads. A :::pagebreak, or the part's break, starts each species. { level: 1, fontSize: pt(22), breakBefore: { enabled: false }, numberingTemplate: '{1}', marginBottom: pt(0), advancedDesign: species }, ] }, headingStyles: [ { id: 'cover', ...unlisted, advancedDesign: cover }, { id: 'contents', ...unlisted, advancedDesign: contentsOpener }, ], toc: contents, parts, paragraphStyles: [ // The habitat's few lines on a divider: no indent, in the paper colour. { id: 'habitat', fontSize: pt(11), color: col('paper'), textAlign: 'left', firstLineIndent: pt(0), marginBottom: pt(LEAD / 2) }, // Set in the note under the contents: the 15 pt leading gives its lines their air. { id: 'colophon', fontFamily: 'Barlow Condensed', fontSize: pt(8), lineHeight: pt(15), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0) }, ], // The note under the contents is pinned to the foot of the text block. calloutStyles: [{ id: 'about', placement: 'fixed', backgroundEnabled: false, stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') }, padding: { top: mm(3), right: pt(0), bottom: pt(0), left: pt(0) }, titleStyle: { ...label, fontSize: pt(8), letterSpacing: pt(1.6), color: col('band') }, body: { fontSize: pt(9.5), lineHeight: pt(13), firstLineIndent: pt(0), textAlign: 'left' } }], header, footer: { elements: [] }, // the default footer would centre a folio in Open Sans }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Esempio Markdown · 95 righe · content.en.md
title: "Birds of the Estuary" subtitle: "A pocket guide to the mudflats and the reedbeds" --- # Birds of \\ the Estuary {style="cover"} :::pagebreak # Contents {style="contents"} :::toc :::callout{type="about" title="How to use this guide"} The birds are grouped by habitat: the open mud of the lower estuary, and the reedbeds along its upper reaches. Each habitat has a colour, printed on its tab at the edge of the page, on the field marks in its species texts and on its row in the list above. Sizes are total length, from bill tip to tail tip. :::paragraphs{style="colophon"} Set in Alegreya, Zilla Slab and Barlow Condensed (SIL OFL) · Text: CC BY 4.0 · Plates: diffusion models. ::: ::: :::part{number="I" title="The \\ Mudflats" palette="band=#8c5e24"} :::paragraphs{style="habitat"} Twice a day the tide drains out of the estuary and leaves a mile of shining mud. It looks empty, but every square metre hides thousands of worms, snails and shrimps, and waders fly in from half of Europe to feed on them. ::: 1. Eurasian Curlew 2. Common Redshank ::: # Eurasian Curlew {latin="Numenius arquata" size="50–60 cm" status="Winter visitor"} ::resource{id="curlew"} Our largest wader, and the easiest to name: no other bird on the mud carries so long and so curved a bill. Curlews work the soft mud at low water, walking slowly and probing to the hilt for worms and small crabs. - **Bill** very long and down-curved, longest in the female. - **Plumage** grey-brown, finely streaked; legs grey-blue. - **In flight** a white wedge runs up the back from the tail. **Voice.** A rising, far-carrying *cur-lee*, the sound of an estuary in winter; in spring, a bubbling song on the moors where it breeds. **Where and when.** On the mud from August to March, roosting in flocks on the saltmarsh at high tide. The curlew is declining across Europe and listed as Near Threatened: give a roost a wide berth. :::pagebreak # Common Redshank {latin="Tringa totanus" size="27–29 cm" status="Resident"} ::resource{id="redshank"} The noisiest bird on the marsh. A redshank sees you first and tells everything within half a mile, bobbing nervously and piping as it flies off; it has long been called the warden of the marshes. - **Legs** bright orange-red; the bill red at the base, dark at the tip. - **Plumage** plain grey-brown in winter, mottled browner in summer. - **In flight** a broad white trailing edge to the wing, and white up the back. **Voice.** A ringing *tew-hu-hu*, often the first sound you hear on the marsh, and a frantic *teuk-teuk-teuk* when it is alarmed. **Where and when.** All year round on the creeks and the tideline, picking shrimps and small snails from the mud. It nests in tussocks of saltmarsh grass, often in loose colonies. :::part{number="II" title="The \\ Reedbeds" palette="band=#51702f"} :::paragraphs{style="habitat"} Where the river meets the tide, the common reed grows in beds taller than a person, green in summer and gold in winter. Few birds live in them, and most of those stay hidden among the stems. ::: 3. Bearded Reedling 4. Eurasian Reed Warbler ::: # Bearded Reedling {latin="Panurus biarmicus" size="14.5–17 cm" status="Resident"} ::resource{id="reedling"} A small, long-tailed bird that seldom leaves the reeds, and you will usually hear it first: a pinging call from deep in the reedbed, then a party of birds whirring low over the plumes on short, rounded wings. - **Male** head blue-grey, with a drooping black moustache (and no beard). - **Body** warm tawny; the long, graduated tail is half the bird. - **Female** brown-headed and without the moustache. **Voice.** A metallic *ping*, like a tiny bell, repeated as the party moves. **Where and when.** All year in large reedbeds. It eats insects in summer and reed seeds in winter, and swallows grit in autumn to grind them. Once classed with the tits (Linnaeus named it *Parus biarmicus*), it now has a family of its own, the Panuridae. :::pagebreak # Eurasian Reed Warbler {latin="Acrocephalus scirpaceus" size="13 cm" status="Summer visitor"} ::resource{id="warbler"} A plain brown warbler, heard far more often than seen. Its song runs on from inside the reeds all through a summer day: a slow, rhythmic chatter, *jit-jit-jit, churr-churr*, with phrases borrowed from other birds. - **Upperparts** unstreaked warm brown; underparts buff, throat whitish. - **Head** a flat forehead and a strong, pointed bill. - **Movement** sidles up the stems, often gripping two at once. **Where and when.** A summer visitor from late April to September, wintering in Africa south of the Sahara. Its nest is a deep cup woven round three or four reed stems, and it is one of the cuckoo’s commonest hosts.`; // content.<lang>.md, inlined by the Cookbook // The plates' captions, and the alt text of every drawing. const CAPTIONS = t({ en: { cover: 'A curlew on the mud, with reeds in front.', strip: 'A curlew and a redshank on the mud, with the reedbed beyond.', curlew: 'Adult at low water. The female’s bill is the longer.', redshank: 'Adult by a saltmarsh creek, on the red legs that give it its name.', reedling: 'Male on a reed stem. The female has a plain brown head.', warbler: 'Singing from the reeds, one foot on each stem.', }, es: { cover: 'Un zarapito en el fango, con carrizos delante.', strip: 'Un zarapito y un archibebe en el fango, y el carrizal al fondo.', curlew: 'Adulto en bajamar. La hembra tiene el pico más largo.', redshank: 'Adulto junto a un caño de la marisma, sobre las patas rojas que lo delatan.', reedling: 'Macho en un tallo de carrizo. La hembra tiene la cabeza parda.', warbler: 'Cantando en el carrizal, con una pata en cada tallo.', } }); // Every picture is a JPEG in assets/, cut to the shape of its frame, declared at its pixels. const drawing = (id, [w, h], more) => ({ id, typeId: 'plate', kind: 'bitmap', bitmap: { fileId: `${id}-${w}.jpg`, format: 'jpeg', width: w, height: h }, createdAt: 0, updatedAt: 0, altText: CAPTIONS[id], ...more }); // Plates stand where ::resource{id="…"} is; since 1.5 an inline figure keeps a line of space // below it too. const resources = [drawing('cover', [1152, 1536]), drawing('strip', [1536, 451]), ...['curlew', 'redshank', 'reedling', 'warbler'].map((id) => drawing(id, [1200, 600], { caption: CAPTIONS[id], placement: { position: 'here' } }))]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses, loaded before the first build. const FONTS = { Alegreya: ['400', '400i', '700'], 'Zilla Slab': ['600', '700'], // text, display 'Barlow Condensed': ['400', '600', '700'] }; // and labels // ─── 4 · Build & show ─────────────────────────────────────────────────────── await Promise.all(resources.map(({ bitmap: b }) => loadImage(b.fileId, asset(b.fileId)))); const doc = await buildDocumentWithFonts({ markdown, resources }, config(), kitFonts(FONTS)); showPages(doc, { title: BOOK });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
#Aggiungi un terzo ambiente
Un blocco prima della prima specie della barena aggiunge un terzo colore, con la sua pagina di parte, il verso dipinto e la riga d’indice, e la configurazione resta com’è. Togli anche la nota sotto l’indice, perché la terza fascia e le sue specie hanno bisogno di spazio.
+:::part{number="III" title="The \\ Saltmarsh" palette="band=#6b4a7a"}
+:::
+
+# Common Shelduck {latin="Tadorna tadorna" size="58–67 cm" status="Resident"}#Metti la specie di fronte alla pagina di parte
Senza l’interruzione verso il recto successivo, la prima specie comincia sul retro della pagina di parte, quindi non resta un verso bianco da dipingere per versoDesign.
- breakAfter: { parity: 'odd' },Errori frequenti
Errore frequente
parts.versoDesign richiede parts.breakAfter con parity 'odd'
Il retro di una pagina di parte viene dipinto solo quando parts.breakAfter { enabled: true, parity: 'odd' } lascia bianco quel verso. Pagine di parte →
Errore frequente
La pagina 1 è un recto: pianifica le pagine con i numeri fisici
La pagina 1 è una pagina destra e la pagina 2 il primo verso, quindi pianifica le doppie pagine con i numeri di pagina fisici: un’apertura su una pagina pari sta di fronte alla pagina dispari che la segue. Interruzioni di pagina e di colonna →
- Una parte ricolora il testo confrontando i valori: ogni colore del testo uguale al valore proprio di
bandcambia con la parte, anche se è collegato a un’altra voce o a nessuna. Riserva quell’esadecimale solo aband. - La linguetta stampa
{partNumber}, che è vuoto fuori da una parte, quindi una pagina di testo prima del primo:::partriceve una linguetta grigio ardesia vuota. Tieni ogni pagina di testo dentro una parte. - Le immagini e i campioni di colore in linea conservano i colori scritti in essi. Qui le tavole prendono i colori del loro ambiente da
HABITATnel codice, quindi un nuovo colore in un:::partva messo anche lì.
Crediti
- Ricetta
- Ignacio Ferro
- Testo
- Testo originale, CC BY 4.0
- Immagini
- The cover: a curlew on the estuary mud, reeds in front · Generated With Diffusion Models · originale
- The contents strip: a curlew and a redshank on the mud, the reedbed beyond · Generated With Diffusion Models · originale
- Plate 1: Eurasian curlew · Generated With Diffusion Models · originale
- Plate 2: common redshank · Generated With Diffusion Models · originale
- Plate 3: bearded reedling · Generated With Diffusion Models · originale
- Plate 4: common reed warbler · Generated With Diffusion Models · originale
- Font
- Alegreya (SIL OFL 1.1) · Zilla Slab (SIL OFL 1.1) · Barlow Condensed (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)

