Saltar al contingut principal
Recepta número 104

Receptari · Capítol 2 · Text i tipografia

Una novel·la curta en català, amb la partició de l'IEC

El transplantat d'Oller, complet, en una butxaca de 32 pàgines: síl·labes catalanes segons l'IEC, l·l tallada com il- | lusió i un gravat per capítol.

En aquesta pàgina
Sortida
Canvas · PDF
Nivell
Intermedi
Postext
Provada amb Postext 1.14.0
Requereix ≥ 1.14.0 · postext-pdf ≥ 1.14.0
Llicència
Actualitzada el 4 d’oct. del 2026
Codi MIT · Text MIT
  • Mostra en castellà: encara no hi ha edició en català
  • Format 120 × 185 mm
  • 1 columna
  • Crimson Pro 10,4/14
  • Cormorant Garamond
  • Cormorant SC
  • 32 pàgines
  • Nivell
  • Postext 1.14.0
  • Compost en 931 ms
  • 179 línies de codi

En poques paraules

Una novel·la curta sencera en català, composta com un llibre de butxaca. Els mots es tallen a final de línia com demanen les normes catalanes, també la ela geminada amb punt volat.

Què compondràs

El transplantat de Narcís Oller sencer, amb el text de les seves Obres completes (Barcelona, 1928), en un llibre de butxaca de 12 × 18,5 cm i 32 pàgines. Abans dels quatre capítols hi ha una avantportada, un frontispici amb el forner davant del seu forn, la portada en Cormorant Garamond cursiva i vermella, i una nota sobre l'edició. Cada capítol s'obre sota un gravat de dotze línies d'alçada, amb el seu número romà en vermell. El text, en Crimson Pro de 10,4 pt, es parteix per síl·labes catalanes com demana l'Institut d’Estudis Catalans: almenys dues lletres a cada costat, cap tall entre vocals en hiat ni just després d'un apòstrof, i titil·laven es talla titil- | laven, amb el guionet al lloc del punt volat. Els diàlegs porten guió i les citacions, «cometes baixes».

Aquesta recepta respon a

  • Com aconsegueixo una bona justificació i partició de mots en textos en castellà, francès o alemany?
  • 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 escric guions de diàleg, anys a l'inici de paràgraf, preus i símbols literals sense que el Markdown els interpreti malament?

La resposta curta

script.js · línies 48–62al codi complet
const LOCALE = 'ca'; // the exact bundled code: 'ca-ES' resolves to it too (postext ≥ 1.14)
const bodyText = { // config().bodyText
  fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
  firstLineIndent: mm(4.5), indentAfterHeading: false,
  // The defaults justify, hyphenate in config().locale and break by Knuth–Plass. With 'ca'
  // the dictionary follows the IEC: two letters on each side (ter-ra, cai-xa), no split in
  // a hiatus (cièn-cia), none after an apostrophe (s’ha-via), and l·l breaks with the hyphen
  // in place of the dot. compounds: false keeps arrossegar-se whole but for its own hyphen,
  // the break the IEC prefers for a verb and its pronoun (Llibre d’estil, VI 2.5).
  hyphenation: { compounds: false },
  // Spaces past 1.6× take up to 10‰ of tracking instead; a last line under 22 characters is
  // a runt. A line that must keep d’inaugurar-se whole still opens to 1.94× on page 7.
  maxWordSpacing: 1.6, maxJustifyTracking: 10, runtMinCharacters: 22,
};

Ingredients

Tipografia
Crimson Pro, Cormorant Garamond, Cormorant SC (SIL OFL 1.1)
Recursos
  • cap1-1200.jpg
  • cap2-1200.jpg
  • cap3-1200.jpg
  • cap4-1200.jpg
  • frontis-1000.jpg
  • Engraving, frontispiece: the baker at his oven's door (Generated With Diffusion Models, original)
  • Engraving, chapter I: the night train into Barcelona (Generated With Diffusion Models, original)
  • Engraving, chapter II: the village street in autumn (Generated With Diffusion Models, original)
  • Engraving, chapter III: the Rambla at dawn (Generated With Diffusion Models, original)
  • Engraving, chapter IV: the old man by the railway (Generated With Diffusion Models, original)

Elaboració

#1 · El català pel seu codi

El codi és la resposta curta de dalt. locale: 'ca' tria els patrons catalans, que des de postext 1.14 segueixen el Llibre d’estil de l'IEC (llengües admeses). Són els mateixos patrons que fa servir TeX, amb almenys dues lletres a cada costat del tall en lloc de les tres del paquet d'npm, així que ter-ra, cai-xa i plu-ja es poden tallar. Les vocals en hiat no se separen (cièn-cia, ca-mions), però una i entre vocals obre la seva síl·laba (fe-ia). La ela geminada es talla entre les seves dues l: la línia acaba en titil- i la següent comença per laven, sense punt volat en cap de les dues, i Knuth–Plass mesura la línia sense el punt.

Pàgina 27: titil·laven a final de línia, compost titil- | laven. És l'única l·l que cau a final de línia en aquestes 32 pàgines.

compounds: false deixa sencers el verb i el seu pronom llevat del seu propi guionet (arrossegar- | se), que és el tall que prefereix l'IEC (partició de mots). Costa una línia fluixa: a la pàgina 7, la línia anterior a d’inaugurar-se obre els seus espais fins a 1,94 vegades, tot i que maxJustifyTracking: 10 absorbeix l'excés a les altres.

#2 · Una pàgina de butxaca de línies senceres

script.js · línies 37–44al codi complet
const TRIM = { width: 120, height: 185 }; // mm
const [TOP, INNER, OUTER, LINES] = [18, 16, 15, 29];
const MEASURE = TRIM.width - INNER - OUTER; // 89 mm: about 64 characters of Crimson Pro
const page = {
  width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'),
  margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - LINES * LINE),
    left: mm(INNER), right: mm(OUTER), mirror: true }, // left is the inner margin on a recto
};

El marge inferior surt del format, el marge superior i LINES, així que la caixa admet 29 línies de 14 pt i tota pàgina plena acaba a la mateixa línia de base. La mesura de 89 mm dona uns 64 caràcters de Crimson Pro. mirror: true manté el marge de 16 mm al llom a les dues pàgines del plec (marges simètrics).

#3 · Una obertura per als quatre capítols

script.js · línies 66–81al codi complet
// # II {style="capitol" art="cap2"}: the art attribute names the picture's resource.
const PICTURE = { width: MEASURE, height: 12 * LINE }; // 89 × 59.3 mm: twelve lines, 3:2
const chapter = { id: 'capitol',
  breakBefore: { enabled: true, parity: 'any' }, // a chapter opens on the next page, either side
  marginBottom: pt(0), // the opener's own height already ends on a line
  advancedDesign: { enabled: true, minHeight: pt(16 * LEAD), slot: { elements: [
    { kind: 'image', id: 'art', resourceId: '{attr.art}',
      placement: at('container', 'top', 0, 0,
        { width: mm(PICTURE.width), height: mm(PICTURE.height) }) },
    // From the container, not the image: images reserve no height (opener-image-no-reserve).
    text('numeral', '{titleText}', DISPLAY, 21, at('container', 'top', 0, PICTURE.height + 4),
      { fontWeight: 600, color: col('vermell'), lineHeight: 1 }),
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('vermell'),
      placement: { ...at('container', 'top', 0, PICTURE.height + 13), size: { width: mm(8) } } },
  ] } },
};

resourceId: '{attr.art}' dibuixa la imatge que anomena el títol, així que # II {style="capitol" art="cap2"} obre el capítol II sota el seu propi gravat amb el mateix estil que els altres (atributs de títol). Les imatges no reserven alçada en una obertura, així que minHeight guarda setze línies: dotze per al gravat, després el número i el seu filet, i el text comença a la línia disset de la pàgina, igual a tots els capítols.

#4 · Quatre pàgines abans del capítol I

script.js · línies 85–114al codi complet
const front = (id, elements, extra = {}) => ({ id, breakBefore: { enabled: true, parity: 'any' },
  marginBottom: pt(0), header: { elements: [] }, footer: { elements: [] }, ...extra,
  advancedDesign: { enabled: true, ...(extra.minHeight && { minHeight: extra.minHeight }),
    slot: { elements } } });
