Saltar al contingut principal
Recepta número 66

Receptari · Capítol 3 · Títols i obertures

Capítols de conte amb vinyetes i sumaris

Un sol estil de títol obre tots els capítols; cada títol anomena en atributs la seva vinyeta i el seu sumari en cursiva, que pinta l'obertura.

En aquesta pàgina
Sortida
Canvas
Nivell
Intermedi
Postext
Provada amb Postext 1.8.0
Requereix ≥ 1.8.0
Llicència
Actualitzada el 28 de set. del 2026
Codi MIT · Text CC BY 4.0

pp. 6–7 · 1–2 de 5

  • Mostra en castellà: encara no hi ha edició en català
  • Format 190 × 240 mm
  • 1 columna
  • Averia Serif Libre 13/19
  • Fredericka the Great
  • Quicksand
  • 5 pàgines
  • Nivell
  • Postext 1.8.0
  • Compost en 195 ms
  • 167 línies de codi

En poques paraules

Les primeres pàgines d'una edició infantil italiana de Pinotxo. Cada capítol comença amb una il·lustració pròpia en aquarel·la i un breu resum del que hi passa.

Què compondràs

Les primeres pàgines d'una edició infantil italiana de Le avventure di Pinocchio, de Collodi, compostes en Averia Serif Libre de 13/19 pt, en format de 190 × 240 mm. Davant del capítol I, a la pàgina parella, va una portada il·lustrada: una aquarel·la dels turons toscans, on un carro estirat per un ase porta llenya al taller d'un fuster, per sobre del títol en Fredericka the Great. Cada capítol comença sota una vinyeta en aquarel·la que es fon amb el paper (mestre Ciliegia amb la destral enlaire sobre el tronc que parla al capítol I, Geppetto amb la seva perruca groga rebent el tronc al II) i continua amb la línia del capítol i el sumari de Collodi en cursiva centrada. Les quatre imatges es van generar amb models de difusió a partir d'unes mateixes indicacions d'estil i la paleta de la pàgina. Només porten capçalera les pàgines 8 i 9, totes dues sense obertura. Els folis van en discos color fusta, i el capítol I acaba amb dues cireres sense peu ni número.

Aquesta recepta respon a

  • Com dono a cada capítol la seva pròpia foto, el seu color o una variant d'obertura?
  • Com componc il·lustracions sense numerar: ornaments, vinyetes, logotips?
  • Com afegeixo a una obertura una línia d'autor, una entradeta o un primer paràgraf amb caplletra?
  • Com amago les capçaleres a les obertures i a les pàgines en blanc, o pinto una pàgina parella en blanc amb el color de la part?

La resposta curta

script.js · línies 45–67al codi complet
// # Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia…"}
// resourceId takes placeholders like a design text: '{attr.vignetta}' draws the resource the
// heading names, so every chapter shares one style.
const VIGNETTE = { width: 84, height: 56 }; // mm, centred at the head of the text block
const SUMMARY = 124; // mm: the summary's measure, narrower than the text's
const opener = { enabled: true,
  // A floor for the height the opener reserves, in whole lines, so the text under chapter I's
  // two-line summary starts on the same grid line as the text under chapter II's three lines.
  minHeight: pt(14 * LEAD),
  slot: { elements: [
    { kind: 'image', id: 'vignette', resourceId: '{attr.vignetta}',
      placement: at('container', 'top', 0, 0,
        { width: mm(VIGNETTE.width), height: mm(VIGNETTE.height) }) },
    // Placed from the container, not from #vignette: images do not count in the height an
    // opener reserves (gotcha: opener-image-no-reserve), the texts under them do.
    text('chapter', '{titleText}', DISPLAY, 24, at('container', 'top-left', trail(2.4),
      VIGNETTE.height + 6, { width: mm(MEASURE) }), { ...caps(2.4), lineHeight: 1 }),
    text('summary', '{attr.summary}', TEXT, 12.5, at('#chapter', 'below',
      (MEASURE - SUMMARY) / 2 - trail(2.4), 5, { width: mm(SUMMARY) }),
    { italic: true, lineHeight: 1.38 }), // a multiple (gotcha: design-lineheight-multiple)
  ] },
};
const chapter = { id: 'capitolo', advancedDesign: opener };

Ingredients

Tipografia
Averia Serif Libre, Fredericka the Great, Quicksand (SIL OFL 1.1)
Recursos
  • ceppo-1200.jpg
  • ciliegie-600.jpg
  • frontespizio-1400.jpg
  • polenta-1200.jpg
  • The frontispiece: Tuscan hills, a hill village and a donkey cart of firewood on its way to the carpenter’s workshop (Generated With Diffusion Models, original)
  • Chapter I’s vignette: Maestro Ciliegia, axe raised over the talking log (Generated With Diffusion Models, original)
  • Chapter II’s vignette: Geppetto in his yellow wig receives the log, beside a pot of polenta (Generated With Diffusion Models, original)
  • The cherry tailpiece (Generated With Diffusion Models, original)

