En bref
Quelques pages d’Alice au pays des merveilles avec les notes d’un éditeur dans la marge. Chaque note se place à côté des lignes qu’elle explique, signalée par une lettre de couleur.
Ce que vous allez composer
Le début du chapitre VII d’Alice au pays des merveilles, le thé chez les fous, composé sur trois pages d’une édition annotée, dans un format de 156 × 234 mm. Le texte de Carroll court sur une seule colonne justifiée en Unna, environ 66 caractères par ligne. Un couloir de 33 mm du côté de la tranche accueille les huit gloses de l’éditeur, chacune à côté du paragraphe qui porte sa lettre : elles descendent à droite d’un recto et à gauche d’un verso. Les gloses sur les mots et les jeux de mots sont vertes, celles sur l’histoire rouges, et chaque lettre du texte prend la couleur de sa glose. Le chapitre s’ouvre sous une aquarelle de la table du thé vue d’en haut, suivie du numéro, du titre en Rozha One et d’un chapeau en italique avec une lettrine verte ; la note sur cette édition se tient dans la marge, à côté du chapeau.
Cette recette répond à
- Comment composer des notes marginales ou des gloses à côté du paragraphe qu’elles commentent ?
- Comment faire des notes de bas de page ?
- Comment faire une mise en page en colonne et demie, avec une colonne de texte large et une colonne latérale étroite ?
- Comment ajouter à une ouverture une ligne d’auteur, un chapeau ou une attaque avec lettrine ?
La réponse courte
const layout = {
layoutType: 'oneAndHalf', // one text column and a narrower side column
sideColumnPercent: 26, // of the 127 mm between the margins: a 33 mm channel
sideColumnRole: 'floats', // no text runs in it: it holds side boxes and figures
sideColumnSide: 'outer', // at the fore-edge, which mirrored margins move page by page
gutterWidth: mm(4.5), // the text column keeps 127 − 33 − 4.5 = 89.5 mm
};
// A gloss is :::callout{type="note" span="side" title="a · …"} in the Markdown.
// span="side" moves it out of the flow into the channel, level with the block after
// the fence, so the fence goes just before the paragraph that carries its letter.
// Glosses never float. One that meets the gloss above stacks under it. One that would
// run past the column's foot slides up, as far as the gloss above allows, until its
// foot sits on the foot; if it still does not fit, it waits for the next page
// (gotcha: side-box-starts-at-fence).
const gloss = (id, hue) => ({ id,
backgroundEnabled: false, // one device: a hairline over the gloss, in its colour
stripe: { enabled: true, side: 'top', width: pt(0.5), color: col(hue) },
padding: { top: mm(1.5), right: pt(0), bottom: pt(0), left: pt(0) },
// Cormorant SC draws lower case as small capitals: the title needs no textTransform.
titleStyle: { fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5),
letterSpacing: pt(0.3), color: col(hue), gap: mm(0.8) },
// Face and colours come from bodyText. A box body also inherits its 4 mm first-line
// indent, which a gloss sets back to 0.
body: { fontSize: pt(7.8), lineHeight: pt(10), textAlign: 'left',
firstLineIndent: pt(0) } });
const calloutStyles = [gloss('note', 'lawn'), gloss('context', 'jam')]; // words, history
Ingrédients
- Fonctionnalités
- Notes marginalesColonne marginale pour les flottantsColonne et demieMarges en miroirEncadrésExposants et indicesOuvertures composéesBandeau de chapitre sur toute la largeurTexte, filets et boîtes dans les maquettes de pageImages dans les maquettes de pageAncrer les éléments de maquetteLettrines dans les ouverturesAttributs de titreTitres courants et foliosTitres courants selon le rôle de la pageFigures et tableaux comme ressourcesPalette de couleurs sémantiqueCoupure optimale des lignes (Knuth–Plass)Pastilles dans le texte
- Utilise aussi
- Notes de bas de pageCouleur du papier
- Typographie
- Unna, Rozha One, Cormorant SC (SIL OFL 1.1)
- Ressources
tea-table-1560.jpg- The tea-table seen from above, a watercolour (Generated With Diffusion Models, original)
Préparation
#1 · Donnez la marge de tranche aux gloses
Le code est dans la réponse courte plus haut. Dans une mise en page à colonne et demie, sideColumnPercent: 26 donne à la colonne latérale 33 mm sur les 127 mm entre les marges, et le texte garde 89,5 mm après la gouttière de 4,5 mm. sideColumnRole: 'floats' tient Carroll hors de la colonne latérale, qui ne contient que ce qu’on y place avec span: 'side'. Une note de bas de page attend au pied de la colonne, sous un filet ; dans la marge, chaque glose se tient à hauteur de la ligne qu’elle explique, et les gloses ne prennent aucune ligne à la colonne de texte. Les deux styles de glose ne diffèrent que par la couleur du filet fin et du titre : le lecteur distingue une glose sur un mot d’une glose sur l’histoire avant même de la lire.
#2 · Mettez la page en miroir pour que le couloir suive la tranche
const PT = 25.4 / 72; // mm in a point
const [TRIM_W, TRIM_H] = [156, 234]; // mm
const [TOP, BOTTOM, INNER, OUTER] = [22, 22, 15, 14]; // mm: 38 lines of text
const page = { width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150,
backgroundColor: col('paper'),
// left is a recto's inner margin; mirror swaps the sides on a verso, and 'outer' moves
// the channel with them: right on a recto, left on a verso.
margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
mirror: true } };
const CONTENT_W = TRIM_W - INNER - OUTER; // 127 mm
const SIDE_W = CONTENT_W * layout.sideColumnPercent / 100; // 33 mm: the glosses' measure
const TEXT_W = CONTENT_W - SIDE_W - layout.gutterWidth.value;
Avec mirror: true, les marges s’inversent sur chaque verso, et sideColumnSide: 'outer' déplace le couloir avec elles : les gloses descendent à droite de la page 81 et de la page 83 et à gauche de la page 82. Des marges haute et basse de 22 mm laissent 190 mm, la place de 38 lignes de 14 pt, et chaque glose commence sur une ligne de la même grille.
#3 · Placez chaque glose dans une clôture avant le paragraphe qu’elle explique
const bodyText = { // justified and hyphenated (en-us) by default
// Copy-fitted to the 89.5 mm column: at 9.5 pt '“Have some wine,” the March Hare said in
// an encouraging tone.' fits one line. At 10.4 pt every measure from 77 to 93 mm left
// it a short last line: 'couraging tone.', 'aging tone.', 'ing tone.' or 'tone.'.
fontFamily: TEXT, fontSize: pt(9.5), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(4),
// The chapter's first paragraph is set flush, though the fences of glosses a and b stand
// between the heading and it: a box that leaves the flow is not the block after a heading.
indentAfterHeading: false,
// Unna's space is narrow, 0.22 em. At the default 0.6 the line breaker set 'Alice felt
// dreadfully puzzled. The Hatter’s remark seemed to have' with its spaces at 0.69.
minWordSpacing: 0.75,
// The dictionary divides the heroine's name, 'said Al-' | 'ice indignantly' on page 81.
// The list keeps it whole wherever it stands; every other word is divided as before.
hyphenation: { exceptions: ['Alice'] } };
Un encadré avec span="side" sur sa clôture :::callout quitte le flux et se tient dans le couloir à hauteur du bloc qui suit la clôture : chaque glose est donc écrite juste avant le paragraphe qui porte sa lettre, comme ci-dessous. Les gloses a et b vont à côté du premier paragraphe, ce qui place leurs clôtures entre le titre et ce paragraphe. Un encadré qui quitte le flux ne compte pas comme le bloc qui suit le titre : indentAfterHeading: false compose donc toujours ce paragraphe sans retrait. Le texte est calibré sur la colonne : en 9,5 pt, le paragraphe qui commence par « Have some wine, » tient sur une ligne de 89,5 mm, alors qu’en 10,4 pt chaque justification de 77 à 93 mm lui laissait une dernière ligne trop courte, comme « ing tone. » ou « tone. ». L’espace-mot d’Unna est étroit, 0,22 em, et avec le minWordSpacing par défaut de 0,6, une ligne de la page 83 voyait ses espaces réduites à 0,69 de cette largeur ; minWordSpacing: 0.75 exclut cette ligne. En page 81, le dictionnaire coupait le nom de l’héroïne, « Al- » et « ice » : ce paragraphe est donc placé dans :::paragraphs{style="whole"}, un style avec hyphenation: false ; son unique coupure est un trait d’union conditionnel (U+00AD) saisi dans « together ».
:::callout{type="context" span="side" title="c · Hair"}
Tenniel drew Alice with long, loose hair. …
:::
“Your hair wants cutting,”:chip[^c^]{style="context"} said the Hatter. …#4 · Imprimez chaque lettre dans la couleur de sa glose
// A letter in the text is a chip named after its gloss's type, :chip[^a^]{style="note"}:
// the Markdown has no mark for coloured text, and a chip style sets a colour. With no
// fill, outline, padding or gap the chip adds no width, so the lines break as they would
// with a plain ^a^.
const letter = (id, hue) => ({ id, backgroundEnabled: false, borderWidth: pt(0),
paddingX: pt(0), gap: pt(0), color: col(hue), bold: true });
const chipStyles = [letter('note', 'lawn'), letter('context', 'jam')];
Markdown n’a pas de marque pour le texte en couleur, mais un style de pastille règle une couleur de texte : chaque lettre est donc une pastille dont le style porte le même nom que le type de sa glose. :chip[^c^]{style="context"} imprime un c rouge, le rouge du filet et du titre de la glose c. Sans fond, contour, remplissage ni écart, la pastille n’ajoute aucune largeur, et chaque ligne se coupe là où elle se coupait avec un simple ^c^.
#5 · Ouvrez le chapitre sous la table du thé
const HEADPIECE = 72; // mm: the depth of the painting at the head of the page
const HEADNOTE = { size: 9.8, lead: LEAD }; // pt: the headnote keeps the body's leading
const CAPS = { text: 0.597, initial: 0.56 }; // cap heights, em: Unna and Rozha One
// The initial's size: its capital runs from the first line's cap height down to the
// last baseline it spans. At the default size it stops short of the first line's capitals.
const dropSize = (lines) => pt(((lines - 1) * HEADNOTE.lead + CAPS.text * HEADNOTE.size)
/ CAPS.initial);
const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' },
offset: { y: mm(y) }, size: { width } });
// The opener reserves the height of its lowest element, rounded up to the grid, and the
// heading's default bottom margin adds one blank line: with a four-line headnote the
// text starts on line 23. The kicker, the title and the headnote hang below the painting.
const opener = { enabled: true, slot: { elements: [
// An image element draws a resource without number or caption. It hangs from the
// trim's corner, over the top margin and both side margins.
{ kind: 'image', id: 'headpiece', resourceId: 'tea-table',
placement: { anchor: { to: 'page', edge: 'top-left' },
size: { width: mm(TRIM_W) } } },
// The numeral comes from the heading line,
// # A Mad Tea-Party {num="VII" headnote="…" …}: the excerpt stands alone, so
// {chapterNumber} would count its one heading and print 1.
{ kind: 'text', id: 'kicker', content: 'Chapter {attr.num}', fontFamily: LABEL,
fontWeight: 600, fontSize: pt(9), letterSpacing: pt(2), textTransform: 'uppercase',
color: col('jam'), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { y: mm(HEADPIECE - TOP + 8) } } }, // 8 mm under the painting's foot
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY,
fontSize: pt(40), lineHeight: 1.05, color: col('ink'), align: 'left', // wraps when longer
placement: below('kicker', 2, 'fill') },
// The drop cap opens the headnote, the editor's voice, italic and ragged; Carroll's
// text opens in the flow, justified.
{ kind: 'text', id: 'headnote', content: '{attr.headnote}', fontFamily: TEXT,
italic: true, fontSize: pt(HEADNOTE.size),
lineHeight: pt(HEADNOTE.lead), color: col('ink'),
align: 'left',
dropCap: { lines: 2, fontFamily: DISPLAY, fontSize: dropSize(2), color: col('lawn'),
gap: mm(1.5) },
placement: below('title', 5, mm(TEXT_W)) },
// The note on this edition stands in the channel, level with the headnote's top.
{ kind: 'text', id: 'edition-label', content: 'This edition', fontFamily: LABEL,
fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(0.3), color: col('muted'),
align: 'left', placement: { anchor: { to: '#headnote', edge: 'right-of' },
offset: { x: layout.gutterWidth }, size: { width: mm(SIDE_W) } } },
{ kind: 'text', id: 'edition', content: '{attr.source}', fontFamily: TEXT,
fontSize: pt(7.5), lineHeight: pt(10), color: col('muted'), align: 'left',
placement: { anchor: { to: '#edition-label', edge: 'below' }, offset: { y: mm(0.8) },
size: { width: mm(SIDE_W) } } },
] } };
L’emplacement de maquette du titre s’étend sur toute la largeur de la page pour que la peinture puisse atteindre le bord de coupe. Le surtitre, le titre et le chapeau s’accrochent sous la peinture, et l’ouverture réserve la place jusqu’au plus bas d’entre eux, arrondie à la grille ; la marge inférieure par défaut du titre ajoute une ligne blanche. Avec un chapeau de quatre lignes, le texte commence à la ligne 23, et une cinquième ligne le ferait passer à la ligne 24. Le numéro, le chapeau et la note sur cette édition viennent d’attributs de titre, {num="VII" headnote="…" source="…"}. Le numéro vient de là parce que {chapterNumber} imprimerait 1 dans un extrait qui est un document à part entière. La lettrine ouvre le chapeau de l’éditeur, composé en drapeau et en italique, et le texte de Carroll commence justifié dans le flux, sous lui. dropSize() dimensionne l’initiale pour que son sommet s’aligne sur les capitales de la première ligne du chapeau.
#6 · Colorez chaque glose selon ce qu’elle explique
const palette = {
ink: '#1f1b1a', // text: a warm near-black
lawn: '#1e6f6b', // glosses on words and jokes, the drop cap
jam: '#b23a48', // glosses on history, the kicker
muted: '#6e645b', // running heads, the note on this edition
paper: '#f8f3e6', // the page, and the tablecloth
};
// col(id): a colour linked to its palette entry, with the entry's hex beside the id.
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const entry = (id, hex, name = id) => ({ id, name, value: { hex, model: 'hex' } });
const colorPalette = [
...Object.entries(palette).map(([id, hex]) => entry(id, hex)),
// The engine's defaults link to 'main-color': aimed at the green, nothing prints blue.
entry('main-color', palette.lawn, 'lawn (defaults)'),
];
Le vert pelouse marque les gloses sur les mots, les dictons et les jeux de mots, et le rouge confiture les personnes, les dates et l’histoire du texte. Une nouvelle valeur pour jam dans le même objet palette recolore en une seule modification les gloses sur l’histoire, leurs lettres et le surtitre ; l’aquarelle qui les surmonte a été choisie pour ce même vert pelouse et ce même rouge confiture.
La recette complète
// ═══ Postext Cookbook · Nº 032 · Annotated classic with margin glosses ════════════ // https://postext.dev/en/cookbook/annotated-classic-glosses // Code: MIT · Text: Lewis Carroll (PD), glosses (CC BY 4.0) · Headpiece: diffusion models // Fonts: Unna, Rozha One, Cormorant SC (SIL OFL 1.1) · Needs postext ≥ 1.28.0 // The mad tea-party as an annotated edition: the text keeps to one column, and its glosses stand // in the outer margin beside the lines they explain, changing sides with the spread. import { buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en') const RECIPE = 'annotated-classic-glosses'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: a lawn green and a jam red on cream paper const palette = { ink: '#1f1b1a', // text: a warm near-black lawn: '#1e6f6b', // glosses on words and jokes, the drop cap jam: '#b23a48', // glosses on history, the kicker muted: '#6e645b', // running heads, the note on this edition paper: '#f8f3e6', // the page, and the tablecloth }; // col(id): a colour linked to its palette entry, with the entry's hex beside the id. const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const entry = (id, hex, name = id) => ({ id, name, value: { hex, model: 'hex' } }); const colorPalette = [ ...Object.entries(palette).map(([id, hex]) => entry(id, hex)), // The engine's defaults link to 'main-color': aimed at the green, nothing prints blue. entry('main-color', palette.lawn, 'lawn (defaults)'), ]; // #endregion const [TEXT, DISPLAY, LABEL] = ['Unna', 'Rozha One', 'Cormorant SC']; const LEAD = 14; // body leading in pt: the grid of every page // #region answer: a float-only channel at the fore-edge, and two gloss styles for it const layout = { layoutType: 'oneAndHalf', // one text column and a narrower side column sideColumnPercent: 26, // of the 127 mm between the margins: a 33 mm channel sideColumnRole: 'floats', // no text runs in it: it holds side boxes and figures sideColumnSide: 'outer', // at the fore-edge, which mirrored margins move page by page gutterWidth: mm(4.5), // the text column keeps 127 − 33 − 4.5 = 89.5 mm }; // A gloss is :::callout{type="note" span="side" title="a · …"} in the Markdown. // span="side" moves it out of the flow into the channel, level with the block after // the fence, so the fence goes just before the paragraph that carries its letter. // Glosses never float. One that meets the gloss above stacks under it. One that would // run past the column's foot slides up, as far as the gloss above allows, until its // foot sits on the foot; if it still does not fit, it waits for the next page // (gotcha: side-box-starts-at-fence). const gloss = (id, hue) => ({ id, backgroundEnabled: false, // one device: a hairline over the gloss, in its colour stripe: { enabled: true, side: 'top', width: pt(0.5), color: col(hue) }, padding: { top: mm(1.5), right: pt(0), bottom: pt(0), left: pt(0) }, // Cormorant SC draws lower case as small capitals: the title needs no textTransform. titleStyle: { fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(0.3), color: col(hue), gap: mm(0.8) }, // Face and colours come from bodyText. A box body also inherits its 4 mm first-line // indent, which a gloss sets back to 0. body: { fontSize: pt(7.8), lineHeight: pt(10), textAlign: 'left', firstLineIndent: pt(0) } }); const calloutStyles = [gloss('note', 'lawn'), gloss('context', 'jam')]; // words, history // #endregion // #region letters: the letter that calls each gloss, in the gloss's colour // A letter in the text is a chip named after its gloss's type, :chip[^a^]{style="note"}: // the Markdown has no mark for coloured text, and a chip style sets a colour. With no // fill, outline, padding or gap the chip adds no width, so the lines break as they would // with a plain ^a^. const letter = (id, hue) => ({ id, backgroundEnabled: false, borderWidth: pt(0), paddingX: pt(0), gap: pt(0), color: col(hue), bold: true }); const chipStyles = [letter('note', 'lawn'), letter('context', 'jam')]; // #endregion // #region page: a trade page with mirrored margins, so the channel is always at the fore-edge const PT = 25.4 / 72; // mm in a point const [TRIM_W, TRIM_H] = [156, 234]; // mm const [TOP, BOTTOM, INNER, OUTER] = [22, 22, 15, 14]; // mm: 38 lines of text const page = { width: mm(TRIM_W), height: mm(TRIM_H), dpi: 150, backgroundColor: col('paper'), // left is a recto's inner margin; mirror swaps the sides on a verso, and 'outer' moves // the channel with them: right on a recto, left on a verso. margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER), mirror: true } }; const CONTENT_W = TRIM_W - INNER - OUTER; // 127 mm const SIDE_W = CONTENT_W * layout.sideColumnPercent / 100; // 33 mm: the glosses' measure const TEXT_W = CONTENT_W - SIDE_W - layout.gutterWidth.value; // #endregion // #region text: book texture, and a first paragraph set flush const bodyText = { // justified and hyphenated (en-us) by default // Copy-fitted to the 89.5 mm column: at 9.5 pt '“Have some wine,” the March Hare said in // an encouraging tone.' fits one line. At 10.4 pt every measure from 77 to 93 mm left // it a short last line: 'couraging tone.', 'aging tone.', 'ing tone.' or 'tone.'. fontFamily: TEXT, fontSize: pt(9.5), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(4), // The chapter's first paragraph is set flush, though the fences of glosses a and b stand // between the heading and it: a box that leaves the flow is not the block after a heading. indentAfterHeading: false, // Unna's space is narrow, 0.22 em. At the default 0.6 the line breaker set 'Alice felt // dreadfully puzzled. The Hatter’s remark seemed to have' with its spaces at 0.69. minWordSpacing: 0.75, // The dictionary divides the heroine's name, 'said Al-' | 'ice indignantly' on page 81. // The list keeps it whole wherever it stands; every other word is divided as before. hyphenation: { exceptions: ['Alice'] } }; // #endregion // #region opener: a headpiece, the chapter's numeral and title, a headnote with a drop cap const HEADPIECE = 72; // mm: the depth of the painting at the head of the page const HEADNOTE = { size: 9.8, lead: LEAD }; // pt: the headnote keeps the body's leading const CAPS = { text: 0.597, initial: 0.56 }; // cap heights, em: Unna and Rozha One // The initial's size: its capital runs from the first line's cap height down to the // last baseline it spans. At the default size it stops short of the first line's capitals. const dropSize = (lines) => pt(((lines - 1) * HEADNOTE.lead + CAPS.text * HEADNOTE.size) / CAPS.initial); const below = (id, y, width) => ({ anchor: { to: `#${id}`, edge: 'below' }, offset: { y: mm(y) }, size: { width } }); // The opener reserves the height of its lowest element, rounded up to the grid, and the // heading's default bottom margin adds one blank line: with a four-line headnote the // text starts on line 23. The kicker, the title and the headnote hang below the painting. const opener = { enabled: true, slot: { elements: [ // An image element draws a resource without number or caption. It hangs from the // trim's corner, over the top margin and both side margins. { kind: 'image', id: 'headpiece', resourceId: 'tea-table', placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: mm(TRIM_W) } } }, // The numeral comes from the heading line, // # A Mad Tea-Party {num="VII" headnote="…" …}: the excerpt stands alone, so // {chapterNumber} would count its one heading and print 1. { kind: 'text', id: 'kicker', content: 'Chapter {attr.num}', fontFamily: LABEL, fontWeight: 600, fontSize: pt(9), letterSpacing: pt(2), textTransform: 'uppercase', color: col('jam'), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(HEADPIECE - TOP + 8) } } }, // 8 mm under the painting's foot { kind: 'text', id: 'title', content: '{titleText}', fontFamily: DISPLAY, fontSize: pt(40), lineHeight: 1.05, color: col('ink'), align: 'left', // wraps when longer placement: below('kicker', 2, 'fill') }, // The drop cap opens the headnote, the editor's voice, italic and ragged; Carroll's // text opens in the flow, justified. { kind: 'text', id: 'headnote', content: '{attr.headnote}', fontFamily: TEXT, italic: true, fontSize: pt(HEADNOTE.size), lineHeight: pt(HEADNOTE.lead), color: col('ink'), align: 'left', dropCap: { lines: 2, fontFamily: DISPLAY, fontSize: dropSize(2), color: col('lawn'), gap: mm(1.5) }, placement: below('title', 5, mm(TEXT_W)) }, // The note on this edition stands in the channel, level with the headnote's top. { kind: 'text', id: 'edition-label', content: 'This edition', fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(0.3), color: col('muted'), align: 'left', placement: { anchor: { to: '#headnote', edge: 'right-of' }, offset: { x: layout.gutterWidth }, size: { width: mm(SIDE_W) } } }, { kind: 'text', id: 'edition', content: '{attr.source}', fontFamily: TEXT, fontSize: pt(7.5), lineHeight: pt(10), color: col('muted'), align: 'left', placement: { anchor: { to: '#edition-label', edge: 'below' }, offset: { y: mm(0.8) }, size: { width: mm(SIDE_W) } } }, ] } }; // #endregion // The running heads: the book on the verso, the chapter on the recto, folios at the fore-edge. const HEAD = 14; // mm from the top trim to the heads' baseline const INSET = 8; // mm from the folio's outer edge to the running head's const DROP = 13; // mm from the bottom trim up to the foot of the drop folio's box // A design text's first baseline sits 0.96 em below the top of its box: the default // lineHeight, 1.2, times 0.8, where the baseline falls in the line box. const BASELINE = 1.2 * 0.8; const baseline = (size) => mm(HEAD - BASELINE * size * PT); const head = (id, parity, content, x, extra = {}) => ({ kind: 'text', id, parity, content, pages: 'body', fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(1.2), textTransform: 'uppercase', color: col('muted'), ...extra, placement: { anchor: { to: 'page', edge: parity === 'even' ? 'top-left' : 'top-right' }, offset: { x: mm(x), y: baseline(extra.fontSize?.value ?? 8.5) } } }); const folio = { fontFamily: TEXT, fontWeight: 700, fontSize: pt(9), letterSpacing: pt(0), color: col('ink') }; const header = { elements: [ head('verso-folio', 'even', '{pageNumber}', OUTER, folio), head('verso-title', 'even', '{title}', OUTER + INSET), head('recto-title', 'odd', '{chapterTitle}', -(OUTER + INSET)), head('recto-folio', 'odd', '{pageNumber}', -OUTER, folio), ] }; // The opener, a recto, carries a drop folio at the foot of its channel instead. const footer = { elements: [{ ...head('drop-folio', 'odd', '{pageNumber}', -OUTER, folio), pages: 'opener', placement: { anchor: { to: 'page', edge: 'bottom-right' }, offset: { x: mm(-OUTER), y: mm(-DROP) } } }] }; const config = () => ({ locale: 'en-us', // the chapter is in English: its hyphenation patterns, and the bundle's language colorPalette, page, layout, bodyText, calloutStyles, chipStyles, header, footer, headings: { fontFamily: DISPLAY, fontWeight: 400, color: col('ink'), // Rozha One has one weight levels: [ // 'odd' puts the opener on a recto; span 'page' lets its design cross the channel. { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' }, advancedDesign: opener }, ], }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Exemple Markdown · 119 lignes · content.en.md
title: "Alice’s Adventures in Wonderland" author: "Lewis Carroll" --- # A Mad Tea-Party {num="VII" headnote="The tea-party is not in the book Carroll wrote out by hand for Alice Liddell in 1864; he added it, with the Cheshire Cat, for the book of 1865. Letters in the text point to the glosses beside it: green for words and jokes, red for history." source="Text after Project Gutenberg eBook 11; notes CC BY 4.0. Set in Unna, Rozha One and Cormorant SC (SIL OFL)."} :::callout{type="note" span="side" title="a · The March Hare"} ‘Mad as a March hare’ is an old saying. Hares are shy, but in early spring they race about the fields and rear up on their hind legs to box. ::: :::callout{type="context" span="side" title="b · The Hatter"} ‘Mad as a hatter’ was a saying before Carroll used it. Hatters made felt from fur treated with mercury, and the fumes gave many of them tremors and fits of shyness and temper. Carroll never calls him the Mad Hatter. ::: There was a table set out under a tree in front of the house, and the March Hare:chip[^a^]{style="note"} and the Hatter:chip[^b^]{style="context"} were having tea at it: a Dormouse was sitting between them, fast asleep, and the other two were using it as a cushion, resting their elbows on it, and talking over its head. “Very uncomfortable for the Dormouse,” thought Alice; “only, as it’s asleep, I suppose it doesn’t mind.” The table was a large one, but the three were all crowded together at one corner of it: “No room! No room!” they cried out when they saw Alice coming. “There’s *plenty* of room!” said Alice indignantly, and she sat down in a large arm-chair at one end of the table. “Have some wine,” the March Hare said in an encouraging tone. Alice looked all round the table, but there was nothing on it but tea. “I don’t see any wine,” she remarked. “There isn’t any,” said the March Hare. “Then it wasn’t very civil of you to offer it,” said Alice angrily. “It wasn’t very civil of you to sit down without being invited,” said the March Hare. “I didn’t know it was *your* table,” said Alice; “it’s laid for a great many more than three.” :::callout{type="context" span="side" title="c · Hair"} Tenniel drew Alice with long, loose hair. Alice Liddell, in Carroll’s photographs of her, wears hers short and dark, with a fringe. ::: “Your hair wants cutting,”:chip[^c^]{style="context"} said the Hatter. He had been looking at Alice for some time with great curiosity, and this was his first speech. “You should learn not to make personal remarks,” Alice said with some severity; “it’s very rude.” :::callout{type="context" span="side" title="d · The riddle"} Carroll made up the riddle without an answer. So many readers asked for one that in a preface of 1896 he offered this: ‘Because it can produce a few notes, tho they are very flat; and it is nevar put with the wrong end in front!’ Later printings changed *nevar*, raven spelt backwards, to *never*, and the joke was lost. ::: The Hatter opened his eyes very wide on hearing this; but all he *said* was, “Why is a raven like a writing-desk?”:chip[^d^]{style="context"} “Come, we shall have some fun now!” thought Alice. “I’m glad they’ve begun asking riddles.—I believe I can guess that,” she added aloud. “Do you mean that you think you can find out the answer to it?” said the March Hare. “Exactly so,” said Alice. “Then you should say what you mean,” the March Hare went on. “I do,” Alice hastily replied; “at least—at least I mean what I say—that’s the same thing, you know.” :::callout{type="note" span="side" title="e · I see what I eat"} The Hatter has the logic right. Turn a statement round and you get its converse, which can be false when the statement is true. Carroll, as Charles Dodgson, taught mathematics at Christ Church, Oxford, and wrote two books on logic. ::: “Not the same thing a bit!” said the Hatter. “You might just as well say that ‘I see what I eat’ is the same thing as ‘I eat what I see’!”:chip[^e^]{style="note"} “You might just as well say,” added the March Hare, “that ‘I like what I get’ is the same thing as ‘I get what I like’!” “You might just as well say,” added the Dormouse, who seemed to be talking in his sleep, “that ‘I breathe when I sleep’ is the same thing as ‘I sleep when I breathe’!” “It *is* the same thing with you,” said the Hatter, and here the conversation dropped, and the party sat silent for a minute, while Alice thought over all she could remember about ravens and writing-desks, which wasn’t much. The Hatter was the first to break the silence. “What day of the month is it?” he said, turning to Alice: he had taken his watch out of his pocket, and was looking at it uneasily, shaking it every now and then, and holding it to his ear. :::callout{type="context" span="side" title="f · The fourth"} Of May: Alice Liddell was born on 4 May 1852. ::: Alice considered a little, and then said “The fourth.”:chip[^f^]{style="context"} “Two days wrong!” sighed the Hatter. “I told you butter wouldn’t suit the works!” he added looking angrily at the March Hare. :::callout{type="note" span="side" title="g · The best butter"} Grocers sold butter by grade, and ‘best’ fetched the highest price. The March Hare defends its quality, which was never the trouble. ::: “It was the *best* butter,”:chip[^g^]{style="note"} the March Hare meekly replied. “Yes, but some crumbs must have got in as well,” the Hatter grumbled: “you shouldn’t have put it in with the bread-knife.” The March Hare took the watch and looked at it gloomily: then he dipped it into his cup of tea, and looked at it again: but he could think of nothing better to say than his first remark, “It was the *best* butter, you know.” Alice had been looking over his shoulder with some curiosity. “What a funny watch!” she remarked. “It tells the day of the month, and doesn’t tell what o’clock it is!” “Why should it?” muttered the Hatter. “Does *your* watch tell you what year it is?” “Of course not,” Alice replied very readily: “but that’s because it stays the same year for such a long time together.” “Which is just the case with *mine*,” said the Hatter. Alice felt dreadfully puzzled. The Hatter’s remark seemed to have no sort of meaning in it, and yet it was certainly English. “I don’t quite understand you,” she said, as politely as she could. “The Dormouse is asleep again,” said the Hatter, and he poured a little hot tea upon its nose. :::callout{type="note" span="side" title="h · The Dormouse"} Hazel dormice sleep through the day and hibernate for half the year, from autumn to spring. The name is often traced to the French *dormir*, to sleep. ::: The Dormouse:chip[^h^]{style="note"} shook its head impatiently, and said, without opening its eyes, “Of course, of course; just what I was going to remark myself.” “Have you guessed the riddle yet?” the Hatter said, turning to Alice again. “No, I give it up,” Alice replied: “what’s the answer?” “I haven’t the slightest idea,” said the Hatter. “Nor I,” said the March Hare. Alice sighed wearily. “I think you might do something better with the time,” she said, “than waste it in asking riddles that have no answers.” “If you knew Time as well as I do,” said the Hatter, “you wouldn’t talk about wasting *it*. It’s *him*.” “I don’t know what you mean,” said Alice. “Of course you don’t!” the Hatter said, tossing his head contemptuously. “I dare say you never even spoke to Time!”`; // content.<lang>.md, inlined by the Cookbook // The headpiece is a resource that only the opener's design draws: never cited, never placed. // It is a watercolour, a JPEG in assets/ cut to the trim's 156 × 72 mm, declared at its pixels. const resources = [{ id: 'tea-table', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'tea-table-1560.jpg', format: 'jpeg', width: 1560, height: 720 }, altText: 'The tea-table under a tree, seen from above: a long white cloth laid with cups, a red ' + 'teapot, bread and butter and a watch on its chain; the Dormouse asleep in a teapot at one ' + 'end and a red arm-chair at the other.' }]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces, loaded before the build Unna: ['400', '400i', '700'], // 700: the folios and the gloss letters 'Rozha One': ['400'], 'Cormorant SC': ['600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadImage('tea-table-1560.jpg', asset('tea-table-1560.jpg')); const continuation = { pageNumbering: { startAt: 81 } }; // chapter VII of a book: an odd folio const doc = await buildDocumentWithFonts({ markdown, resources, continuation }, config(), kitFonts(FONTS)); showPages(doc, { title: 'Annotated classic with margin glosses' });Kit · core, fonts, viewer, images : 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
#Gardez les gloses à droite de chaque page
Pour une édition lue page par page à l’écran, désactivez le miroir : 'outer' désigne alors le bord droit de chaque page, et les titres courants gardent leur place, puisqu’ils sont ancrés au bord de coupe.
- mirror: true } };
+ mirror: false } };#Renvoyez les notes longues au pied de la page
Écrivez une note trop longue pour la marge en note de bas de page, [^1] dans le texte et sa définition [^1]: sous le paragraphe : Postext la compose au pied de la colonne de texte, sur 89,5 mm de large sous un filet court, et les gloses gardent le couloir. Notes au pied de la colonne qui les appelle met ces notes en forme.
Erreurs fréquentes
Erreur fréquente
Un encadré latéral commence au niveau du bloc qui suit sa clôture
Dans postext 1.4.1, un encadré span: 'side' se place dans la colonne latérale à la hauteur atteinte par le texte à sa clôture, sur la ligne de grille suivante, et sous tout encadré déjà présent. Placez une glose juste avant le paragraphe qu’elle commente : placée après, elle commence en face du paragraphe suivant. Un encadré qui dépasserait le pied de la colonne remonte jusqu’à ce que son pied repose sur celui de la colonne, autant que le permet l’encadré au-dessus ; s’il ne tient toujours pas, il attend la colonne latérale de la page suivante. Notes marginales →
- Les gloses s’empilent dans l’ordre du Markdown, l’une sous l’autre : une glose longue repousse donc la suivante vers le bas. En page 81, les deux lettres tombent dans la deuxième ligne du premier paragraphe : la glose a commence à hauteur du paragraphe et la glose b sous elle, cinq lignes sous sa lettre. Là où les lettres se suivent de près, faites des gloses courtes.
Crédits
- Recette
- Ignacio Ferro
- Texte
- Alice’s Adventures in Wonderland (1865), the opening of chapter VII, “A Mad Tea-Party” · Lewis Carroll · domaine public
- The headnote, the eight glosses and the note on this edition · Ignacio Ferro · CC BY 4.0
- Images
- The tea-table seen from above, a watercolour · Generated With Diffusion Models · original
- Polices
- Unna (SIL OFL 1.1) · Rozha One (SIL OFL 1.1) · Cormorant SC (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)