const FRONTIS = { width: MEASURE, height: MEASURE * 1.5 }; // mm: the engraving is 2:3
const headingStyles = [
  front('avantportada', [text('title', '{titleText}', DISPLAY, 19,
    at('container', 'top', 0, 9 * LINE), { italic: true, fontWeight: 500, lineHeight: 1 })]),
  front('frontis', [
    { kind: 'image', id: 'art', resourceId: '{attr.art}',
      placement: at('container', 'top', 0, 0,
        { width: mm(FRONTIS.width), height: mm(FRONTIS.height) }) },
    text('legend', '{titleText}', LABEL, 8.5, at('container', 'top', 0, FRONTIS.height + 5),
      { ...caps(8.5, 1.4, 'muted') }),
  ]),
  front('portada', [
    text('author', '{author}', LABEL, 10, at('container', 'top', 0, 5 * LINE),
      caps(10, 2.2, 'ink')),
    text('title', '{titleText}', DISPLAY, 34, at('container', 'top', 0, 8 * LINE),
      { italic: true, fontWeight: 500, color: col('vermell'), lineHeight: 1 }),
    text('subtitle', '{subtitle}', DISPLAY, 13, at('container', 'top', 0, 8 * LINE + 17),
      { italic: true, fontWeight: 500 }),
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('vermell'),
      placement: { ...at('container', 'top', 0, 8 * LINE + 29), size: { width: mm(14) } } },
  ]),
  // The note sits low on the verso behind the title, its label a line above it.
  front('nota', [text('label', '{titleText}', LABEL, 8.5, at('container', 'top', 0, 13 * LINE),
    caps(8.5, 1.4))], { minHeight: pt(15 * LEAD) }),
  chapter,
];

Cada peça dels preliminars és un títol de primer nivell amb estil propi: # El transplantat {style="portada"} imprimeix l'autor i el subtítol del frontmatter i el títol com a títol. Cada estil repeteix el seu salt de pàgina, perquè un estil hereta el del seu nivell, i buida la capçalera i el peu, així que aquestes pàgines no porten capçaleres ni folis. El frontispici cau a la pàgina 2, parella, i queda davant de la portada.

#5 · L'autor a la parella, el títol a la senar

script.js · línies 118–132al codi complet
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre is off the page's centre
const head = (id, content, parity, edge, x, y = 10, pages = 'body') => ({
  kind: 'text', id, content, parity, pages, ...caps(8, 1.3, 'muted'),
  placement: at('page', edge, x, y),
});
const folio = { fontFamily: TEXT, fontWeight: 400, letterSpacing: pt(0), color: col('ink') };
const header = { elements: [
  { ...head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER), ...folio },
  head('verso-author', '{author}', 'even', 'top', -SHIFT),
  head('recto-title', '{title}', 'odd', 'top', SHIFT),
  { ...head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER), ...folio },
] };
const drop = (parity, x) => ({ ...head(`drop-${parity}`, '{pageNumber}', parity, 'bottom', x,
  -11, 'opener'), ...folio });
const footer = { elements: [drop('odd', SHIFT), drop('even', -SHIFT)] };

parity posa l'autor a les pàgines parelles i el títol a les senars; pages: 'body' les treu de les obertures, on va un foli al peu amb pages: 'opener'. SHIFT centra cada capçalera sobre la caixa, que està desplaçada mig mil·límetre del centre de la pàgina perquè els dos marges no mesuren el mateix.

#6 · Cinc gravats, sense peu

script.js · línies 136–152al codi complet
const resourceTypes = [{ id: 'gravat', name: 'Gravat', namePlural: 'Gravats', shortLabel: 'Gravat',
  captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal' }];
const PICTURES = { // id: [file, width, height, alt text in the book's language]
  frontis: ['frontis-1000.jpg', 1000, 1500, 'Un carrer de poble al capvespre. A la porta del forn, '
    + 'on cremen les flames, el forner, gras i somrient, amb davantal blanc i els braços creuats.'],
  cap1: ['cap1-1200.jpg', 1200, 800, 'De nit, el tren baixa cap a Barcelona: rengleres de fanals, '
    + 'la muntanya de Montjuïc i la lluna sobre el mar. Un pagès treu el cap per la finestreta.'],
  cap2: ['cap2-1200.jpg', 1200, 800, 'Un carrer de poble buit en una nit de tardor. Per la porta '
    + 'baixa de la ferreria, el manyà pica l’enclusa enmig d’una estrella d’espurnes.'],
  cap3: ['cap3-1200.jpg', 1200, 800, 'La Rambla a trenc d’alba des d’un balcó. Un fanaler apaga '
    + 'el gas amb la perxa; al fons, els pals dels vaixells del port.'],
  cap4: ['cap4-1200.jpg', 1200, 800, 'Capvespre als afores. Un vell de cabells blancs, assegut '
    + 'al caire del desmunt, mira la via del tren, que es perd recta fins a l’horitzó.'],
};
const resources = Object.entries(PICTURES).map(([id, [fileId, w, h, altText]]) => ({
  id, typeId: 'gravat', kind: 'bitmap', createdAt: 0, updatedAt: 0, altText,
  bitmap: { fileId, format: 'jpeg', width: w, height: h } }));

Les imatges són d'un tipus de recurs amb captionPrefix buit, així que res no les numera ni les retola. Cadascuna es declara amb la seva mida en píxels i es carrega amb el seu identificador de fitxer abans de compondre. Abans de desar-les es va portar el seu blanc al crema de la pàgina i el seu negre a la tinta del text, així que s'imprimeixen com a part de la pàgina i no com a fotos enganxades a sobre.

La recepta completa

Sandbox
// ═══ Postext Cookbook · Nº 104 · A Catalan novella, hyphenated by the IEC rules ═══════
// https://postext.dev/en/cookbook/catalan-pocket-novella
// Code: MIT · Text: N. Oller, El transplantat, 1928 (PD, Viquitexts) · Pictures: diffusion models
// Fonts: Crimson Pro, Cormorant Garamond, Cormorant SC (SIL OFL 1.1) · Needs postext ≥ 1.14.0
import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage }
  from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'es'; // @lang: the language of the sample document (the sample is Catalan in both)
const RECIPE = 'catalan-pocket-novella';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
const palette = {
  ink: '#2a2320', // text: a warm near-black, the pictures' ink too
  vermell: '#953222', // the one accent: chapter numerals, rules, the title page's title
  muted: '#6e6359', // running heads, the edition's note, the colophon
  paper: '#f5efe1', // a cream book paper, the pictures' white
};
// 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' } })),
  { id: 'main-color', name: 'vermell (defaults)', value: { hex: palette.vermell, model: 'hex' } },
];
const [TEXT, DISPLAY, LABEL] = ['Crimson Pro', 'Cormorant Garamond', 'Cormorant SC'];
const [BODY, LEAD] = [10.4, 14]; // pt: body and leading, the pitch of every vertical measure
const LINE = (LEAD * 25.4) / 72; // mm: one line, 4.94
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', placement, ...extra }); // wrap, never '…' (gotcha: overflow-ellipsis-default)
const caps = (size, tracking, color = 'vermell') => ({ fontFamily: LABEL, fontSize: pt(size),
  fontWeight: 600, letterSpacing: pt(tracking), color: col(color) });

// #region page: a 12 × 18.5 cm pocket book; the text block holds 29 whole lines
const TRIM = { width: 120, height: 185 }; // mm
const [TOP, INNER, OUTER, LINES] = [18, 16, 15, 29];
const MEASURE = TRIM.width - INNER - OUTER; // 89 mm: about 64 characters of Crimson Pro
const page = {
  width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'),
  margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - LINES * LINE),
    left: mm(INNER), right: mm(OUTER), mirror: true }, // left is the inner margin on a recto
};
// #endregion

// #region answer: Catalan by its own code: l·l breaks as il- | lusió, no word splits its hiatus
const LOCALE = 'ca'; // the exact bundled code: 'ca-ES' resolves to it too (postext ≥ 1.14)
const bodyText = { // config().bodyText
  fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
  boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
  firstLineIndent: mm(4.5), indentAfterHeading: false,
  // The defaults justify, hyphenate in config().locale and break by Knuth–Plass. With 'ca'
  // the dictionary follows the IEC: two letters on each side (ter-ra, cai-xa), no split in
  // a hiatus (cièn-cia), none after an apostrophe (s’ha-via), and l·l breaks with the hyphen
  // in place of the dot. compounds: false keeps arrossegar-se whole but for its own hyphen,
  // the break the IEC prefers for a verb and its pronoun (Llibre d’estil, VI 2.5).
  hyphenation: { compounds: false },
  // Spaces past 1.6× take up to 10‰ of tracking instead; a last line under 22 characters is
  // a runt. A line that must keep d’inaugurar-se whole still opens to 1.94× on page 7.
  maxWordSpacing: 1.6, maxJustifyTracking: 10, runtMinCharacters: 22,
};
// #endregion