Elaboració

#1 · Un sol estil de títol, la vinyeta a cada títol

El codi d'aquest pas és a la resposta curta. El resourceId d'un element d'imatge admet els mateixos marcadors que un text de disseny, així que '{attr.vignetta}' dibuixa el recurs que anomena el títol, igual que {attr.summary} imprimeix el seu sumari. Els dos capítols comparteixen l'estil capitolo i anomenen la seva imatge a la línia de títol: # Capitolo II {style="capitolo" vignetta="polenta" summary="…"}. Un tercer capítol necessita un recurs i un títol més, no un altre estil (elements d'imatge). La línia del capítol es col·loca des del cap del contenidor, 6 mm per sota de la caixa de la vinyeta, en lloc d'ancorar-se a #vignette. Una imatge no reserva alçada, de manera que els textos ancorats a ella es mesuren com si el dibuix no hi fos i, sense minHeight, el text del capítol I començaria a 62 mm de la vora superior de la pàgina, dins del dibuix. Amb minHeight com a mínim de 14 línies, el text del capítol I comença a la mateixa línia que el del II, el sumari del qual ocupa una línia més; sense aquest mínim, el del capítol I comença 6,7 mm més amunt (span i disseny avançat).

#2 · Carrega les il·lustracions com a recursos de mapa de bits

script.js · línies 126–137al codi complet
const PICTURES = { // id: [file, width, height, alt text in the book's language]
  frontespizio: ['frontespizio-1400.jpg', 1400, 1105, 'Colline toscane con cipressi e un borgo; '
    + 'un contadino guida un asinello con un carretto di legna verso la bottega di un falegname.'],
  ceppo: ['ceppo-1200.jpg', 1200, 800, 'Maestro Ciliegia, con la scure alzata sopra un pezzo di '
    + 'legno sul banco, si guarda intorno spaventato.'],
  polenta: ['polenta-1200.jpg', 1200, 800, 'Geppetto, con la parrucca gialla, riceve il pezzo di '
    + 'legno da Maestro Ciliegia; accanto, un paiolo di polenta fumante.'],
  ciliegie: ['ciliegie-600.jpg', 600, 626, 'Due ciliegie rosse su un picciolo, con una foglia.'],
};
const resources = Object.entries(PICTURES).map(([id, [fileId, w, h, altText]]) => ({
  id, typeId: 'fregio', kind: 'bitmap', createdAt: 0, updatedAt: 0, altText,
  bitmap: { fileId, format: 'jpeg', width: w, height: h } }));

Cada imatge és un JPEG de la carpeta assets/ de la recepta, declarat amb la seva mida en píxels: la composició només llegeix aquestes mides, i el llenç pinta el que loadImage() hagi registrat amb cada identificador de fitxer quan dibuixa la pàgina. Les vinyetes i les cireres es van generar sobre blanc i després es van multiplicar pel color del paper, #fffaf0, de manera que els seus marges surten del color de la pàgina i les imatges no tenen una vora visible. La portada està retallada a 190:150, la forma del seu camp, perquè la imatge ajustada l'ompli sense una franja de paper. El paisatge i les vinyetes els pinten elements d'imatge dels dissenys; les cireres, la línia ::resource{id="ciliegie"} del Markdown.

#3 · Fes de la portada un estil de títol

script.js · línies 71–89al codi complet
const SKY = 150; // mm from the top edge: the foot of the painted field
// # Pinocchio {style="frontespizio" kicker="Le avventure di"}; subtitle and author come from
// the frontmatter. Every element hangs from the page, so the field runs off three edges.
const frontispiece = {
  id: 'frontespizio',
  span: 'page', // or the field is cut at the top of the text block (gotcha: opener-clipped-at-top)
  footer: { elements: [] }, // no folio; the running heads already skip openers
  advancedDesign: { enabled: true, slot: { elements: [ // array order is paint order
    { kind: 'image', id: 'landscape', resourceId: 'frontespizio', // cut to the field's 190:150
      placement: at('page', 'top-left', 0, 0, { width: 'fill', height: mm(SKY) }) },
    text('kicker', '{attr.kicker}', LABEL, 11, at('page', 'top', trail(2.6), SKY + 11),
      { ...caps(2.6), fontWeight: 700 }),
    text('title', '{titleText}', DISPLAY, 70, at('page', 'top', 0, SKY + 17),
      { lineHeight: 1, color: col('bark') }),
    text('subtitle', '{subtitle}', TEXT, 15, at('page', 'top', 0, SKY + 44), { italic: true }),
    text('author', '{author}', LABEL, 9, at('page', 'top', trail(2), SKY + 58),
      { ...caps(2), fontWeight: 600 }),
  ] } },
};

