Skip to main content
Recipe number 104

Cookbook · Chapter 2 · Type & text

A Catalan novella, hyphenated by the IEC rules

Narcís Oller’s El transplantat, complete, as a 32-page pocket book: Catalan syllables by the IEC rules, l·l broken as il- | lusió, and an engraving per chapter.

On this page

pp. 2–3 of 32

  • Trim 120 × 185 mm
  • 1 column
  • Crimson Pro 10.4/14
  • Cormorant Garamond
  • Cormorant SC
  • 32 pages
  • Level
  • Postext 1.14.0
  • Laid out in 899 ms
  • 179 lines of code

In short

A whole short novel in Catalan, set as a small paperback. Words are split at the line end the way Catalan style guides ask, including the double l with a dot.

What you'll build

Narcís Oller’s El transplantat in full, in the text of his Obres completes (Barcelona, 1928), as a 12 × 18.5 cm pocket book of 32 pages. A half-title, a frontispiece of the baker at his oven, a title page in red italic Cormorant Garamond and a note on the edition come before the four chapters. Each chapter opens under an engraving twelve lines deep, with its Roman numeral in red. The text, Crimson Pro at 10.4 pt, breaks at Catalan syllables as the Institut d’Estudis Catalans asks: two letters at least on each side, no split between vowels in hiatus, none right after an apostrophe, and titil·laven breaks as titil- | laven, the hyphen taking the place of the middle dot. Dialogue is set with em dashes and quotations in «cometes baixes».

This recipe answers

  • How do I get good justification and hyphenation for Spanish, French or German text?
  • How do I give every chapter its own photo, colour or opener variant?
  • How do I set unnumbered artwork: ornaments, vignettes, logos?
  • How do I write dialogue dashes, years at a paragraph start, prices and literal symbols without Markdown misreading them?

The short answer

script.js · lines 48–62in full code
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,
};

Catalan by its own code: l·l breaks as il- | lusió, no word splits its hiatus

Ingredients

Type
Crimson Pro, Cormorant Garamond, Cormorant SC (SIL OFL 1.1)
Assets
  • 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)

Method

#1 · Catalan by its code

The code is the short answer above. locale: 'ca' selects the Catalan patterns, and since postext 1.14 they follow the IEC’s Llibre d’estil (supported locales). The patterns are the ones TeX uses, with two letters at least on each side of a break instead of the three the npm package ships, so ter-ra, cai-xa and plu-ja can break. Vowels in hiatus stay together (cièn-cia, ca-mions), but an i between vowels opens its syllable (fe-ia). The ela geminada breaks between its two l: the line ends titil- and the next opens laven, with no middle dot on either side, and Knuth–Plass measures the line without the dot.

Page 27: titil·laven at a line end, set titil- | laven. It is the one l·l that falls at a line end in these 32 pages.

compounds: false keeps a verb and its pronoun whole but for their own hyphen (arrossegar- | se), which the IEC prefers (hyphenation). It costs one loose line: on page 7, the line before d’inaugurar-se opens its spaces to 1.94×, even with maxJustifyTracking: 10 taking the excess on the other lines.

#2 · A pocket page of whole lines

script.js · lines 37–44in full code
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
};

The bottom margin comes from the trim, the top margin and LINES, so the text block holds 29 lines of 14 pt and every full page ends on the same baseline. The 89 mm measure holds about 64 characters of Crimson Pro. mirror: true keeps the 16 mm margin at the spine on both pages of a spread (mirrored margins).

#3 · One opener for four chapters

script.js · lines 66–81in full code
// # 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}' draws the picture the heading names, so # II {style="capitol" art="cap2"} opens chapter II under its own engraving with the same style as the others (heading attributes). Images reserve no height in an opener, so minHeight holds sixteen lines: twelve for the picture, then the numeral and its rule, and the text starts on the seventeenth line of the page, the same in every chapter.

#4 · Four pages before chapter I

script.js · lines 85–114in full code
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,
];

Each piece of front matter is a level-one heading with a style of its own: # El transplantat {style="portada"} prints the author and the subtitle from the frontmatter and the heading as the title. Every style restates its page break, since a style inherits its level’s, and empties the header and footer, so these pages carry no running heads or folios. The frontispiece falls on page 2, a verso, so it faces the title page across the spread.

#5 · The author on the verso, the title on the recto

script.js · lines 118–132in full code
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 puts the author on versos and the title on rectos; pages: 'body' keeps both off the openers, where a drop folio with pages: 'opener' takes their place. SHIFT moves each head onto the centre of the text block, half a millimetre off the page’s centre because the two margins differ.