// #region opener: the chapter's engraving, then its numeral, in one style for all four
// # II {style="capitol" art="cap2"}: the art attribute names the picture's resource.
const PICTURE = { width: MEASURE, height: 12 * LINE }; // 89 × 59.3 mm: twelve lines, 3:2
const chapter = { id: 'capitol',
  breakBefore: { enabled: true, parity: 'any' }, // a chapter opens on the next page, either side
  marginBottom: pt(0), // the opener's own height already ends on a line
  advancedDesign: { enabled: true, minHeight: pt(16 * LEAD), slot: { elements: [
    { kind: 'image', id: 'art', resourceId: '{attr.art}',
      placement: at('container', 'top', 0, 0,
        { width: mm(PICTURE.width), height: mm(PICTURE.height) }) },
    // From the container, not the image: images reserve no height (opener-image-no-reserve).
    text('numeral', '{titleText}', DISPLAY, 21, at('container', 'top', 0, PICTURE.height + 4),
      { fontWeight: 600, color: col('vermell'), lineHeight: 1 }),
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('vermell'),
      placement: { ...at('container', 'top', 0, PICTURE.height + 13), size: { width: mm(8) } } },
  ] } },
};
// #endregion

// #region front: half-title, frontispiece, title page and the edition's note, one page each
const front = (id, elements, extra = {}) => ({ id, breakBefore: { enabled: true, parity: 'any' },
  marginBottom: pt(0), header: { elements: [] }, footer: { elements: [] }, ...extra,
  advancedDesign: { enabled: true, ...(extra.minHeight && { minHeight: extra.minHeight }),
    slot: { elements } } });
const FRONTIS = { width: MEASURE, height: MEASURE * 1.5 }; // mm: the engraving is 2:3
const headingStyles = [
  front('avantportada', [text('title', '{titleText}', DISPLAY, 19,
    at('container', 'top', 0, 9 * LINE), { italic: true, fontWeight: 500, lineHeight: 1 })]),
  front('frontis', [
    { kind: 'image', id: 'art', resourceId: '{attr.art}',
      placement: at('container', 'top', 0, 0,
        { width: mm(FRONTIS.width), height: mm(FRONTIS.height) }) },
    text('legend', '{titleText}', LABEL, 8.5, at('container', 'top', 0, FRONTIS.height + 5),
      { ...caps(8.5, 1.4, 'muted') }),
  ]),
  front('portada', [
    text('author', '{author}', LABEL, 10, at('container', 'top', 0, 5 * LINE),
      caps(10, 2.2, 'ink')),
    text('title', '{titleText}', DISPLAY, 34, at('container', 'top', 0, 8 * LINE),
      { italic: true, fontWeight: 500, color: col('vermell'), lineHeight: 1 }),
    text('subtitle', '{subtitle}', DISPLAY, 13, at('container', 'top', 0, 8 * LINE + 17),
      { italic: true, fontWeight: 500 }),
    { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(0.5), color: col('vermell'),
      placement: { ...at('container', 'top', 0, 8 * LINE + 29), size: { width: mm(14) } } },
  ]),
  // The note sits low on the verso behind the title, its label a line above it.
  front('nota', [text('label', '{titleText}', LABEL, 8.5, at('container', 'top', 0, 13 * LINE),
    caps(8.5, 1.4))], { minHeight: pt(15 * LEAD) }),
  chapter,
];
// #endregion

// #region heads: the author on the verso, the title on the recto; a drop folio on openers
const SHIFT = (INNER - OUTER) / 2; // mm: the text block's centre is off the page's centre
const head = (id, content, parity, edge, x, y = 10, pages = 'body') => ({
  kind: 'text', id, content, parity, pages, ...caps(8, 1.3, 'muted'),
  placement: at('page', edge, x, y),
});
const folio = { fontFamily: TEXT, fontWeight: 400, letterSpacing: pt(0), color: col('ink') };
const header = { elements: [
  { ...head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER), ...folio },
  head('verso-author', '{author}', 'even', 'top', -SHIFT),
  head('recto-title', '{title}', 'odd', 'top', SHIFT),
  { ...head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER), ...folio },
] };
const drop = (parity, x) => ({ ...head(`drop-${parity}`, '{pageNumber}', parity, 'bottom', x,
  -11, 'opener'), ...folio });
const footer = { elements: [drop('odd', SHIFT), drop('even', -SHIFT)] };
// #endregion

// #region pictures: five engravings, unnumbered and uncaptioned, at their pixel size
const resourceTypes = [{ id: 'gravat', name: 'Gravat', namePlural: 'Gravats', shortLabel: 'Gravat',
  captionPrefix: '', numberingTemplate: '{n}', resetOn: 'never', counterFormat: 'decimal' }];
const PICTURES = { // id: [file, width, height, alt text in the book's language]
  frontis: ['frontis-1000.jpg', 1000, 1500, 'Un carrer de poble al capvespre. A la porta del forn, '
    + 'on cremen les flames, el forner, gras i somrient, amb davantal blanc i els braços creuats.'],
  cap1: ['cap1-1200.jpg', 1200, 800, 'De nit, el tren baixa cap a Barcelona: rengleres de fanals, '
    + 'la muntanya de Montjuïc i la lluna sobre el mar. Un pagès treu el cap per la finestreta.'],
  cap2: ['cap2-1200.jpg', 1200, 800, 'Un carrer de poble buit en una nit de tardor. Per la porta '
    + 'baixa de la ferreria, el manyà pica l’enclusa enmig d’una estrella d’espurnes.'],
  cap3: ['cap3-1200.jpg', 1200, 800, 'La Rambla a trenc d’alba des d’un balcó. Un fanaler apaga '
    + 'el gas amb la perxa; al fons, els pals dels vaixells del port.'],
  cap4: ['cap4-1200.jpg', 1200, 800, 'Capvespre als afores. Un vell de cabells blancs, assegut '
    + 'al caire del desmunt, mira la via del tren, que es perd recta fins a l’horitzó.'],
};
const resources = Object.entries(PICTURES).map(([id, [fileId, w, h, altText]]) => ({
  id, typeId: 'gravat', kind: 'bitmap', createdAt: 0, updatedAt: 0, altText,
  bitmap: { fileId, format: 'jpeg', width: w, height: h } }));
// #endregion