La portada és el primer títol del Markdown, # Pinocchio {style="frontespizio" kicker="Le avventure di"}, i el seu estil la pinta sencera: el paisatge, de 150 mm d'alçada des de la vora superior de la pàgina, i després l'avanttítol de l'atribut, el títol i, des de les metadades del principi del fitxer, el subtítol i l'autor. L'estil porta span: 'page' encara que el llibre tingui una sola columna, perquè un disseny que es queda a la columna es retalla al cap de la caixa de text, i els 22 mm superiors de la il·lustració sortirien del color del paper. El footer buit treu el disc del foli que porten les obertures de capítol, i pages: 'body' ja deixa les capçaleres fora d'aquesta pàgina (estils d'encapçalament).

#4 · Capçaleres a les pàgines de text, folis en discos

script.js · línies 93–114al codi complet
// The excerpt opens on book page 6, a verso, so the title page faces chapter I.
const continuation = { pageIndexOffset: 5, pageNumbering: { startAt: 6 } };
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre sits off the page's centre
const head = (id, content, parity, x) => text(id, content, LABEL, 8,
  at('page', 'top', x + trail(1.8), 12), { ...caps(1.8), fontWeight: 600, parity, pages: 'body' });
const DISC = 7.4; // mm: the folio's disc
// A fixed square with a radius of half its side is a circle; the figures sit in its middle.
// lineHeight 1.17: the baseline falls at 0.8 of the line, 0.94 em down, so Quicksand's
// figures (0.70 em tall) leave 0.23 em above and below them, and 'middle' centres that line.
const disc = (id, parity, pages, edge, x) => text(id, '{pageNumber}', LABEL, 8.5,
  at('page', edge, x, -(BOTTOM - DISC) / 2, { width: mm(DISC), height: mm(DISC) }),
  { fontWeight: 700, color: col('paper'), lineHeight: 1.17, verticalAlign: 'middle', parity,
    pages, box: { backgroundColor: col('wood'), borderRadius: mm(DISC / 2) } });
const header = { elements: [
  head('verso-title', '{title}', 'even', -SHIFT), // the book on the left-hand page
  head('recto-chapter', '{chapterTitle}', 'odd', SHIFT), // the chapter on the right
] };
const footer = { elements: [
  disc('verso-folio', 'even', 'body', 'bottom-left', OUTER),
  disc('recto-folio', 'odd', 'body', 'bottom-right', -OUTER),
  disc('opener-folio', 'all', 'opener', 'bottom', 0), // a chapter opens on either side
] };

continuation numera l'extracte des de la pàgina 6 del llibre, que és parella, així que la portada queda davant del capítol I i el primer foli imprès és el 7. pages: 'body' deixa les capçaleres fora de la portada i de les dues obertures, de manera que només surten a les pàgines 8 i 9: el títol del llibre a la parella i el del capítol a la senar (elements de text). El foli és un text de disseny dins d'un quadrat fix de 7,4 mm les cantonades del qual tenen un radi de mig costat, és a dir, un cercle; verticalAlign: 'middle' i l'alçada de línia que es calcula al comentari hi centren les xifres. Les xifres, color paper, tenen un contrast de 4,98:1 sobre la fusta. trail() desplaça cada rètol en majúscules espaiades a la dreta la meitat del seu espaiat, perquè la 1.4.1 centra una línia espaiada com si l'espai que segueix l'última lletra en formés part.

#5 · Tanca el capítol amb un cul de llàntia sense número

script.js · línies 118–122al codi complet
// No caption prefix and no caption, so the cherries that close chapter I print with no
// label or number. The pictures the design slots paint are of this type too.
const resourceTypes = [{ id: 'fregio', name: 'Fregio', namePlural: 'Fregi', shortLabel: 'Fregio',
  captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal',
  defaultPlacement: { position: 'here', width: 0.16, align: 'center' } }];

Un tipus de recurs imprimeix el seu captionPrefix i el seu número davant del peu. Amb el prefix buit i sense peu, sota les cireres no surt res; amb captionPrefix: 'Fregio', el mateix recurs porta a sota Fregio 1. i creix 10 mm (tipus de recurs). El defaultPlacement del tipus col·loca cada recurs on hi ha la seva línia ::resource, centrat i amb un sisè de la mesura d'amplada, així que al Markdown n'hi ha prou amb ::resource{id="ciliegie"} després de l'últim paràgraf del capítol. Les altres imatges comparteixen el tipus, però cap :ref ni ::resource no les anomena, així que mai no es col·loquen ni es compten.

