En bref
Un guide de poche des oiseaux d’estuaire en deux parties, l’une brun vase, l’autre vert roseau. Il montre comment un seul réglage de couleur par partie teinte sa page d’ouverture, ses onglets, ses numéros de page et sa ligne de sommaire.
Ce que vous allez composer
Birds of the Estuary est un guide de terrain de poche en deux parties, une par milieu. Les vasières s’impriment en brun vase et les roselières en vert roseau, et chaque couleur s’écrit une seule fois, dans le :::part qui ouvre son milieu. Chaque page de partie est un aplat à fond perdu avec un chiffre romain de 150 pt et les espèces du milieu listées en crème. Le verso du feuillet est peint assorti, avec l’onglet de la partie en réserve sur la tranche. Sur les pages d’espèces, l’onglet, le folio, le surtitre, le filet sous le nom latin, les signes distinctifs en gras, les puces et l’étiquette de la planche prennent la couleur de la partie. La table des matières s’ouvre sous un bandeau d’estuaire et donne à chaque partie un bandeau à sa couleur, avec le numéro de page de sa page de partie. La couverture, le bandeau et les quatre planches sont des peintures générées par des modèles de diffusion, chargées en images JPEG ; la palette d’une partie n’atteint pas les images, qui gardent leurs propres couleurs.
Cette recette répond à
- Comment diviser un livre en parties ou en sections, chacune avec sa couleur et sa page de partie ?
- Comment masquer les titres courants sur les ouvertures et les pages blanches, ou peindre un verso blanc dans la couleur de la partie ?
- Comment ajouter une table des matières qui se met à jour seule (points de conduite, numéros de page, auteurs, lignes de partie) ?
- Comment colorer les termes clés (en gras ou en italique) dans le texte ou dans les encadrés ?
La réponse courte
// 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) } },
};
Ingrédients
- Fonctionnalités
- Couleurs par partiePages de partieTable des matièresPalette de couleurs sémantiqueGras, italique et leurs couleursBandeaux à fond perdu et ongletsTitres courants selon le rôle de la pageTitres courants et foliosOuvertures composéesAttributs de titreTitres numérotésStyles de titreChapitres non numérotésSauts de ligne dans les titresCouvertures, pages de titre et colophonsImages dans les maquettes de pageTypes de ressource personnalisésDes figures exactement iciEncadrés et badges épinglésSauts de page et de colonne
- Typographie
- Alegreya, Zilla Slab, Barlow Condensed (SIL OFL 1.1)
- Ressources
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, original)
- The contents strip: a curlew and a redshank on the mud, the reedbed beyond (Generated With Diffusion Models, original)
- Plate 1: Eurasian curlew (Generated With Diffusion Models, original)
- Plate 2: common redshank (Generated With Diffusion Models, original)
- Plate 3: bearded reedling (Generated With Diffusion Models, original)
- Plate 4: common reed warbler (Generated With Diffusion Models, original)
Préparation
#1 · Une entrée de palette que les parties remplacent
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' } },
];
Une partie remplace des entrées de palette par leur identifiant : tout ce qui doit changer avec le milieu est donc lié à une seule entrée, band. Sa propre valeur, un gris ardoise, ne s’imprime qu’hors des parties (le surtitre de la couverture et le titre de la note sous la table des matières), et aucune autre couleur de la configuration n’a le même code hexadécimal. col() donne à chaque couleur son paletteId, et c’est cet identifiant que suit le remplacement d’une partie, dans les styles de texte comme dans les éléments de maquette.
#2 · Une page de partie, son verso et sa liste
Le code de cette étape est la réponse courte ci-dessus. Un :::part ouvre une page de partie sur un recto, et parts.design la met en page sur tout le format. Le Markdown entre ses délimiteurs commence à 125 mm du bord supérieur, sous le titre : quelques lignes sur le milieu dans le style de paragraphe habitat, puis la liste de ses espèces en bodyStyle. versoDesign peint le verso du feuillet (l’aplat et l’onglet en réserve) seulement quand cette page est blanche, et breakAfter: { parity: 'odd' } la garde blanche en renvoyant la première espèce au recto suivant. {numberRoman} imprime le number="I" de la partie en chiffre romain ("1" imprimerait le même I), tandis que les espèces sont numérotées de 1 à 4 en chiffres arabes. Le \\ du titre coupe la ligne sur la page de partie seulement ; la table des matières et les titres courants composent le titre sur une ligne.
#3 · Les accents du texte suivent la partie
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) };
D’un :::part au suivant, toute couleur de texte égale à la valeur propre de band prend la valeur de la partie (le conteneur :::part). Ici, ce sont les signes distinctifs en gras, les puces et les étiquettes des planches. Le texte courant et l’italique restent à l’encre. Les planches forment un type de ressource à part, étiqueté Plate et jamais remis à zéro : elles comptent de 1 à 4 au même rythme que les espèces.
#4 · L’ouverture d’espèce
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’ouverture d’espèce imprime trois attributs du titre : status dans le surtitre après le numéro, size à droite du surtitre et latin sous le nom. Le surtitre et le filet sous le nom latin sont liés à band : une seule maquette sert aux deux milieux. Le titre ne fixe pas de saut de page à lui : chaque espèce commence après un :::pagebreak, ou après le saut qui suit la partie, si bien que sa page compte comme page de texte et imprime les titres courants. La taille est composée sans les capitales de l’étiquette, parce que cm est un symbole d’unité.
#5 · Un bandeau par partie dans la table des matières
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 donne une ligne à chaque :::part et y met en page toc.parts.design avec le numéro, le titre, la page de partie et la palette de cette partie (table des matières) : le même encadré s’imprime en brun pour les vasières et en vert pour les roselières. Chaque ligne d’espèce sous un bandeau montre son numéro, des points de conduite et sa page, avec le nom latin tiré de l’attribut latin à la ligne suivante.
#6 · La partie dans les titres courants et l’onglet
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
] };
Avec pages: 'body', les titres courants ne s’impriment que sur les pages de texte. Les pages de partie sont des pages part et leurs versos peints des pages blank ; la couverture et la table des matières s’ouvrent chacune sur un titre sur toute la largeur de la page, ce qui en fait des ouvertures. Le titre courant du recto imprime {partTitle} sur une ligne, et l’onglet sur la tranche imprime {partNumber} sur un encadré dont le fond est lié à band.
La recette complète
// ═══ 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`---Exemple Markdown · 95 lignes · 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 : identique dans toutes les recettes · 270 lignes
// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · 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 ───────────────────────────────────────────────────────────────────────
Le script.js composé s’exécute tel quel : collez-le dans le script de module de n’importe quelle page, ou ouvrez la recette sur CodePen. Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)
Variantes
#Ajouter un troisième milieu
Un bloc placé avant la première espèce des prés salés ajoute une troisième couleur, avec sa page de partie, son verso peint et sa ligne de sommaire, et la configuration ne change pas. Supprimez aussi la note sous la table des matières, car le troisième bandeau et ses espèces ont besoin de la place.
+:::part{number="III" title="The \\ Saltmarsh" palette="band=#6b4a7a"}
+:::
+
+# Common Shelduck {latin="Tadorna tadorna" size="58–67 cm" status="Resident"}#Mettre l’espèce en regard de la page de partie
Sans le saut au recto suivant, la première espèce commence au verso de la page de partie : il ne reste aucun verso blanc à peindre pour versoDesign.
- breakAfter: { parity: 'odd' },Erreurs fréquentes
Erreur fréquente
parts.versoDesign demande parts.breakAfter avec la parité 'odd'
Le verso d’une page de partie n’est peint que lorsque parts.breakAfter { enabled: true, parity: 'odd' } laisse ce verso blanc. Pages de partie →
Erreur fréquente
La page 1 est un recto : planifiez les pages avec les numéros physiques
La page 1 est une page de droite et la page 2 le premier verso : planifiez donc les doubles pages avec les numéros de page physiques. Une ouverture sur une page paire fait face à la page impaire qui la suit. Sauts de page et de colonne →
- Une partie recolore le texte en comparant des valeurs : toute couleur de texte égale à la valeur propre de
bandchange avec la partie, même liée à une autre entrée ou à aucune. Réservez ce code hexadécimal àband. - L’onglet imprime
{partNumber}, vide hors d’une partie : une page de texte placée avant le premier:::partreçoit un onglet ardoise vide. Gardez chaque page de texte à l’intérieur d’une partie. - Les images et les pastilles de couleur en ligne gardent les couleurs écrites en elles. Les planches tirent ici les couleurs de leur milieu de
HABITATdans le code : une nouvelle couleur dans un:::partdoit y être ajoutée aussi.
Crédits
- Recette
- Ignacio Ferro
- Texte
- Texte original, CC BY 4.0
- Images
- The cover: a curlew on the estuary mud, reeds in front · Generated With Diffusion Models · original
- The contents strip: a curlew and a redshank on the mud, the reedbed beyond · Generated With Diffusion Models · original
- Plate 1: Eurasian curlew · Generated With Diffusion Models · original
- Plate 2: common redshank · Generated With Diffusion Models · original
- Plate 3: bearded reedling · Generated With Diffusion Models · original
- Plate 4: common reed warbler · Generated With Diffusion Models · original
- Polices
- Alegreya (SIL OFL 1.1) · Zilla Slab (SIL OFL 1.1) · Barlow Condensed (SIL OFL 1.1)
Modifier ce texte ↗ (s’ouvre dans un nouvel onglet)Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)