const paragraphStyles = [
  { id: 'nota', fontSize: pt(9), lineHeight: pt(LEAD * 0.9), color: col('muted'),
    italicColor: col('muted'), firstLineIndent: mm(4.5) },
  { id: 'fi', fontFamily: LABEL, fontSize: pt(9), letterSpacing: pt(2), color: col('vermell'),
    textAlign: 'center', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  { id: 'colofo', fontSize: pt(7.5), lineHeight: pt(10), color: col('muted'),
    italicColor: col('muted'), textAlign: 'center', firstLineIndent: pt(0),
    marginTop: pt(2 * LEAD) },
];

const config = () => ({ // a factory: the engine caches resolved configs per object
  locale: LOCALE,
  resourceTypes, colorPalette, page, bodyText, paragraphStyles, headingStyles, header, footer,
  layout: { layoutType: 'single' },
  headings: { // the designs paint every title; this keeps Open Sans unloaded
    fontFamily: DISPLAY, fontWeight: 600,
    // Any headings object drops the H1 break (gotcha: headings-drop-h1-break): restated.
    levels: [{ level: 1, breakBefore: { enabled: true, parity: 'any' } }],
  },
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
// The front matter, then the four chapters; chapter I in two files (2,500 words each at most).
const markdown = [String.raw`---
Mostra en Markdown · 18 línies · content.es.mdtitle: "El transplantat" subtitle: "De «Croquis del natural»" author: "Narcís Oller" --- # El transplantat {style="avantportada"} # El Forner de les Monges {style="frontis" art="frontis"} # El transplantat {style="portada"} # Nota a aquesta edició {style="nota"} :::paragraphs{style="nota"} Aquest text reprodueix «El transplantat» tal com Narcís Oller (1846–1930) el va revisar per al volum VI de les seves *Obres completes* (Barcelona, 1928), ja en l’ortografia de Pompeu Fabra. El conte havia aparegut el 1879 a *Croquis del natural*, el seu primer recull. S’hi han esmenat unes quantes errades de la impressió i, com fan les edicions d’avui, s’hi han suprimit els signes d’interrogació i d’exclamació inicials i els diàlegs s’hi marquen amb el guió llarg. Les cursives són de l’autor. Les il·lustracions s’han fet per a aquesta edició. :::
`, String.raw`# I {style="capitol" art="cap1"}
Mostra en Markdown · 54 línies · content.c1a.en.md Daniel (o el Forner de les Monges, si el voleu conèixer per son nom popular), lliurat amb la major fe al treball des de molt jove, veient com ses gotes de suor anaven a parar al calaix convertides en monedes d’argent i d’or que la muller guardava, no com a instruments de canvi, sinó com a medalles honorífiques, era feliç en son poble, i res del món no li pareixia tan bonic. Degut a sa felicitat impertorbable, o tal volta perquè la farina alimenta fins amb ses flaires, havia conservat sempre una gran robustesa, i s’havia tornat tan desmesuradament panxut i gros que quasi feia pena. I dic quasi perquè el color sa de son rostre, la calma de sa mirada i el somrís permanent de sos llavis revelaven tanta satisfacció i salut que era impossible conservar el sentiment de compassió que l’aspecte de la deformitat vos hagués inspirat al primer cop d’ull. Res de descontentament o disgust de la vida, res de malestar: la felicitat del Forner de les Monges era proverbial en el poble i en tota l’encontrada. És clar que, en els seixanta anys que ja tenia, no havia pogut lliurar-se d’aquelles desgràcies que persegueixen a tot mortal, com eren les pèrdues de família, entre les quals hi havia la molt dolorosa de sa muller; però totes les penes, fins les més fortes, eren poc duradores per a en Daniel, gràcies a sa pròpia pasta, molt millor que la dels pans que enfornava, i gràcies també a la *filosofia* i *religió* de què en casos consemblants sabia revestir-se. Tenia una paraula per a tothom, i no hi havia qui passés per davant seu sense donar-li el bon dia o preguntar-li si *prenia la fresca*, o *el sol*, o *si feia temps*. La popularitat, que capgira tanta gent, era la flaca del Forner de les Monges, i a satisfer-la posava tota la voluntat, procurant-se les simpaties de pobres i rics, amanyagant a tothom i no ofenent a ningú. Això, que no deixa d’ésser difícil en totes les esferes socials, ho és en grau superlatiu en un poblet; però, com que l’instint tot ho pot i en Daniel en tenia per a això i portava a més a més la patent d’inofensiu, assolí fàcilment el que volia. Així aconseguí *fer-se* amb el doctor Andreu, home sapientíssim del poble, que deixava bocabadats tots sos veïns amb les descripcions sobre història, geografia o astronomia que els endreçava poc després d’haver-les après ell de cor, i amb els proverbis i *sentències* que, com altre Sanxo Pança, deixava caure pel descosit. En una de ses converses, que, sens dubte pel que eren instructives, qualificava el doctor Andreu de conferències, digué un dia que «en el poble curt l’home savi es torna ruc». Ignoro si el doctor, dient això, pensava haver degenerat, o si considerava que sos oients havien pogut ésser savis algun dia. Tampoc no sé que en Daniel tingués motiu de creure’s mal aprofitat en el poble on havia nascut i era feliç tot fent pans i acabalant or. El que sé és que l’esperit humà presenta rareses absurdes i que, per una d’aquestes, tal volta, aquella sentència donà molt que pensar al nostre senzill forner, qui des de llavors afalagà el desig de sortir un jorn de son poble per, com diríem nosaltres, descobrir nous horitzons. Sé, per fi, que, gràcies a aquest fenomen, en Daniel veié amb gust marxar son fill gran a Barcelona per aprendre l’ofici de perruquer, malgrat la pena que la separació per una part, i per una altra el rompiment del rutinari costum de fer de l’hereu el mateix que fou son pare, podien causar-li. Qui sap si el bon home creia que a son fill podria entrar-li la ciència pels dits tot pentinant algun acadèmic o algun senyoràs d’aquells que són autoritats i llueixen creus i bandes! Fos el que fos, el fill del Forner de les Monges va fer son aprenentatge a Barcelona, i, a l’època a què ens referim, obrí *saló* pel seu compte, invertint en la instal·lació i ornament bona part de les medalles que tant havia guardat sa mare. El plantar *saló* tenia massa importància perquè es portés a efecte sense la presència del pare; i, com era natural, el fill fou a buscar-lo i se l’emmenà a Barcelona, on no havia estat mai. El *saló* era a la Rambla, i l’endemà de l’arribada havia d’inaugurar-se solemnement. Era no sols bonic, sinó verament luxós. En Miquelet, que en el món industrial portava com a nom de batalla el de *Rumbós*, tenia més lletra menuda que son mestre Fígaro. Havia comprès l’època: sabia com són avui reproductives certes despeses, i no volia perdonar-ne cap. Així, no satisfet d’haver posat un *saló* com cap altre no n’hi hagués a Espanya, cregué convenient d’armar rebombori en els diaris, i, seguint l’exemple d’altres empreses, resolgué dar un dinar als representants de la premsa, com ara es diu. Ell i son pare arribaren a Barcelona la nit abans. Ja hem dit que en Daniel no hi havia estat mai, i falta afegir que encara no havia vist cap ciutat d’igual importància. Judiqueu de ses impressions, cent voltes majors que les que experimenta en tals casos una criatura, a la qual manca sempre suficient judici per a fer-se càrrec del valor absolut de les novetats, i coneixement cabal d’allò que té vist per a comparar-ho amb el que veu per primera vegada. Pocs quilòmetres abans d’arribar ja no sabia en Daniel dar-se compte del que li passava contemplant les llargues rengleres de fanals que unes voltes veia senceres i altres cops trencades, segons les corbes o nivells que el tren travessava. Cap barceloní no ha vist tan gran la ciutat com apareixia als ulls del foraster. El fill d’aquest coadjuvava en gran manera a engrandir aquesta impressió. —Veieu, pare, aquella renglera de mà esquerra per on corren llumets de colors? És el passeig de Gràcia. Les altres transversals són carrers de l’*Ensanxe*… Mireu a la dreta: tot aquell rengle tan llarg de fanals és un camí: el camí de Sans… Allà dalt hi ha Montjuïc… El veieu, ara que la lluna ha sortit d’aquells nuvolots?… No distingiu, dessota, allà lluny, una gran resplendor? És el mar… Ara que hi toca la lluna, mireu que és bonic! Talment sembla un bassal de *plata* bullent! En Daniel guaitava a la dreta, a l’esquerra, amunt i avall, a terra i al cel, i quasi es marejava. Una paret, una casa, un desnivell de la via, la mateixa rapidesa del tren, li feien pampalluguejar amb tan estranya inquietud els llums, que a voltes en confonia amb estrelles i arribava a perdre l’esma de cel i terra. —Veniu a aquest altre cantó. Si se’n veuen, ara, de fanals! En Daniel feia uns ulls com unes taronges. —Si fos de dia us ensenyaria Pedralbes, Sarrià, Vallvidrera, Sant Gervasi, Gràcia; un caseriu que no s’acaba mai… Ja ens hi anem acostant… Hola! el tren de Sarrià!… Mireu aquest talús que encreua la via: ara ens passarà un altre tren per sobre. El forner, prou marejat, quasi no entenia com podria passar per sobre sense aixafar-los, i estigué a punt de senyar-se. Per fi la màquina xiulà, i, ja parat el tren, aparegueren els mossos de l’estació a recollir els bitllets, enlluernant amb llurs brillants i inquietes llanternes als viatgers. Mentrestant, en Daniel, seguint la indicació de son fill, contemplava la feixuga mole de la Universitat, que, embolcallada en l’ombra, li semblava un monstruós parapet flanquejat per dues grans torres. —Heu vist aquella punxa tan prima de més avall? És un convent de monges: les *Adoratrices*. Hem passat també les *Arrepentides*, Sant Joan de Déu i altres convents. Tot això n’és ple. Els frares i les monges ho entenen com sempre: aquest és el cantó de migjorn… Ja tornem a caminar: ara la màquina ens empeny per darrera. Alceu-vos. Veieu aquestes quatre rengleres de llums? Són la Gran Via: un carrer que tindrà quatre hores. Els frens començaren a grunyir, les rodes a arrossegar-se peresosament. —Ja hem arribat —digué el Rumbós tot ficant-se la gorra a la butxaca i posant-se un bolet molt elegantó—. Amaniu-vos i *tentu* amb el rellotge, que per aquí corren uns alets… Impossible de descriure minuciosament cada una de les impressions que el forner experimentà tot travessant la Rambla. Els fanalets de color dels cotxes que es movien en direccions diverses, la gent que ocupava l’imperial dels tramvies, els dolls de claror que sortien de les botigues, els rengles de persones deturades davant certs aparadors, la multitud que passejava per sota els arbres, els fanals d’anunci; tot oferia a sos ulls estranya novetat i acabava de marejar-lo. Barcelona li pareixia un cel per sa il·luminació, un infern per son moviment. No sabia si li agradava o no: desitjava arribar a casa, i al mateix temps hauria volgut no arribar-hi massa aviat. Aquella vacil·lació d’esperit el deixà fatigat. Per això quan, després d’haver sopat, es va veure al llit, l’home respirà amb satisfacció i tancà els ulls per adormir-se. Però era inútil: son agitat esperit feia de les seves, més entestat que mai a no descansar. Si obria els ulls, li pareixia distingir, entre la fosca, que els cantons superiors i inferiors de la cambra s’enfonsaven o s’enlairaven com els d’una cabina d’embarcació lliurada a la inestabilitat perpètua de les ones. Si els tancava, veia un eixam de llumenetes de tots colors voleiar com una immensa constel·lació deslligada de tota llei física. En Daniel no sabia què fer en aquell llit de turment, i més d’una vegada pensà a alçar-se. Per fi l’ensopiment de la son pogué més que els nervis, la cambra restà embolcada per la negror impenetrable i quieta de la fosca, aquelles llumenetes s’aprimaren fins a formar migrades ratlles d’esblanqueïda resplendor, i l’esperit d’en Daniel davallà pausadament per la costa suau de la son, on es perd tot sentit d’existència.
`, String.raw`L’endemà no fou dia de menys emocions. Matí i tarda passejà per Barcelona, aturant-se a cada pas davant els aparadors i a respectuosa distància dels cotxes que veia córrer envers ell.
Mostra en Markdown · 48 línies · content.c1b.en.md La vista del port, amb son bosc de pals i cordam, per entre els quals a penes s’albirava la interminable bellugadissa de la Riba; els cotxes de luxe, amb llurs troncs arrogants i llurs galonejats lacais; la plaça Reial, amb sa agradable simetria i sa netedat; la Rambla, amb son alegre brogit; i el passeig de Gràcia, ple també de moviment i vida; tenien embadalit en Daniel. Però, sumades totes les impressions del dia, no arribaven a poder ésser comparades amb les que li proporcionà el convit de la vesprada. En Daniel no cabia a la pell en contemplar son fill rodejat dels representants de la premsa, que, passejant els ulls de les parets a la taula tot esperant el sopar, omplien d’elogis l’amfitrió per la riquesa i el gust desplegats en l’establiment, i li auguraven un gran esdevenidor. Els miralls, els llums i els tocadors, i, sobretot, els olis, pomades, essències i cosmètics de procedència anglesa (una novetat que el nou perruquer presentava per millor acreditar-se), eren minuciosament examinats i lloats molt calorosament. El Rumbós no ho podia deixar d’ésser en aquell moment: amb un gest expressiu, feia que un dels aprenents anés empaquetant les ampolletes i pots que eren més elogiats, mal que l’armari perdés tota aquella simetria i abundància que a primera hora oferia. El Forner de les Monges quasi plorava de goig. «Son fill era un savi. Bé deia el doctor Andreu que “en el poble curt l’home savi es torna ruc”. Quan, en Miquelet, hauria tractat tanta gent de lletres ni après a viure entre ells amb tanta confiança i intimitat?» La perspicàcia del vell, baldament poc conreada, començava a veure en allò cert corrent secret d’utilitat que son fill explotava amb tota la malícia refinada d’un cortesà o, si voleu, d’un industrial d’avui. El pare tot just naixia i, com diu l’endevinalla del foc, el fill ja saltava pels terrats». Durant el sopar (que, com podeu comprendre, era confiat a un *intel·ligent* restaurant per més que tingués lloc a la mateixa perruqueria) la conversa fou poca al principi, s’animà un xic quan les viandes i els vins començaren a fer bullir els cervells, i en aparèixer el xampany esclatà amb tota sa força. El perruquer, sense immutar-se i com si hi estigués ja avesat, alçà la copa i pronuncià un discurs, en el qual «la premsa, la veu de l’opinió pública, la gran palanca del progrés», quedava col·locada per damunt totes les potestats de la terra. —Perquè —deia el perruquer— ningú com vosaltres s’associa i protegeix totes les empreses, per humils que siguin. La premsa, de la mateixa manera assisteix als convits de *palacio* que als d’un modest industrial com jo: jamai desdenya ningú, i la mateixa confraternitat que guarda entre tots sos representants, sense distinció de partits, dispensa a totes les classes socials. Els comensals aplaudiren estrepitosament. El forner plorava i reia alhora. Llavors començaren els brindis dels periodistes. L’un donà, en nom de la premsa (no barcelonina ni espanyola, sinó del món), les gràcies al perruquer pels elogis que havia prodigat a «la més gran institució dels nostres temps». Un altre procurà demostrar que no hi ha acte humà, per insignificant que sigui, que pugui deixar d’ésser útil a l’obra del progrés; i afegí que per això la premsa, «com havia dit molt bé l’amo de l’establiment», no es desdenya d’associar-se a la més petita de les empreses, protestant, però, que pogués qualificar-se de petita la d’una perruqueria, on l’art de la decoració, que purifica el gust, i l’art de tallar els cabells, que rodoneja i perfecciona el bust de l’home civilitzat fins a impedir, ja al primer cop d’ull, que es confongui amb el salvatge, corren units. El cervell del Forner de les Monges estava a punt d’esclatar. Un altre, amb dades estadístiques que duia apuntades a la memòria, provà que en el món civilitzat no hi ha encara sinó un perruquer per cada cinquanta mil homes, i que, si bé a Barcelona no existia tal desproporció, podia, de totes maneres, assegurar-se que el servei no s’havia explotat prou i que, per tant, la nova perruqueria tindria aviat bona parròquia. Aquesta última paraula inspirà un altre brindis, en el qual es sostenia que l’art de tallar i pentinar cabells no era indiferent al mateix catolicisme, com ho provava la diferència de pentinats que usaven els frares, i els edictes d’excomunió que en temps antics havia hagut d’expedir l’Església contra certes modes exagerades que, a Anglaterra i altres nacions, daven a l’home i a la dona l’aspecte d’éssers monstruosos. De tot això deduïa l’orador que la inauguració d’una perruqueria no era, en realitat, cosa desposseïda d’importància ni d’interès per a tots aquells que, estudiant l’essència de la civilització, no creuen que sigui element de progrés tota cosa nova, sinó tota cosa bona. Ja no dic ara el que passava per l’enteniment del bon forner. «Mai, mai no ho hauria somiat! Estava rodejat de savis molt més grans que el doctor Andreu!… I qui els havia aplegat, qui els obsequiava i en rebia elogis, era en Miquelet! El seu fill, Senyor, el seu fill!» Una vaga esperança d’ésser conegut entre aquells senyors tan il·lustrats llampeguejà per l’esperit d’en Daniel. Aquell espectacle de barreja i confraternitat entre persones que ell veia amb els ulls d’una il·lusió daurada el transportaven a l’esfera dels entusiasmes més candorosos. Remembrà la *sentència* del doctor Andreu, i mai no la veié tan justa com aleshores. Tots els veïns del seu poble li semblaren dignes de la major compassió. «Viure a Barcelona era viure.» Allò que havia vist de dia, el que veia ara, era un somni que convidava a fer-ne una realitat perpètua. «Estava resolt: a ses velleses s’havia de fer barceloní.» Li entrà l’afany de viure entre *persones*, i, sentint-se com superior a si mateix, volgué brindar. El Rumbós, que era a l’altre cap de taula, dissimulà amb estudiat somrís el temor que son pare fes alguna atzagaiada. Totes les veus callaren; tots els ulls cercaren, per sobre les estovalles, un punt en què fixar-se. Aquest silenci desconcertà el nou orador ja abans que desclogués els llavis; però el compromís era contret, el cor volia parlar. —Senyors —digué, per fi, en Daniel—, estic molt agraït… però molt… i… brindo per la salut de vostès i de totes les seves famílies… i… no dic res més perquè no puc. I caigué aplomat a la cadira, plens de llàgrimes els ulls, mentre l’auditori en pes l’eixordava amb sos aplaudiments tot mossegant-se els llavis. Dos o tres discursos més, d’altres tants periodistes, coronaren aquell monument d’oratòria; i, abans d’aparèixer el cafè, vingué la gran sorpresa que el Rumbós havia preparat amb el major secret. Era, aquesta, una mena d’urna de metall blanc, tapada i d’elegant forma etrusca. Col·locada al mig de la taula, tothom l’havia presa per un objecte d’adorn destinat a augmentar la magnificència de l’agençament entre les flors, els canelobres, les copes, les ampolles i els coberts, on s’emmirallava la llum de gas, desfent-se en mil colors, si viu i inquiet l’un, més encara l’altre. Però no era aquesta la destinació de la tal urna. En Miquelet s’alçà, pitjà el botó amb què acabava la figurada tapa, i per cent brolladors (de tan petits, invisibles) eixiren, impel·lits amb força, altres tants raigs d’aromàtica essència, que refrescaren aquella atmosfera i ompliren el saló de delitosa fragància. No cal pas dir si el truc fou celebrat: tot el concurs picava de mans, rabejant-se de gust, amb la fruïció sibarítica d’un romà del baix imperi, dins la suau ruixada d’aquella boira enervadora. La flaire de les tasses de cafè s’enlairava vaporosa fins al sostre amb els cercles de fum dels cigars; les espelmes feien una llum més blanca i prima; les bombes de gas prenien un to blavenc; i un somni de tendresa i benestar embadalia els esperits. L’endemà, els diaris de la ciutat, sense exceptuar-ne ni un, pintaven la nova perruqueria com un palau de fades, elogiaven totes les innovacions importades pel Rumbós, i daven compte dels industrials que havien intervingut en el decorat, encara que en això alguns incorrien en errades o mala atribució de noms.
`, String.raw`# II {style="capitol" art="cap2"}
Mostra en Markdown · 30 línies · content.c2.en.md Les impressions abans descrites, i la sort que en Miquelet estava fent, engendraren en en Daniel el desig de llogar el forn i traslladar-se a Barcelona a passar els darrers anys de sa vida. Prou havia treballat, ja, perquè ningú pogués criticar que cap a ses velleses es lliurés al descans. Els cabals li ho permetien, i, de més a més, son fill li oferia, de bon grat, plat a taula i un bon llit. Amb tals propòsits, se’n tornà en Daniel al poble on tants anys havia viscut feliç. Si no hi hagués arribat amb la ferma resolució de deixar-lo, potser l’hauria acabat de resoldre el trist desencant que sofrí en petjar novament aquells carrers. Era a la tardor, i el nostre home hi arribava ja de vespre. Els que sols han vist els pobles a l’estiu, no poden judicar de la tristor que desperten a entrada d’hivern, i de com aquesta tristor creix quan el sol s’és amagat darrera les muntanyes. La primera oreneta que s’allunya és, per als pobles, la precursora de la soledat i la quietud. Darrera d’ella segueixen les altres, deixant buits els nius, abans calentons i animats, que el vent i la pluja esgrunaran. Els masos queden barrats; els pocs cotxes i tartanes de luxe passen a ensopir-se en els magatzems, on ben aviat els cobrirà una capa de pols i de teranyines; les diligències no poden donar l’abast a endur-se’n els forasters que durant tres mesos han animat el poble i sos entorns. S’han acabat les fontades, les balles, les festes majors, el brogit i l’alegria: tota la vida es reconcentra dins les cases, i ben aviat carrers i camins seran del domini exclusiu del vent. Quan el Forner de les Monges entrà en son carrer, el crepuscle havia terminat. Ja no hi trobà els rotllets de gent que pocs jorns abans hi prenia la fresca, ni veié oberta la botiga del fuster amb les peces de treball endreçades, el sòl regat i el menjador tribuna que en el fons s’albirava a la tèrbola claror d’un llum de petroli a mitja fosca. Tot era tancat, no passava ànima vivent, i sobre les desiguals teulades s’estenia, com tenderol esqueixalat, una llenca de cel blau turquí sembrat d’estrelles. En mig d’aquella soledat sentí xisclar la garlopa, grunyir la serra i copejar el martell del fuster. Els sorolls arribaven esmorteïts al cor d’en Daniel: per una vaga associació d’idees, cregué que el fuster feia una caixa de mort. Deu passos més amunt dringava, com campaneta llunyana, un martell sobre l’enclusa, i, per la portella d’una escaleta, en Daniel veié son veí, el manyà, entre la resplendor rogenca d’una estrella d’espumes que, al compàs dels cops, estirava o encongia ses punxes com eriçó de foc en turment. En Daniel va empènyer la feixuga porta de sa casa, davant la qual se li mostrà l’encesa boca del forn, plena de brilladores flames que, saltant pels aires i llepant l’estreta porta de llur presó, semblaven celebrar la benvinguda de l’amo. La fleca s’omplí d’alegria. Tothom deixà la feina voltant en Daniel, que no sabia per on girar-se ni com respondre a les cent preguntes que li endreçaven alhora. Quan arribà al pis i guaità a la finestra com portat d’un tèrbol desig de comparació, el carrer oferia la més trista soledat. El vent s’arrossegava per terra empenyent polsoses i alades fantasmes, la fosca feia invisibles les fatxades, i sols el cant quasi perdut d’una mare que bressava son infant, barrejat amb el no menys frèvol d’unes cosidores i el llarg rebombori d’un garranyic de cancell, trencava aquella quietud de cementiri. En Daniel es sentí contristat. Tot Barcelona li bullia en el cervell, i debades cercava en aquell fosc carrer els atractius que hi havia trobat durant tota sa vida. —*Al menos* fes lluna! —deia interiorment. I era que enyorava tots aquells rius de claror que en arribar la nit inundaven la ciutat; la resplendent claredat de la perruqueria; la il·luminació *veneciana* dels cotxes i tramvies que des del balcó de casa son fill veia passar corrent i fent capritxoses giragonses. No cal estar-hi avesat de molt temps: la bona llum artificial s’enyora així que els ulls n’han fruït algun dia. Penseu, doncs, el que devia passar al nostre forner, que s’havia vist en un establiment on, des del negre barret de copa del parroquià fins al mateix mosaic del sòl, tot lluïa, tot reflectia la clara llum de cent becs de gas. Cregué ja impossible de viure alegre en aquell poble tan fosc, tan brut, tan solitari, i recordà el sopar de la premsa, el proverbi del doctor Andreu, els avenços d’en Miquelet, els oferiments que aquest li feia, la promesa d’acceptar-los, els motius que per a descansar tenia… i, tancant la finestra, es ficà al llit, més resolt que mai a abandonar el seu poble.
`, String.raw`# III {style="capitol" art="cap3"}
Mostra en Markdown · 30 línies · content.c3.en.md Fàcil fou a en Daniel d’arrendar el forn: ja no tant de deixar aquelles parets que, després de sos entusiasmes de moment i en començar a esvair-se-li el record de Barcelona, li tornaven a guanyar el cor revestint-se de tota la història d’una existència feliç, menyspreada de sobte i potser sense motiu prou just. Però la resolució era ferma: el forn era arrendat; ja no li quedava, en el poble, sinó una filla casada, amb la qual no vivia. «S’ajuntaria amb son fill, tractaria *persones*, i mil divertiments l’ajudarien a passar el temps que aquí, sense treballar, li havia d’ésser insuportable. Endemés, els estius hi tornaria: que per ventura no la veuria mai més, la casa? Doncs bé podia deixar-la.» I, tot espurnejant-li els ulls i fent el cor fort, aquell home, que pocs dies abans havia sentit tanta tristesa en arribar al poble, el deixà llavors amb una recança infinita. Però dins la colossal figura d’en Daniel alenava un esperit d’infant que, com tots sos consemblants, es sentia més decantat als alegrois frívols que a la tristor. Per això, quan veié les fileres de fanals que li anunciaven la nova arribada, l’home oblidà ja la poesia de sa casa i del seu forn, i pensà altra volta que, entre allò que deixava i el que la ciutat li oferia, l’elecció no era dubtosa. Oblidava, el pobre Daniel, que Barcelona no podia oferir-li novetats diàries, i que la força del costum, a certa edat, és del tot aclaparadora. En sa primera estada ja havia recorregut quasi tota la ciutat; son fill, enfeinat en la perruqueria, no podria acompanyar-lo: hauria de passejar tot sol; i, si vint-i-quatre hores diàries de renda són insuportables als mateixos ganduls, quant més no ho havien d’ésser a un home acostumat, com ell, al treball? L’endemà de l’arribada començà a tocar-ne els resultats. Avesat a aixecar-se quan les amassadores havien deixat pastada una fornada de pa, es despertà a l’hora en què això acostumava a succeir, que per a la perruqueria era a deshora; i, com que ara no hi estava en qualitat de foraster ni volia desvesar-se de matinejar, l’home saltà del llit. L’alba tot just clarejava el que era indispensable perquè a sa llum incipient pogués el forner vestir-se, endevinant amb el palp quina era cada una de les peces del seu vestit que penjaven de les cadires com draps de color incert. Tots els altres de la casa dormirien encara tres hores, i la mateixa escalforeta i quietud que regnava en dormitoris i sales feia caminar de puntetes i contenir l’alè al que acabava de llevar-se. D’altra part no sabia on eren les claus del pis ni la de la porta forana, i, per tant, no podia eixir al carrer. Si algun jorn us veieu en tals condicions, tancats dins una cambra que, per tot esbarjo, té una finestra de celobert, sabreu què és patir i comprendreu l’angúnia que degué passar en Daniel aquella matinada. I com, al seu poble, una nit demanava lluna, aquest dia demanava sol a ciutat. «Per tot hi ha cent llegües de mal camí!», mormolaren els seus llavis. I, no podent ja suportar el lligament de sa presó, mal s’exposés a despertar els de la casa, travessà el pis i obrí un dels balcons que daven a la Rambla, per esperar més distretament que els altres s’aixequessin. Els fanalers, mig ensopegant de son, anaven apagant el gas, que, tot esgrogueït, semblava estar ja cansat de fer pampallugues; un que altre cotxe tancat, un que altre carro buit, i una que altra persona mig perduda per la deserta via, formaven tot el moviment d’aquella Rambla que la nit d’arribada, amb sa constant bellugadissa, havia marejat en Daniel. Però, a poc a poc, el vel cendrós que s’estenia del port a la muntanya anà replegant-se sobre aquesta, arrabassat per un fresc oreig capaç de despertar les potències més adormides. Els arbres redreçaren les poques fulles que encara conservaven verdes, i remogueren llur brancatge com per espolsar-se la son; sota un cel de blau puríssim lluïren les punxes dels parallamps, els ferros i vidres dels campanars i torratxes, simulant enlluernadors estels; anaren obrint-se portes i finestres; i un formigueig de gent i carruatges, cada instant més gros, començà a bellugar per tots cantons com eixam d’abelles a l’entorn d’un buc. Un xic més tard, els aprenents de la perruqueria es posaren a despertar el *saló* a cops d’espolsadors, a raspallar-lo amb les escombres, a rentar-li la cara, a endreçar-lo i vestir-lo; en una paraula, a deixar-lo a punt de rebre les persones que aviat començarien a visitar-lo. Quan el forner baixà l’escala, el Rumbós encara dormia, però el *saló* era ja net, lluent, estirat com tot un senyor que sap honrar sa dignitat pròpia i vol que jamai ningú, per de matí que es llevi, no pugui trobar-lo desprevingut. No diré que en Daniel passés el dia enujat o avorrit: encara el contrast de l’animació de Barcelona amb la soledat i quietud de son poble el tenia entretingut i distret; però no tant, per això, com en sa primera estada. Els edificis i les botigues ja no li oferien novetat: ja quasi sabia de cor què trobaria a la dreta i què a l’esquerra en emprendre tal carrer o plaça de les principals, i per experimentar noves impressions havia d’allunyar-se fins a barris que li eren desconeguts, o bé escaure’s davant algun espectacle d’aquells que sempre atreuen badocs per més que es repeteixin, com són els arrencaqueixals, els ocellets de la bonaventura, l’orquestra de cecs de Sant Gaietà, les mones que s’enfilen pels balcons, i tants i tants altres, que ell, al cap de dos dies d’haver arribat del poble, sabia més que tots els habitants de la ciutat. Un jorn visitant la Barceloneta, veient, un altre, el castell de Montjuïc, ara anant a Pedralbes, o a Can Tunis, o a Vallcarca, o a la Bonanova (en una paraula, recorrent els encontorns que no havia pogut veure en son primer viatge), passà encara una mesada distreta; però no per això en Daniel es sentia ben centrat. Havia perdut, sense saber com, l’alegria que per tants anys resplendí en son cor: per sol que fes i per boniques que fossin les coses, li apareixien rodejades de certa ombra misteriosa que li causava més enuig que joia. Recordava el poble, sa passada vida, i anava convencent-se que podria encara suportar-la bé. Altres voltes evocava el record del sopar de la premsa, pensava en la sort que estava fent son fill, i sentia entrar en l’esperit un nou raig d’esperança i felicitat. Però en arribar a casa després de quatre o cinc hores de passejar-se sol sense obrir la boca, i en veure’s obligat a romandre assegut en el rebedor com un porter, o bé en el menjador, sense poder conversar amb ningú perquè així ho exigia la feina, el nostre home tornava a caure en un pou de melangia. Ell mateix no sabia donar-se compte del que li passava, ni arribava a descobrir la causa principal d’aquell malestar. —No tens bona taula, no tens bon llit, no *disfrutes* de la major llibertat i independència? Per ventura ton fill no et tracta bé? No ha de servir-te de satisfacció veure com avança son establiment, i no et trobes tu on desitjaves de molts anys ençà? Totes ses respostes havien d’ésser afirmatives… i, això no obstant, no quedava satisfet. La privació que sentí la primera matinada s’havia corregit el mateix dia proveint a en Daniel de les claus corresponents perquè fos amo d’entrar i sortir quan li fes pler; i, com aquesta, son fill havia anat destruint, una per una, totes les altres privacions de què aquell s’hagués queixat. Cada dia procurava assenyalar-li nous passeigs, i les vetllades de poca feina l’acompanyava a veure algun teatre o cafè mentre l’home volgués anar-hi, que fou ben poques vegades, perquè preferia dormir en el llit a adormir-se en públic.
`, String.raw`# IV {style="capitol" art="cap4"}
Mostra en Markdown · 62 línies · content.c4.en.md Una vetlla en Daniel entrà molt content: havia topat un conterrani seu i parlat llargament amb ell del poble. Era la primera vegada, fora de les poques que eixia amb son fill, que havia passejat per Barcelona parlant. Aquell mutisme en mig de la multitud era una de les coses que més el posaven de mal humor. Recordava que en el seu poble tothom el saludava, i no podia avenir-se a travessar per entre mils i mils persones com ombra indiferent. Per això, en mig del soroll confós de la multitud, en Daniel hi trobava un silenci glacial molt més trist que el de la quietud i la soledat. Per això, a voltes, les cames se l’emportaven instintivament a indrets solitaris; i, un dia que en Miquelet se n’estranyà, son pare li respongué que el moviment l’entristia, que el centre de Barcelona li produïa l’efecte d’un cementiri on els morts es passegessin. Son conterrani trobà en Daniel decaigut, molt més blanc de cabells i «com si s’hagués escurçat o s’hagués posat calces de creixença». Observà també que mentre li parlava de Barcelona quasi no escoltava, i que en canvi s’entretenia molt a remembrar son poble, a preguntar per tothom, a informar-se de tot el que hi havia passat d’ençà de sa marxa, deixant caure alguna llàgrima sobre les noves dels morts o naixements que aquell li comunicava. Aquestes observacions van ésser comentades en el poble, i atribuïdes a desavinença entre pare i fill per uns, a un principi de feridura per altres. A la nit en Daniel somià son poble. Es veié, de primer, a si mateix passejant amb la Roser, la seva dona, pel cementiri cobert d’herbam barrejat amb roselles i campanetes blaves que s’entortolligaven en les poques creus que hi havia; veié després un cel molt negrós, en el qual una munió d’estrelles titil·laven sense parar; i, de la pròpia manera que havia passat de la claror del sol a plena nit sense crepuscle, es trobà, també sense saber com, dins de casa, en cos de camisa i amb la pala a la mà, davant d’un forn encès com un segment del sol. Sa llum, escapant-se per la boca, queia sobre l’enrajolat en forma pareguda a la del cercle lluminós d’una llanterna màgica; i, al mig d’ella, en Daniel veia l’ombra de sa pròpia figura escurçant-se o allargant-se a mercè de l’inquiet reflex de les flames i escarnint monstruosament les contorsions que ell amb la pala feia. Mentrestant, el pa, dins el forn, s’inflava i s’estovava com llavor dins bona terra. Poc després esdevenia de color d’or, i calia veure maniobrar en Daniel per muntar-lo a la pala i atreure-se’l en mig del riu de flames que li disputava la presa. En arribar aquí, un xiscle mig ofegat trencà la solemne quietud del dormitori. En Daniel es tombà sobre l’altre costat, i lliscà per sos llavis com un suau somrís; l’últim adéu de l’alegria que el visità aquell jorn. Després… son esperit reposà profundament. En despertar, sos ulls van clavar-se un instant en el balcó del celobert, i una llàgrima vergonyant s’escolà per llurs pestanyes. Llavors en Daniel rumià quant mancava per a arribar a l’estiu. Tot just era pel gener: cinc mesos li van semblar un segle. Però, com que el mateix desig de consol li engendrava noves esperances, l’home en concebé ben aviat una, i fou que anant a l’estació del carril veuria arribar algú altre del seu poble. En Daniel no pensà que, essent aquest a la província de Tarragona i trobant-se a cinc hores del ferrocarril, els seus veïns venien a la ciutat comtal sols per causes extraordinàries, i que, component-se de curt nombre d’habitants, pocs eren els que durant l’any visitaven Barcelona. Heus aquí, doncs, el bon home fent, dies i dies, debades, el camí de l’estació, anant-hi amb una esperança, tornant-se’n amb un desengany. L’enyorança, mentrestant, anava minant-lo cada dia més. En Daniel no semblava ja el gros Forner de les Monges: la roba li venia cada cop més ampla i com més llarga, se li havien tornat blancs tots els cabells, i un cercle blavenc d’amarga tristor que voltava sos ulls li dava un aspecte desencaixat, quasi cadavèric. En Miquelet, amb tot i la poca atenció que podia dispensar-li, se n’adonà, i volgué dur-lo a casa un metge. Tot fou inútil. El malalt havia posat mal geni. No sols no tenia fe en els facultatius desconeguts, sinó que (cosa pitjor) ni volia curar-se. Avorria el seu estat, avorria la vida, i notava en si mateix un misteriós afany que no sabia definir-se sinó com un cert desig de córrer, de córrer molt i empènyer les hores, que trobava molt llargues. La gran excitació en què vivia el feia passar del matí a la nit per tota una escala d’exaltacions i abatiments. A la senzilla vista d’una fleca en què el comprador no pot albirar el forn d’on surten els pans, es posava furiós. Altres voltes l’hauríeu vist dolçament entretingut en la contemplació de les flors o dels ocells que es venen a la Rambla. Si li haguessin preguntat què hi veia, per què s’hi delitava, hauria de segur respost que jamai, fins llavors, no s’havia fixat en flors ni en ocells, i que, no obstant, ara els contemplava amb gust, sense saber per què! Potser, sense adonar-se’n, hi descobria una secreta relació amb la primavera, precursora de l’anhelat estiu! L’home esclau d’un desig no sent pertot arreu veus misterioses de l’objecte de son afany? A les mateixes veus obeïa, sens dubte, la tendència que el forner sentia per la soledat, i més encara pels voltants de l’estació del ferrocarril. Ja no s’acontentava d’anar-hi a l’arribada del tren: cada tarda sos passeigs eren per aquest cantó de ciutat. No semblava sinó que per la via li arribessin flaires de la seva terra sense les quals no podia viure. Una tarda que les cames el portaren més enllà del convent de les Adoratrius, arribà a un punt en què la via s’adreçava i seguia recta fins a l’extrem de la visual, on els rails convergien com els talls d’una llança que es perdia dins el vel morat de la distància. Allí clavà sos ulls, desitjós de traspassar el límit de l’horitzó; i, encisat, sens dubte, per una atracció secreta, va asseure’s al caire mateix del vall o desmunt que formava la via. El sol anava enfonsant-se per l’aiguavés de Vallvidrera oposat a la ciutat; el caseriu de Pedralbes, Sarrià i Sant Gervasi era engolit per una boira cendrosa; i l’ombra gegantina del serrat envaïa solemnement tot el pla, ensems que la nuvolada mudava i ennegria, cada volta més, sos virolats colors. El mateix vall de la via anava enfosquint-se per moments, mentre en son fons els rails prenien la lluïssor de l’aigua a la dubtosa llum de la celístia. L’esguard d’en Daniel lliscava per damunt d’ells com el d’un desterrat per sobre el riu que va a parar a sa llunyana pàtria. El pobre home plorava. No sabia explicar-se com, el camí que un dia li havia ofert tants plaers, podia avui entristir-lo tant. Justificava una volta més la veritat del poeta: l’espectacle el porta l’espectador dins d’ell mateix. Tot d’una en Daniel s’alçà i prengué resolt el camí de la perruqueria amb el semblant animat com de feia molt temps no havia tingut. —Gràcies a Déu que us veig content! —digué son fill—. Sembleu tot un altre! A què es deu aquesta sobtada alegria? —Crec que m’he endevinat el mal, i penso posar-hi cura. M’enyoro: demà me’n tornaré a casa. —Pare! No us torneu boig? Enyorar-se a Barcelona el veí d’un poblet? No recordeu l’efecte que us va causar l’arribar-hi, venint d’aquí? —Aleshores això em semblava un cel, i ara em sembla un cementiri. —Espereu *sisquera* l’estiu. Que per ventura us manca alguna cosa? —Em manca tot i no em manca res: vés-ho a entendre. Soc arbre vell que un malentès ha transplantat. Si em quedo aquí em moriré de tristesa. Deixa’m tornar a casa meva i no ho prenguis a mal. T’agraeixo tot el que has fet per mi, t’ho estimo molt… però me n’he de tornar a casa… Què hi faré, pobre de mi, si m’enyoro… m’enyoro i… En arribar aquí, en Daniel plorava, i en una estreta de mà a son fill va encloure cent protestes d’agraïment i totes les altres raons en què es basava sa resolució. En Miquelet comprengué l’estat de son pare, i el matí següent l’acompanyà fins al carril. Era ja tard. L’alegria és una saba que, si es perd a certa edat, ja no es recobra. En Daniel l’havia perduda per sempre; i, si en retornar a la seva terra pogué per un instant creure-la reconquerida, aviat en tocà el desengany. Ell ho havia dit: era un arbre vell transplantat. I l’arbre vell, un cop arrencat, és inútil replantar-lo en el lloc primitiu: ja mai més no farà brotada. Per què tractar de pintar-vos ara el pobre forner altra volta al seu poble, fora de la casa i del forn que tan incautament havia arrendat, i perdent de dia en dia les forces i els colors, els ulls fixos a terra, enterbolits pel plor, sense delit d’enraonar ni de veure ningú, sense cap goig, sense altra esperança que la d’acabar el sofriment en la mortalla? Ni aquell estiu (temps per ell tan desitjat algun jorn!) no pogué ja veure: pel maig, l’herbam, barrejat amb les roselles i les campanetes blaves, embolcallava ja la creu de ferro que s’aixecava de la tomba del malaurat Daniel i sa muller sota el mantell joliu d’un cel pur de primavera. :::paragraphs{style="fi"} Fi ::: :::paragraphs{style="colofo"} Compost en Crimson Pro, Cormorant Garamond i Cormorant SC (SIL OFL). Text: Narcís Oller, *Obres completes*, vol. VI, Barcelona, 1928, domini públic (Viquitexts). Il·lustracions generades amb models de difusió. :::
`].join('\n\n'); // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // every face the pages use, loaded before the build (gotcha: fonts-first) 'Crimson Pro': ['400', '400i'], // the text, the note, the colophon, the folios 'Cormorant Garamond': ['500i', '600'], // half-title and title; the chapter numerals 'Cormorant SC': ['400', '600'], // the closing Fi; author, labels, running heads }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); await Promise.all(Object.values(PICTURES).map(([file]) => loadImage(file, asset(file)))); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: 'El transplantat' }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider, resourceBytes: imageBytes }), `${RECIPE}.pdf`);
Kit · core, fonts, viewer, pdf, images: igual a totes les receptes · 310 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 · pdf v1 ── the same in every recipe that exports a PDF ────────────── /** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen * used, snapping to a weight the family ships and falling back to upright * when it has no italic: the PDF asks for every face a block could use. */ async function fontsourceProvider(family, weight, style) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); } /** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new * tab, since CodePen's preview frame cannot show PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── 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

#Que els pronoms també es tallin

Amb el compounds: true per defecte, d’inaugurar-se es pot tallar d’inau-gurar-se i la pàgina 7 perd la seva línia fluixa. A canvi, en aquestes pàgines el tall titil- | laven deixa de caure a final de línia.

-  hyphenation: { compounds: false },
+  hyphenation: { compounds: true },

#Obrir cada capítol en pàgina senar

parity: 'odd' afegeix una pàgina en blanc abans d'un capítol que començaria en parella, com en una edició més folgada. El capítol IV, que s'obre a la pàgina 26, passa a la 27 després d'una pàgina en blanc.

-  breakBefore: { enabled: true, parity: 'any' }, // a chapter opens on the next page, either side
+  breakBefore: { enabled: true, parity: 'odd' },

Errors freqüents

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

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

Un estil de títol hereta el salt de pàgina del seu nivell

Una entrada de headingStyles pren del seu nivell de títol tot allò que no fixa, també breakBefore. Un índex o un colofó amb estil sobre un H1 després d'un :::pagebreak hereta la paritat 'odd' i cau darrere d'una pàgina en blanc. Dona a aquest estil breakBefore: { enabled: false }. Estils de títol →

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 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

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

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 →

  • Copia el text de Viquitexts amb les seves cursives: Oller marca tentu, Ensanxe i palacio com a col·loquials o castellanes, i una exportació en text pla les perd. Acara els mots dubtosos amb el facsímil del volum de 1928: sis vegades la transcripció porta tom- on el verb és tornar (tomà, tomaré, tomar), i una vegada jom per jorn.
  • Amb 6800 paraules, els cinc fitxers de contingut i el kit deixen l'script compost en uns 69 KB. Un llibre sencer és la mostra més llarga que cap en un pen; una obra més llarga va en un paquet .postext.

Crèdits

Text
  • El transplantat, complete, from the Obres completes, vol. VI (Barcelona, 1928), with misprints corrected, no opening ¿ ¡ and today’s dialogue dashes · Narcís Oller · domini públic
  • The note to the edition · Postext Cookbook · original
Imatges
  • Engraving, frontispiece: the baker at his oven's door · Generated With Diffusion Models · original
  • Engraving, chapter I: the night train into Barcelona · Generated With Diffusion Models · original
  • Engraving, chapter II: the village street in autumn · Generated With Diffusion Models · original
  • Engraving, chapter III: the Rambla at dawn · Generated With Diffusion Models · original
  • Engraving, chapter IV: the old man by the railway · Generated With Diffusion Models · original
Fonts
Crimson Pro (SIL OFL 1.1) · Cormorant Garamond (SIL OFL 1.1) · Cormorant SC (SIL OFL 1.1)
SandboxPDF