La recepta completa

Sandbox
// ═══ Postext Cookbook · Nº 066 · Storybook chapters with vignettes and summaries ═══
// https://postext.dev/en/cookbook/storybook-chapter-vignettes
// Code: MIT · Text: C. Collodi, 1883 (PD, Gutenberg #52484) · Pictures: diffusion models
// Fonts: Averia Serif Libre, Fredericka the Great, Quicksand (SIL OFL 1.1) · Needs postext ≥ 1.8.0
import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage }
  from 'https://esm.sh/postext';

const LANG = 'es'; // @lang: the language of the sample document (the sample is Italian in both)
const RECIPE = 'storybook-chapter-vignettes';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = { // every colour in the config comes from here; the pictures were painted to it
  ink: '#2b2118', // text: a brown near-black
  paper: '#fffaf0', // a warm book paper, which the vignettes' margins were tinted to
  bark: '#6b4224', // chapter lines, running heads, the title
  wood: '#9a5f30', // the folio discs
};
// Each colour names its palette entry and carries its hex (gotcha: palette-skips-designs).
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 defaults linked to main-color, bold among them, print in bark instead of blue.
  { id: 'main-color', name: 'bark (defaults)', value: { hex: palette.bark, model: 'hex' } },
];
// Text and summaries; the book's title and the chapter lines; running heads and folios.
const [TEXT, DISPLAY, LABEL] = ['Averia Serif Libre', 'Fredericka the Great', 'Quicksand'];
const TRIM = { width: 190, height: 240 }; // mm
const [TOP, INNER, OUTER] = [22, 22, 20]; // mm; mirrored, so the spine margin swaps sides
const MEASURE = TRIM.width - INNER - OUTER; // 148 mm
const [BODY, LEAD, LINES] = [13, 19, 29]; // pt, pt, and whole lines in the text block
const BOTTOM = TRIM.height - TOP - (LINES * LEAD * 25.4) / 72; // mm: 23.6
// Design-slot shorthands: a placement from an anchor's edge (mm), and a design text.
const at = (to, edge, x = 0, y = 0, size) => ({ anchor: { to, edge },
  offset: { x: mm(x), y: mm(y) }, ...(size && { size }) });
const text = (id, content, fontFamily, size, placement, extra = {}) => ({ kind: 'text', id,
  content, fontFamily, fontSize: pt(size), color: col('ink'), align: 'center',
  overflow: 'wrap', // wrap, never '…' (gotcha: overflow-ellipsis-default)
  placement, ...extra });
const caps = (tracking) => ({ textTransform: 'uppercase', letterSpacing: pt(tracking),
  color: col('bark') });
// Half a tracking (mm): 1.4.1 tracks after a centred line's last letter, shifting it left.
const trail = (tracking) => (tracking * 25.4) / 72 / 2;

// #region answer: one opener for every chapter: the vignette the heading names, then its summary
// # Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia…"}
// resourceId takes placeholders like a design text: '{attr.vignetta}' draws the resource the
// heading names, so every chapter shares one style.
const VIGNETTE = { width: 84, height: 56 }; // mm, centred at the head of the text block
const SUMMARY = 124; // mm: the summary's measure, narrower than the text's
const opener = { enabled: true,
  // A floor for the height the opener reserves, in whole lines, so the text under chapter I's
  // two-line summary starts on the same grid line as the text under chapter II's three lines.
  minHeight: pt(14 * LEAD),
  slot: { elements: [
    { kind: 'image', id: 'vignette', resourceId: '{attr.vignetta}',
      placement: at('container', 'top', 0, 0,
        { width: mm(VIGNETTE.width), height: mm(VIGNETTE.height) }) },
    // Placed from the container, not from #vignette: images do not count in the height an
    // opener reserves (gotcha: opener-image-no-reserve), the texts under them do.
    text('chapter', '{titleText}', DISPLAY, 24, at('container', 'top-left', trail(2.4),
      VIGNETTE.height + 6, { width: mm(MEASURE) }), { ...caps(2.4), lineHeight: 1 }),
    text('summary', '{attr.summary}', TEXT, 12.5, at('#chapter', 'below',
      (MEASURE - SUMMARY) / 2 - trail(2.4), 5, { width: mm(SUMMARY) }),
    { italic: true, lineHeight: 1.38 }), // a multiple (gotcha: design-lineheight-multiple)
  ] },
};
const chapter = { id: 'capitolo', advancedDesign: opener };
// #endregion