#6 · Five engravings, no labels

script.js · lines 136–152in full code
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 } }));

The pictures belong to a resource type with an empty captionPrefix, so nothing numbers or labels them. Each is declared at its pixel size and loaded under its file id before the build. Their white was mapped to the page’s cream and their black to its ink before they were saved, so they print as part of the page and not as photographs pasted on it.

The whole recipe

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 = 'en'; // @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`---
Markdown sample · 18 lines · content.en.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"}
Markdown sample · 54 lines · 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.
Markdown sample · 48 lines · 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"}
Markdown sample · 30 lines · 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"}
Markdown sample · 30 lines · 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"}
Markdown sample · 62 lines · 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: the same in every recipe · 310 lines// ─── 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 ───────────────────────────────────────────────────────────────────────

The composed script.js runs as it is: paste it into any page’s module script, or open the recipe on CodePen. Recipe folder on GitHub ↗ (opens in a new tab)

Variations

#Let pronouns divide too

With the default compounds: true, d’inaugurar-se can break as d’inau-gurar-se and page 7 loses its loose line. On these pages the titil- | laven break moves off the line end in exchange.

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

#Open every chapter on a recto

parity: 'odd' adds a blank page before a chapter that would start on a verso, as in a roomier edition. Chapter IV, which opens on page 26, moves to page 27 behind a blank page.

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

Pitfalls

Pitfall

Only 8 locales hyphenate, by exact code

Hyphenation ships for en-us, es, fr, de, it, pt, ca and nl, matched exactly: 'es-ES' or any other language silently falls back to American English. Hyphenation and document language →

Pitfall

An opener's images never count towards the height it reserves

In postext 1.4.1 an advanced-design heading measures the height it reserves without its images: its texts, rules and boxes count, even when anchored to the page, but an image, such as a picture bled across the head of the page, reserves nothing, so the text can start on top of it. Set minHeight to where the text should begin. Designed openers →

Pitfall

A heading style inherits its level's page break

A headingStyles entry takes every field it leaves out from its heading level, breakBefore included. A contents page or a colophon styled on an H1 after a :::pagebreak inherits parity 'odd' and lands behind a blank page. Give such a style breakBefore: { enabled: false }. Heading styles →

Pitfall

Any headings object switches off the H1 page break

By default an H1 breaks to a recto (always-odd), but passing any headings object resets that default, so chapters run on and span: 'page' does nothing. Restate headings.levels[0].breakBefore: { enabled: true, parity } in every config. Chapters that open on a recto →

Pitfall

A swapped palette misses design elements and the reference colour

postext 1.4.1 reads colorPalette into the text styles (body, headings, lists, captions, tables, boxes) but not into the elements of headers, footers, openers and part pages, nor into bodyText.referenceColor: they keep the hex written beside their paletteId. When you swap the palette, for a dark screen edition or a retint, rewrite every linked colour from colorPalette before the build. Semantic colour palette →

Pitfall

Design text overflow defaults to 'ellipsis-end'

A design text element that does not fit its width ends in an ellipsis by default. Set overflow: 'wrap' for titles that should break onto more lines. Text, rules and boxes in page designs →

Pitfall

Attribute values: no { or }; single-quote a value with "

An attribute value ends at the closing brace, so it cannot hold { or }. A value that contains a double quote goes in single quotes; a dollar sign is fine. Heading attributes →

Pitfall

Quote every frontmatter value

YAML reads title: 1984 as a number and a date as a Date object, and non-string values print empty in placeholders and leave the PDF without a title. Quote every value: title: "1984". Document metadata →

Pitfall

Load every face before layout

Layout measures text with the faces the browser has loaded and caches the widths, so a face that arrives after the first build leaves wrong line breaks and a PDF that no longer matches the screen. Load every weight and style first, and call clearMeasurementCache() before rebuilding when one arrives late. Fonts before layout →

  • Copy the text from Viquitexts with its italics: Oller marks tentu, Ensanxe and palacio as colloquial or Spanish, and a plain-text export drops them. Check doubtful words against the scan of the 1928 volume: six times the transcription has tom- where the verb is tornar (tomà, tomaré, tomar), and once jom for jorn.
  • With 6,800 words, the five content files and the kit make the composed script about 69 KB. A whole book is the largest sample a pen can carry; a longer work belongs in a .postext bundle.

Credits

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 · public domain
  • The note to the edition · Postext Cookbook · original
Images
  • 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