// #region frontispiece: the title page as a heading style: a painted field, then the title
const SKY = 150; // mm from the top edge: the foot of the painted field
// # Pinocchio {style="frontespizio" kicker="Le avventure di"}; subtitle and author come from
// the frontmatter. Every element hangs from the page, so the field runs off three edges.
const frontispiece = {
  id: 'frontespizio',
  span: 'page', // or the field is cut at the top of the text block (gotcha: opener-clipped-at-top)
  footer: { elements: [] }, // no folio; the running heads already skip openers
  advancedDesign: { enabled: true, slot: { elements: [ // array order is paint order
    { kind: 'image', id: 'landscape', resourceId: 'frontespizio', // cut to the field's 190:150
      placement: at('page', 'top-left', 0, 0, { width: 'fill', height: mm(SKY) }) },
    text('kicker', '{attr.kicker}', LABEL, 11, at('page', 'top', trail(2.6), SKY + 11),
      { ...caps(2.6), fontWeight: 700 }),
    text('title', '{titleText}', DISPLAY, 70, at('page', 'top', 0, SKY + 17),
      { lineHeight: 1, color: col('bark') }),
    text('subtitle', '{subtitle}', TEXT, 15, at('page', 'top', 0, SKY + 44), { italic: true }),
    text('author', '{author}', LABEL, 9, at('page', 'top', trail(2), SKY + 58),
      { ...caps(2), fontWeight: 600 }),
  ] } },
};
// #endregion

// #region folios: running heads on body pages only, folios in wood-coloured discs
// The excerpt opens on book page 6, a verso, so the title page faces chapter I.
const continuation = { pageIndexOffset: 5, pageNumbering: { startAt: 6 } };
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre sits off the page's centre
const head = (id, content, parity, x) => text(id, content, LABEL, 8,
  at('page', 'top', x + trail(1.8), 12), { ...caps(1.8), fontWeight: 600, parity, pages: 'body' });
const DISC = 7.4; // mm: the folio's disc
// A fixed square with a radius of half its side is a circle; the figures sit in its middle.
// lineHeight 1.17: the baseline falls at 0.8 of the line, 0.94 em down, so Quicksand's
// figures (0.70 em tall) leave 0.23 em above and below them, and 'middle' centres that line.
const disc = (id, parity, pages, edge, x) => text(id, '{pageNumber}', LABEL, 8.5,
  at('page', edge, x, -(BOTTOM - DISC) / 2, { width: mm(DISC), height: mm(DISC) }),
  { fontWeight: 700, color: col('paper'), lineHeight: 1.17, verticalAlign: 'middle', parity,
    pages, box: { backgroundColor: col('wood'), borderRadius: mm(DISC / 2) } });
const header = { elements: [
  head('verso-title', '{title}', 'even', -SHIFT), // the book on the left-hand page
  head('recto-chapter', '{chapterTitle}', 'odd', SHIFT), // the chapter on the right
] };
const footer = { elements: [
  disc('verso-folio', 'even', 'body', 'bottom-left', OUTER),
  disc('recto-folio', 'odd', 'body', 'bottom-right', -OUTER),
  disc('opener-folio', 'all', 'opener', 'bottom', 0), // a chapter opens on either side
] };
// #endregion

// #region tailpiece: an unnumbered resource type, set where ::resource stands
// No caption prefix and no caption, so the cherries that close chapter I print with no
// label or number. The pictures the design slots paint are of this type too.
const resourceTypes = [{ id: 'fregio', name: 'Fregio', namePlural: 'Fregi', shortLabel: 'Fregio',
  captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal',
  defaultPlacement: { position: 'here', width: 0.16, align: 'center' } }];
// #endregion

// #region pictures: four paintings, declared at their pixel size under the file ids they load as
const PICTURES = { // id: [file, width, height, alt text in the book's language]
  frontespizio: ['frontespizio-1400.jpg', 1400, 1105, 'Colline toscane con cipressi e un borgo; '
    + 'un contadino guida un asinello con un carretto di legna verso la bottega di un falegname.'],
  ceppo: ['ceppo-1200.jpg', 1200, 800, 'Maestro Ciliegia, con la scure alzata sopra un pezzo di '
    + 'legno sul banco, si guarda intorno spaventato.'],
  polenta: ['polenta-1200.jpg', 1200, 800, 'Geppetto, con la parrucca gialla, riceve il pezzo di '
    + 'legno da Maestro Ciliegia; accanto, un paiolo di polenta fumante.'],
  ciliegie: ['ciliegie-600.jpg', 600, 626, 'Due ciliegie rosse su un picciolo, con una foglia.'],
};
const resources = Object.entries(PICTURES).map(([id, [fileId, w, h, altText]]) => ({
  id, typeId: 'fregio', kind: 'bitmap', createdAt: 0, updatedAt: 0, altText,
  bitmap: { fileId, format: 'jpeg', width: w, height: h } }));
// #endregion

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: 'it', // Italian hyphenation, by its exact code (gotcha: hyphenation-locales)
  resourceTypes, colorPalette, header, footer, layout: { layoutType: 'single' },
  page: { width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'),
    margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER),
      mirror: true } },
  bodyText: {
    fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
    italicColor: col('ink'), // *ohi* in ink, not in the defaults' bark
    // The book cites nothing, but a :ref added later prints in ink: main-color does not reach
    // this colour, which stays #295AA3 (gotcha: palette-skips-designs).
    referenceColor: col('ink'),
    firstLineIndent: mm(5), indentAfterHeading: false,
    maxWordSpacing: 1.45, // the default 2 lets 14 lines stretch past 1.45×; 3 still do, to 1.58×
  },
  headings: {
    fontFamily: DISPLAY, fontWeight: 400, // the designs paint the titles: no Open Sans to load
    // Any headings object drops the H1 break (gotcha: headings-drop-h1-break): every
    // chapter on a new page, on either side, so chapter II opens on a verso.
    levels: [{ level: 1, breakBefore: { enabled: true, parity: 'any' } }],
  },
  headingStyles: [frontispiece, chapter],
  paragraphStyles: [{ id: 'colofon', fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(11),
    color: col('bark'), textAlign: 'center', firstLineIndent: pt(0), marginTop: pt(LEAD) }],
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Mostra en Markdown · 74 línies · content.es.mdtitle: "Le avventure di Pinocchio" subtitle: "Storia di un burattino" author: "Carlo Collodi" --- # Pinocchio {style="frontespizio" kicker="Le avventure di"} # Capitolo I {style="capitolo" vignetta="ceppo" summary="Come andò che Maestro Ciliegia, falegname, trovò un pezzo di legno che piangeva e rideva come un bambino."} — C’era una volta… — Un re! — diranno subito i miei piccoli lettori. — No, ragazzi, avete sbagliato. C’era una volta un pezzo di legno. Non era un legno di lusso, ma un semplice pezzo da catasta, di quelli che d’inverno si mettono nelle stufe e nei caminetti per accendere il fuoco e per riscaldare le stanze. Non so come andasse, ma il fatto gli è che un bel giorno questo pezzo di legno capitò nella bottega di un vecchio falegname, il quale aveva nome mastr’Antonio, se non che tutti lo chiamavano maestro Ciliegia, per via della punta del suo naso, che era sempre lustra e paonazza, come una ciliegia matura. Appena maestro Ciliegia ebbe visto quel pezzo di legno, si rallegrò tutto; e dandosi una fregatina di mani per la contentezza, borbottò a mezza voce: — Questo legno è capitato a tempo; voglio servirmene per fare una gamba di tavolino. — Detto fatto, prese subito l’ascia arrotata per cominciare a levargli la scorza e a digrossarlo; ma quando fu lì per lasciare andare la prima asciata, rimase col braccio sospeso in aria, perchè sentì una vocina sottile sottile, che disse raccomandandosi: — Non mi picchiar tanto forte! — Figuratevi come rimase quel buon vecchio di maestro Ciliegia! Girò gli occhi smarriti intorno alla stanza per vedere di dove mai poteva essere uscita quella vocina, e non vide nessuno! Guardò sotto il banco, e nessuno: guardò dentro un armadio che stava sempre chiuso, e nessuno; guardò nel corbello dei trucioli e della segatura, e nessuno; aprì l’uscio di bottega per dare un’occhiata anche sulla strada, e nessuno. O dunque?… — Ho capito; — disse allora ridendo e grattandosi la parrucca — si vede che quella vocina me la son figurata io. Rimettiamoci a lavorare. — E ripresa l’ascia in mano, tirò giù un solennissimo colpo sul pezzo di legno. — Ohi! tu m’hai fatto male! — gridò rammaricandosi la solita vocina. Questa volta maestro Ciliegia restò di stucco, cogli occhi fuori del capo per la paura, colla bocca spalancata e colla lingua giù ciondoloni fino al mento, come un mascherone da fontana. Appena riebbe l’uso della parola, cominciò a dire tremando e balbettando dallo spavento: — Ma di dove sarà uscita questa vocina che ha detto ohi?… Eppure qui non c’è anima viva. Che sia per caso questo pezzo di legno che abbia imparato a piangere e a lamentarsi come un bambino? Io non lo posso credere. Questo legno eccolo qui; è un pezzo di legno da caminetto, come tutti gli altri, e a buttarlo sul fuoco, c’è da far bollire una pentola di fagioli… O dunque? Che ci sia nascosto dentro qualcuno? Se c’è nascosto qualcuno, tanto peggio per lui. Ora l’accomodo io! — E così dicendo, agguantò con tutt’e due le mani quel povero pezzo di legno, e si pose a sbatacchiarlo senza carità contro le pareti della stanza. Poi si messe in ascolto, per sentire se c’era qualche vocina che si lamentasse. Aspettò due minuti, e nulla; cinque minuti, e nulla; dieci minuti, e nulla! — Ho capito — disse allora sforzandosi di ridere e arruffandosi la parrucca — si vede che quella vocina che ha detto *ohi*, me la son figurata io! Rimettiamoci a lavorare. — E perchè gli era entrato addosso una gran paura, si provò a canterellare per farsi un po’ di coraggio. Intanto, posata da una parte l’ascia, prese in mano la pialla, per piallare e tirare a pulimento il pezzo di legno; ma nel mentre che lo piallava in su e in giù, sentì la solita vocina che gli disse ridendo: — Smetti! tu mi fai il pizzicorino sul corpo! — Questa volta il povero maestro Ciliegia cadde giù come fulminato. Quando riaprì gli occhi, si trovò seduto per terra. Il suo viso pareva trasfigurito, e perfino la punta del naso, di paonazza come era quasi sempre, gli era diventata turchina dalla gran paura. ::resource{id="ciliegie"} # Capitolo II {style="capitolo" vignetta="polenta" summary="Maestro Ciliegia regala il pezzo di legno al suo amico Geppetto, il quale lo prende per fabbricarsi un burattino maraviglioso, che sappia ballare, tirar di scherma e fare i salti mortali."} In quel punto fu bussato alla porta. — Passate pure, — disse il falegname, senza aver la forza di rizzarsi in piedi. Allora entrò in bottega un vecchietto tutto arzillo, il quale aveva nome Geppetto; ma i ragazzi del vicinato, quando lo volevano far montare su tutte le furie, lo chiamavano col soprannome di Polendina, a motivo della sua parrucca gialla, che somigliava moltissimo alla polendina di granturco. Geppetto era bizzosissimo. Guai a chiamarlo Polendina! Diventava subito una bestia, e non c’era più verso di tenerlo. :::paragraphs{style="colofon"} Composto in Averia Serif Libre, Fredericka the Great e Quicksand (SIL Open Font License). Testo di Carlo Collodi (1883) secondo l’edizione Bemporad del 1902, Project Gutenberg n. 52484. Illustrazioni generate con modelli di diffusione. :::
`; // content.<lang>.md: Collodi's Italian in both editions // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Averia Serif Libre': ['400', '400i'], 'Fredericka the Great': ['400'], Quicksand: ['400', '600', '700'] }; // every face, loaded before layout (gotcha: fonts-first) // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); // A design slot or ::resource names a resource, the resource a file id: load each file. await Promise.all(Object.values(PICTURES).map(([file]) => loadImage(file, asset(file)))); const doc = await buildWithFonts( () => buildDocument({ markdown, resources, continuation }, config()), markdown); showPages(doc, { title: 'Le avventure di Pinocchio' });
Kit · core, fonts, viewer, images: igual a totes les receptes · 270 línies// ─── 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 v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { 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', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true 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; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), 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 v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { 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, { 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 v1 ── 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 (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** 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 ───────────────────────────────────────────────────────────────────────

L'script.js compost funciona tal com és: enganxa'l com a script de mòdul en qualsevol pàgina o obre la recepta a CodePen. Carpeta de la recepta a GitHub ↗ (s'obre en una pestanya nova)

Variants

#Obre cada capítol en pàgina senar

Amb 'odd', el capítol II passa a la pàgina 11 i la 10 queda en blanc; la portada desactiva el seu propi salt, perquè hereta el del nivell i, si no, passaria a la pàgina 7 després d'una pàgina 6 en blanc.

@@ l'estil de la portada @@
   id: 'frontespizio',
+  breakBefore: { enabled: false },
@@ headings, a config() @@
-    levels: [{ level: 1, breakBefore: { enabled: true, parity: 'any' } }],
+    levels: [{ level: 1, breakBefore: { enabled: true, parity: 'odd' } }],

Errors freqüents

Error freqüent

Les imatges d'una obertura no compten per a l'alçada que reserva

A postext 1.4.1, un títol amb disseny avançat mesura l'alçada que reserva sense comptar-ne les imatges: els seus textos, filets i caixes compten, encara que estiguin ancorats a la pàgina, però una imatge, com un dibuix a sang al capdamunt de la pàgina, no reserva res, de manera que el text pot començar a sobre. Fixa amb minHeight on ha de començar el text. Obertures dissenyades →

Error freqüent

Una obertura que es queda a la seva columna es talla al capdamunt de la caixa de text

A postext 1.4.1, un títol amb disseny avançat que es queda a la seva columna es retalla per la vora superior de la columna: una caixa o una imatge ancorades a la pàgina o a la sang es pinten als marges laterals, però no al superior, i cap avís no ho diu. Dona span: 'page' a aquest títol, encara que el llibre tingui una sola columna: llavors el seu disseny es pinta sencer, com a banda d'obertura de la pàgina. Obertures dissenyades →

Error freqüent

Valors d'atribut: sense { ni }; cometes simples si porten "

Un valor d'atribut acaba a la clau de tancament, així que no pot contenir { ni }. Un valor que porti cometes dobles va entre cometes simples; el signe de dòlar no dona problemes. Atributs de títol →

Error freqüent

Posa entre cometes cada valor del frontmatter

YAML llegeix title: 1984 com un nombre i una data com un objecte Date, i els valors que no són cadenes s'imprimeixen buits als marcadors i deixen el PDF sense títol. Posa entre cometes cada valor: title: "1984". Metadades del document →

Error freqüent

Qualsevol objecte headings desactiva el salt de pàgina de l'H1

Per defecte un H1 salta a una pàgina senar (always-odd), però qualsevol objecte headings anul·la aquest valor, de manera que els capítols van seguits i span: 'page' no fa res. Torna a declarar headings.levels[0].breakBefore: { enabled: true, parity } a cada configuració. Capítols que obren en pàgina senar →

Error freqüent

Una paleta canviada no arriba als elements de disseny ni al color de les remissions

postext 1.4.1 aplica colorPalette als estils de text (cos, títols, llistes, peus, taules, requadres), però no als elements de capçaleres, peus de pàgina, obertures i portadelles, ni a bodyText.referenceColor: conserven l'hex escrit al costat del seu paletteId. Si canvies la paleta, per a una edició de pantalla fosca o per recolorejar, reescriu cada color enllaçat a partir de colorPalette abans de compondre. Paleta de color semàntica →

Error freqüent

El lineHeight d'un text de disseny és un múltiple, mai una mesura

En una ranura de disseny, el lineHeight d'un element de text multiplica el seu cos (lineHeight: 1.05). A postext 1.4.1 una mesura com pt(15) no dona error: l'alçada de l'obertura surt NaN, l'espai que reserva, minHeight inclòs, es perd sense avís i el text se superposa al títol. Textos, filets i caixes als dissenys de pàgina →

Error freqüent

El desbordament del text de disseny és 'ellipsis-end' per defecte

Un element de text de disseny que no cap en la seva amplada acaba en punts suspensius per defecte. Posa overflow: 'wrap' als títols que hagin de passar a més línies. Textos, filets i caixes als dissenys de pàgina →

Error freqüent

Només 8 llengües tenen partició de mots, amb el codi exacte

La partició de mots existeix per a en-us, es, fr, de, it, pt, ca i nl, amb el codi exacte: 'es-ES' o qualsevol altra llengua passa sense avís a l'anglès americà. Partició de mots i llengua del document →

Error freqüent

Carrega totes les fonts abans de compondre

La composició mesura el text amb les fonts que el navegador ha carregat i en desa les amplades, així que una font que arriba després de la primera composició deixa talls de línia erronis i un PDF que ja no coincideix amb la pantalla. Carrega abans tots els pesos i estils, i crida clearMeasurementCache() abans de recompondre si alguna arriba tard. Fonts abans de compondre →

  • Al Markdown, un espai de no separació (U+00A0) uneix cada guió llarg de diàleg de Collodi a la seva paraula, perquè un guió de tancament no obri mai línia; dos més mantenen junts di legno i in piedi al final dels seus paràgrafs. A la 1.4.1, si el paràgraf porta negreta o cursiva, la línia encara es pot partir en un espai de no separació. L'únic paràgraf amb aquests espais i una paraula en cursiva, el de l'ohi de la pàgina 9, en té quatre, i a la captura tots cauen a mitja línia o al final del paràgraf.

Crèdits

Text
  • Le avventure di Pinocchio (1883): chapter I and the opening of chapter II with Collodi’s chapter summaries, from the 1902 Bemporad edition, with typographic apostrophes, dashes and ellipses; chapter I’s summary has the comma after “falegname” that the edition’s table of contents prints and its chapter head leaves out · Carlo Collodi · domini públic
Imatges
  • The frontispiece: Tuscan hills, a hill village and a donkey cart of firewood on its way to the carpenter’s workshop · Generated With Diffusion Models · original
  • Chapter I’s vignette: Maestro Ciliegia, axe raised over the talking log · Generated With Diffusion Models · original
  • Chapter II’s vignette: Geppetto in his yellow wig receives the log, beside a pot of polenta · Generated With Diffusion Models · original
  • The cherry tailpiece · Generated With Diffusion Models · original
Fonts
Averia Serif Libre (SIL OFL 1.1) · Fredericka the Great (SIL OFL 1.1) · Quicksand (SIL OFL 1.1)
Sandbox