En poques paraules
Cinc pàgines d'un llibre d'assajos de butxaca. Ensenya a posar el títol del llibre a dalt de les pàgines de l'esquerra i el de l'assaig a les de la dreta.
Què compondràs
Les pàgines 29 a 33 dels Assajos de Montaigne en edició de butxaca i traducció nova: el capítol IV sencer i l'inici del V. Les parelles porten el títol del llibre en versals espaiades, i les senars, el de l'assaig en cursiva, tallat amb punts suspensius perquè el del capítol IV té 87 caràcters. Cada foli va en una pestanya sèpia al marge exterior, a 3 mm d'un filet de 0,5 pt a la vora de la caixa. Les obertures només tenen un foli al peu, i la pàgina en blanc anterior al capítol V no porta capçalera ni foli. El text va en Baskervville sobre paper ossi; els títols, en Libre Caslon Display, i els folis i el títol del llibre, en Alegreya SC. La configuració defineix totes les capçaleres, així que la del motor (Open Sans de 8 pt, en blau) no s'imprimeix.
Aquesta recepta respon a
- Com poso capçaleres: el títol del llibre a la pàgina esquerra, el del capítol a la dreta i el foli per fora?
- Com amago les capçaleres a les obertures i a les pàgines en blanc, o pinto una pàgina parella en blanc amb el color de la part?
- Com evito que els títols, les negretes i les vinyetes surtin en blau?
La resposta curta
const GAP = 3; // mm from the tab to the divider, which lands on the text block's edge
const HEAD = 12; // mm from the top edge of the page to the top of the tab
const CAP = t({ en: 60.5, es: 66.8 }); // mm: fitted so essay IV's head ends on a word
// (gotcha: ellipsis-mid-word); a short running title (see Variations) suits any title
const page = (edge) => ({ to: 'page', edge }); // the trim box (gotcha: negative-offsets)
const header = { elements: [
// Verso (even): tab | divider | THE BOOK'S TITLE from the frontmatter (gotcha: quote-frontmatter)
folio({ id: 'folio-even', parity: 'even', pages: 'body', anchor: page('top-left'),
x: OUTER - GAP - TAB, y: HEAD }),
divider({ id: 'rule-even', parity: 'even', from: 'folio-even', edge: 'right-of', x: GAP }),
{ kind: 'text', id: 'book', content: '{title}', parity: 'even', pages: 'body',
fontFamily: 'Alegreya SC', fontSize: pt(FOLIO_PT), lineHeight: LINE / FOLIO_PT,
color: col('muted'), textTransform: 'uppercase', letterSpacing: pt(1.3),
placement: { anchor: { to: '#rule-even', edge: 'right-of' }, // top: the tab's top
offset: { x: mm(2.5), y: pt(PAD.top) } } }, // PAD.top down: on the folio's baseline
// Recto (odd): the essay's title, cut short with an ellipsis | divider | tab.
folio({ id: 'folio-odd', parity: 'odd', pages: 'body', anchor: page('top-right'),
x: -(OUTER - GAP - TAB), y: HEAD }), // from the right edge, a negative x runs inwards
divider({ id: 'rule-odd', parity: 'odd', from: 'folio-odd', edge: 'left-of', x: -GAP }),
{ kind: 'text', id: 'essay', content: '{chapterTitle}', parity: 'odd', pages: 'body',
fontFamily: 'Baskervville', italic: true, fontSize: pt(TITLE_PT),
lineHeight: LINE / TITLE_PT, color: col('muted'),
overflow: 'ellipsis-end', // stated, though default (gotcha: overflow-ellipsis-default)
placement: { anchor: { to: '#rule-odd', edge: 'left-of' },
offset: { x: mm(-2.5), y: pt(PAD.top) }, size: { maxWidth: mm(CAP) } } },
] };
// Openers: no head, only a drop folio HEAD above the page's foot, centred under the text block.
const footer = { elements: [folio({ id: 'drop-folio', parity: 'all', pages: 'opener',
anchor: { to: 'container', edge: 'bottom' }, y: -HEAD })] };
Ingredients
- Funcions
- Capçaleres i folisCapçaleres segons el tipus de pàginaAncoratge d'elements de dissenyTextos, filets i caixes als dissenys de pàginaObertures dissenyadesAtributs de títolSalts de línia als títolsCapítols que obren en pàgina senarMarges simètricsFormat de pàginaColor del paperMetadades del documentNegreta, cursiva i els seus colorsPaleta de color semànticaRequadresEstils de paràgrafVídues, òrfenes i línies curtesPàgines en un canvasLlibres construïts capítol a capítol
- També fa servir
- Banda de capítol d'amplada completa
- Tipografia
- Baskervville, Libre Caslon Display, Alegreya SC (SIL OFL 1.1)
- Recursos
- Cap: totes les imatges es dibuixen en codi
Elaboració
#1 · Substitueix la capçalera blava de sèrie
const palette = {
ink: '#1f1b16', // text: a warm near-black, never #000
sepia: '#8a5a2b', // the one accent: folio tabs, dividers, opener plates (5.3:1 on paper)
muted: '#6f6558', // running heads, verse glosses, the colophon (5.2:1 on paper)
paper: '#f8f4ec', // a warm off-white page; also the type reversed out of the sepia
};
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' } })),
// Text-style defaults (headings, bold, italic, lists, boxes) link to 'main-color'. The
// built-in header and footer keep #295AA3, and design slots never read the palette
// (gotcha: palette-skips-designs), so every element colour here carries its hex.
{ id: 'main-color', name: 'sepia (defaults)', value: { hex: palette.sepia, model: 'hex' } },
];
Cada color de la configuració enllaça amb una d'aquestes entrades. Si deixes header sense definir, el motor dibuixa la seva capçalera de sèrie: {title} a les senars i {chapterTitle} a les parelles sobre un filet d'1 pt, en Open Sans 600 de 8 pt i en blau #295AA3 (encapçalaments i peus). Si main-color pren el valor del sèpia, canvien els colors per defecte del text, però aquesta capçalera continua en blau, perquè les ranures de disseny no llegeixen la paleta. Per això aquí cada capçalera s'escriu sencera, amb els seus colors en hexadecimal. La negreta, la cursiva i les referències també prenen main-color per defecte, i bodyText els retorna el color de la tinta.
#2 · Totes les capçaleres surten d'una pestanya
const FOLIO_PT = 7.5; // pt: the folio, and the book's title in capitals beside it
const TITLE_PT = 8.8; // pt: the essay's title, whose italic lowercase reads small at 7.5
const LINE = FOLIO_PT * 1.2; // pt: the folio and both titles share this line box and baseline
const PAD = { top: 1, bottom: 1.4 }; // pt: 0.4 pt more below for the descending 3, 5, 7, 9
const TAB = 7.5; // mm: the tab's width
const TAB_H = LINE + PAD.top + PAD.bottom; // pt: the tab's height, which the divider matches
const folio = ({ id, parity, pages, anchor, x = 0, y }) => ({
kind: 'text', id, content: '{pageNumber}', parity, pages, fontFamily: 'Alegreya SC',
fontSize: pt(FOLIO_PT), fontWeight: 700, lineHeight: LINE / FOLIO_PT, color: col('paper'),
box: { backgroundColor: col('sepia'), borderRadius: mm(2), // a pill; the number is centred
padding: { top: pt(PAD.top), bottom: pt(PAD.bottom) } }, // by default in both directions
placement: { anchor, offset: { x: mm(x), y: mm(y) }, size: { width: mm(TAB) } },
});
const divider = ({ id, parity, from, edge, x }) => ({
kind: 'rule', id, parity, pages: 'body', direction: 'vertical', thickness: pt(0.5),
color: col('sepia'), placement: { anchor: { to: `#${from}`, edge }, offset: { x: mm(x) },
size: { height: pt(TAB_H) } },
});
La resposta curta ancora a 'page' la pestanya de cada capçalera, així que els seus desplaçaments es mesuren des del tall. Des de la cantonada top-right, una x positiva treu la pestanya de la pàgina; per això la de la senar la porta negativa. De la pestanya penja el filet, i del filet, el títol (posicionament d'elements). Les mesures de la pestanya van en constants perquè les altres peces es mesuren a partir d'aquestes. El filet fa TAB_H d'alt. El foli i els dos títols comparteixen una caixa de línia de 9 pt, i la línia de base d'un text de disseny cau al 80 % de l'alçada d'aquesta caixa, així que tots tres queden sobre la mateixa línia de base amb qualsevol cos.


#3 · Marges en mirall i línies senceres
const TRIM = { width: 132, height: 198 }; // mm
const TOP = 22; // mm: the top margin, which holds the heads (gotcha: header-paints-over-text)
const INNER = 15; // mm
const OUTER = 21; // mm: the outer margin, where the folio tabs hang
const LINES = 31; // whole lines of LEAD in the text block, so full pages end level
const MEASURE = TRIM.width - INNER - OUTER; // the text block's width: 96 mm
const geometry = { // left is the inner margin on a recto; mirror swaps it on the versos
width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, // 150 dpi is for the screen
backgroundColor: col('paper'),
margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - (LINES * LEAD * 25.4) / 72),
left: mm(INNER), right: mm(OUTER), mirror: true },
};
Amb mirror: true, el marge right és l'exterior a la pàgina senar i passa a l'esquerra a la parella, així que OUTER és sempre el del tall davanter, on pengen les pestanyes. El marge inferior es calcula a partir del format, del marge superior i de LINES, de manera que la caixa de text té exactament LINES línies de LEAD (aquí, 31 de 14 pt), tingui el format o el marge superior que tingui. Tota pàgina plena acaba a la línia 31, i per això dues pàgines encarades, com la 30 i la 31, acaben a la mateixa altura.
#4 · L'obertura mostra el títol sencer
const PLATE = 26; // mm: a square plate at the head of the text block
const opener = {
enabled: true,
minHeight: mm(58), // the text of every essay starts on the same line
slot: { elements: [
{ kind: 'box', id: 'plate', style: { backgroundColor: col('sepia') },
placement: { anchor: { to: 'container', edge: 'top-left' },
size: { width: mm(PLATE), height: mm(PLATE) } } },
{ kind: 'text', id: 'numeral', content: '{attr.num}', fontFamily: 'Libre Caslon Display',
fontSize: pt(48), lineHeight: 1, color: col('paper'),
placement: { anchor: { to: '#plate', edge: 'align-top' }, // the plate's own box, where
size: { width: mm(PLATE), height: mm(PLATE) } } }, // the text centres by default
{ kind: 'text', id: 'book', content: '{subtitle}', fontFamily: 'Alegreya SC',
fontSize: pt(8.5), letterSpacing: pt(1.4), color: col('sepia'), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { x: mm(PLATE + 4.5), y: mm(PLATE - 2.9) } } }, // its baseline on the plate's foot
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Libre Caslon Display',
fontSize: pt(20), lineHeight: 1.12, color: col('ink'), align: 'left',
overflow: 'wrap', // an opener shows the whole title; the running head cuts it
placement: { anchor: { to: '#plate', edge: 'below' }, offset: { y: mm(6) },
size: { width: mm(MEASURE) } } },
] },
};
L'obertura compon el títol complet amb overflow: 'wrap', partit a cada \\ de la línia del títol. Només una obertura a tota la pàgina respecta aquests salts, i d'aquí ve el span: 'page' del nivell (salts de línia als títols). A la capçalera, {chapterTitle} dona sempre el títol en una sola línia, i els punts suspensius tallen aquesta línia. El número romà surt de l'atribut {num="IV"} del títol, i les versaletes al costat de la placa, del subtitle del frontmatter.
#5 · Cada assaig obre en pàgina senar
headings: { // 400, the face's only weight: the default 700 would ask for one that is missing
fontFamily: 'Libre Caslon Display', fontWeight: 400, color: col('ink'),
levels: [
// Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
// 'odd' puts every opener on a recto, adding a blank verso when one is needed;
// span: 'page' makes the design an opener, where a \\ in the title breaks the line.
{ level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
marginTop: pt(0), marginBottom: pt(0), advancedDesign: opener },
],
},
Qualsevol objecte headings desactiva el salt de pàgina de l'H1; per això aquí es torna a declarar. Amb 'odd', un assaig que acaba en senar va seguit d'una pàgina parella en blanc. Aquí el capítol IV acaba en una senar, de manera que la 32 queda en blanc i el V obre a la 33. El pages: 'body' de les capçaleres les deixa fora de les pàgines en blanc i de les obertures (les pàgines el primer bloc de les quals és aquest títol). El pages: 'opener' del peu imprimeix el foli només a les obertures (elements de text).
#6 · Els folis comencen pel 29
// 28 pages come before this one, so recto and verso, the mirrored margins and the odd/even
// heads follow the book page (gotcha: parity-page1-recto); the folios start at 29.
const continuation = { pageIndexOffset: 28, pageNumbering: { startAt: 29 } };
const doc = await buildWithFonts(
() => buildDocument({ markdown, continuation }, config()), markdown);
showPages(doc, { title: t({ en: 'Running heads by parity', es: 'Cabeceras según la paridad' }) });
parity es calcula a partir del lloc que ocupa la pàgina al llibre, i el foli no hi intervé. pageIndexOffset: 28 suma 28 a l'índex de cada pàgina, així que les senars i les parelles, els marges en mirall i les capçaleres de cada costat segueixen la pàgina del llibre. Després, pageNumbering.startAt numera aquestes pàgines del 29 al 33. Sense aquest desplaçament, la primera pàgina composta és senar, sigui quin sigui el seu foli. Amb el foli 29 coincideix per casualitat, però amb startAt: 30 el títol del llibre cauria a la 31 i el de l'assaig a la 32, cadascun al costat equivocat del plec.
La recepta completa
// ═══ Postext Cookbook · Nº 005 · Running heads by parity ═══════════════════════════════ // https://postext.dev/en/cookbook/running-heads-by-parity // Code: MIT · Text: Montaigne, tr. Cotton (PD, Gutenberg #3600); es: new translation (MIT) // Fonts: Baskervville, Libre Caslon Display, Alegreya SC (SIL OFL 1.1) · Needs postext ≥ 1.4.1 // Pages 29 to 33 of a pocket Montaigne. Versos carry the book's title and rectos the essay's, // the folios sit in tabs in the outer margin, and an opener prints only a folio at the foot. import { buildDocument, renderPageToCanvas, clearMeasurementCache } from 'https://esm.sh/postext'; const LANG = 'es'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'running-heads-by-parity'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: four colours with semantic ids; main-color points at the sepia const palette = { ink: '#1f1b16', // text: a warm near-black, never #000 sepia: '#8a5a2b', // the one accent: folio tabs, dividers, opener plates (5.3:1 on paper) muted: '#6f6558', // running heads, verse glosses, the colophon (5.2:1 on paper) paper: '#f8f4ec', // a warm off-white page; also the type reversed out of the sepia }; 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' } })), // Text-style defaults (headings, bold, italic, lists, boxes) link to 'main-color'. The // built-in header and footer keep #295AA3, and design slots never read the palette // (gotcha: palette-skips-designs), so every element colour here carries its hex. { id: 'main-color', name: 'sepia (defaults)', value: { hex: palette.sepia, model: 'hex' } }, ]; // #endregion const LEAD = 14; // pt: the body leading, the pitch of the baseline grid // #region tab: the folio tab and its divider, the two pieces every head is built from const FOLIO_PT = 7.5; // pt: the folio, and the book's title in capitals beside it const TITLE_PT = 8.8; // pt: the essay's title, whose italic lowercase reads small at 7.5 const LINE = FOLIO_PT * 1.2; // pt: the folio and both titles share this line box and baseline const PAD = { top: 1, bottom: 1.4 }; // pt: 0.4 pt more below for the descending 3, 5, 7, 9 const TAB = 7.5; // mm: the tab's width const TAB_H = LINE + PAD.top + PAD.bottom; // pt: the tab's height, which the divider matches const folio = ({ id, parity, pages, anchor, x = 0, y }) => ({ kind: 'text', id, content: '{pageNumber}', parity, pages, fontFamily: 'Alegreya SC', fontSize: pt(FOLIO_PT), fontWeight: 700, lineHeight: LINE / FOLIO_PT, color: col('paper'), box: { backgroundColor: col('sepia'), borderRadius: mm(2), // a pill; the number is centred padding: { top: pt(PAD.top), bottom: pt(PAD.bottom) } }, // by default in both directions placement: { anchor, offset: { x: mm(x), y: mm(y) }, size: { width: mm(TAB) } }, }); const divider = ({ id, parity, from, edge, x }) => ({ kind: 'rule', id, parity, pages: 'body', direction: 'vertical', thickness: pt(0.5), color: col('sepia'), placement: { anchor: { to: `#${from}`, edge }, offset: { x: mm(x) }, size: { height: pt(TAB_H) } }, }); // #endregion // #region page: a pocket trim; the margins mirror, the text block holds 31 whole lines const TRIM = { width: 132, height: 198 }; // mm const TOP = 22; // mm: the top margin, which holds the heads (gotcha: header-paints-over-text) const INNER = 15; // mm const OUTER = 21; // mm: the outer margin, where the folio tabs hang const LINES = 31; // whole lines of LEAD in the text block, so full pages end level const MEASURE = TRIM.width - INNER - OUTER; // the text block's width: 96 mm const geometry = { // left is the inner margin on a recto; mirror swaps it on the versos width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, // 150 dpi is for the screen backgroundColor: col('paper'), margins: { top: mm(TOP), bottom: mm(TRIM.height - TOP - (LINES * LEAD * 25.4) / 72), left: mm(INNER), right: mm(OUTER), mirror: true }, }; // #endregion // #region answer: running heads by parity: book on the verso, essay on the recto, folios outside const GAP = 3; // mm from the tab to the divider, which lands on the text block's edge const HEAD = 12; // mm from the top edge of the page to the top of the tab const CAP = t({ en: 60.5, es: 66.8 }); // mm: fitted so essay IV's head ends on a word // (gotcha: ellipsis-mid-word); a short running title (see Variations) suits any title const page = (edge) => ({ to: 'page', edge }); // the trim box (gotcha: negative-offsets) const header = { elements: [ // Verso (even): tab | divider | THE BOOK'S TITLE from the frontmatter (gotcha: quote-frontmatter) folio({ id: 'folio-even', parity: 'even', pages: 'body', anchor: page('top-left'), x: OUTER - GAP - TAB, y: HEAD }), divider({ id: 'rule-even', parity: 'even', from: 'folio-even', edge: 'right-of', x: GAP }), { kind: 'text', id: 'book', content: '{title}', parity: 'even', pages: 'body', fontFamily: 'Alegreya SC', fontSize: pt(FOLIO_PT), lineHeight: LINE / FOLIO_PT, color: col('muted'), textTransform: 'uppercase', letterSpacing: pt(1.3), placement: { anchor: { to: '#rule-even', edge: 'right-of' }, // top: the tab's top offset: { x: mm(2.5), y: pt(PAD.top) } } }, // PAD.top down: on the folio's baseline // Recto (odd): the essay's title, cut short with an ellipsis | divider | tab. folio({ id: 'folio-odd', parity: 'odd', pages: 'body', anchor: page('top-right'), x: -(OUTER - GAP - TAB), y: HEAD }), // from the right edge, a negative x runs inwards divider({ id: 'rule-odd', parity: 'odd', from: 'folio-odd', edge: 'left-of', x: -GAP }), { kind: 'text', id: 'essay', content: '{chapterTitle}', parity: 'odd', pages: 'body', fontFamily: 'Baskervville', italic: true, fontSize: pt(TITLE_PT), lineHeight: LINE / TITLE_PT, color: col('muted'), overflow: 'ellipsis-end', // stated, though default (gotcha: overflow-ellipsis-default) placement: { anchor: { to: '#rule-odd', edge: 'left-of' }, offset: { x: mm(-2.5), y: pt(PAD.top) }, size: { maxWidth: mm(CAP) } } }, ] }; // Openers: no head, only a drop folio HEAD above the page's foot, centred under the text block. const footer = { elements: [folio({ id: 'drop-folio', parity: 'all', pages: 'opener', anchor: { to: 'container', edge: 'bottom' }, y: -HEAD })] }; // #endregion // #region opener: the essay's numeral on a sepia plate; the title below it, set in full const PLATE = 26; // mm: a square plate at the head of the text block const opener = { enabled: true, minHeight: mm(58), // the text of every essay starts on the same line slot: { elements: [ { kind: 'box', id: 'plate', style: { backgroundColor: col('sepia') }, placement: { anchor: { to: 'container', edge: 'top-left' }, size: { width: mm(PLATE), height: mm(PLATE) } } }, { kind: 'text', id: 'numeral', content: '{attr.num}', fontFamily: 'Libre Caslon Display', fontSize: pt(48), lineHeight: 1, color: col('paper'), placement: { anchor: { to: '#plate', edge: 'align-top' }, // the plate's own box, where size: { width: mm(PLATE), height: mm(PLATE) } } }, // the text centres by default { kind: 'text', id: 'book', content: '{subtitle}', fontFamily: 'Alegreya SC', fontSize: pt(8.5), letterSpacing: pt(1.4), color: col('sepia'), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: mm(PLATE + 4.5), y: mm(PLATE - 2.9) } } }, // its baseline on the plate's foot { kind: 'text', id: 'title', content: '{titleText}', fontFamily: 'Libre Caslon Display', fontSize: pt(20), lineHeight: 1.12, color: col('ink'), align: 'left', overflow: 'wrap', // an opener shows the whole title; the running head cuts it placement: { anchor: { to: '#plate', edge: 'below' }, offset: { y: mm(6) }, size: { width: mm(MEASURE) } } }, ] }, }; // #endregion const config = () => ({ // a factory: the engine caches resolved configs per object locale: t({ en: 'en-us', es: 'es' }), // hyphenation by exact code (gotcha: hyphenation-locales) colorPalette, page: geometry, layout: { layoutType: 'single' }, bodyText: { // bold, italic and references default to main-color (the sepia): set to the ink fontFamily: 'Baskervville', fontSize: pt(10.2), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(4), indentAfterHeading: false, // Hyphenation, optimal breaking and widow, orphan and runt control are on by default; a band // of 0.7–1.65 (default 0.6–2) evens the grey from line to line. At the default runt length, // 20 space widths, English essay IV ends on 'minds.' alone; at 16 it ends on 'our minds.'. minWordSpacing: 0.7, maxWordSpacing: 1.65, runtMinCharacters: 16, }, // #region levels: every essay opens on a recto; the heading draws the opener headings: { // 400, the face's only weight: the default 700 would ask for one that is missing fontFamily: 'Libre Caslon Display', fontWeight: 400, color: col('ink'), levels: [ // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break). // 'odd' puts every opener on a recto, adding a blank verso when one is needed; // span: 'page' makes the design an opener, where a \\ in the title breaks the line. { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' }, marginTop: pt(0), marginBottom: pt(0), advancedDesign: opener }, ], }, // #endregion // Montaigne's quotations: a box with no fill keeps verse and gloss together. Its two 10.5 pt // margins and a two-line gloss (2 × 10.5 pt) make three whole lines, so the grid adds no space. calloutStyles: [{ id: 'quote', backgroundEnabled: false, padding: { top: pt(0), right: mm(8), bottom: pt(0), left: mm(8) }, marginTop: pt(LEAD * 0.75), marginBottom: pt(LEAD * 0.75) }], paragraphStyles: [ { id: 'verse', fontSize: pt(9.6), textAlign: 'center', firstLineIndent: pt(0) }, { id: 'gloss', fontSize: pt(7.6), lineHeight: pt(LEAD * 0.75), color: col('muted'), textAlign: 'center', firstLineIndent: pt(0) }, // Markdown has no horizontal rule (--- prints as text; gap: markdown-extras), so the rule // over the colophon is eight em dashes of Alegreya SC that overlap into a 0.5 pt line. { id: 'end', fontFamily: 'Alegreya SC', fontSize: pt(8), color: col('sepia'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) }, // No italics in the colophon: a style's italic runs take bodyText.italicColor, the ink, // and would print darker than the muted words around them (gotcha: style-italic-colour). { id: 'colophon', fontSize: pt(7), lineHeight: pt(9.5), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0) }, ], header, footer, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Mostra en Markdown · 70 línies · content.es.md
title: "Ensayos de Montaigne" subtitle: "Libro primero" author: "Michel de Montaigne" --- # Cómo el alma descarga sus pasiones \\ sobre objetos falsos \\ cuando le faltan los verdaderos {num="IV"} Un gentilhombre de mi tierra, muy aquejado de gota, a quien los médicos apremiaban para que dejara del todo las carnes saladas, solía responder con gracia que, en lo más recio del mal, necesitaba tener con quién tomarla, y que, maldiciendo ya la morcilla, ya la lengua de buey y el jamón, sentía con ello algún alivio. Y hablando en serio: así como al brazo alzado para golpear le duele que el golpe no encuentre nada y se vaya al aire, así la vista, para que el paisaje resulte grato, no ha de perderse en el vacío, sino tener un término que la sostenga a razonable distancia. :::callout{type="quote"} :::paragraphs{style="verse"} *Ventus ut amittit vires, nisi robore densae* *Occurrant sylvae, spatio diffusus inani.* ::: :::paragraphs{style="gloss"} Como el viento, disperso en el espacio vacío, pierde su fuerza si no le salen al paso los troncos de un bosque espeso. — Lucano, III, 362 ::: ::: Parece que el alma, sacudida y conmovida, se pierde en sí misma si no se le da asidero, y hay que darle siempre un objeto en que apoyarse y obrar. Plutarco dice, a propósito de quienes se encariñan con monas y perrillos, que la parte amorosa que hay en nosotros, a falta de un objeto legítimo, antes que quedarse ociosa se forja uno falso y frívolo. Y vemos que el alma, en sus pasiones, prefiere engañarse a sí misma, forjándose un objeto falso y fantástico contra su propia creencia, antes que no obrar contra nada. Así arrastra la rabia a las bestias a atacar la piedra o el hierro que las ha herido, y a vengarse a dentelladas en sí mismas del mal que sienten: :::callout{type="quote"} :::paragraphs{style="verse"} *Pannonis haud aliter, post ictum saevior ursa,* *Cui jaculum parva Lybis amentavit habena,* *Se rotat in vulnus, telumque irata receptum* *Impetit, et secum fugientem circuit hastam.* ::: :::paragraphs{style="gloss"} Así la osa de Panonia, herida por el dardo libio, se revuelve contra la lanza y gira tras el asta que huye. — Lucano, VI, 220 ::: ::: ¿Qué causas no inventamos para las desgracias que nos suceden? ¿Con qué no la tomamos, con razón o sin ella, por tener contra qué batirnos? No son esas trenzas rubias que desgarras, ni la blancura de ese pecho que, despechada, golpeas con tanta crueldad, las que mataron de un desdichado balazo a ese hermano tan querido: tómala con otra cosa. Tito Livio dice del ejército romano en España que, tras la pérdida de los dos hermanos, sus grandes capitanes, *flere omnes repente, et offensare capita*, «todos a una lloraron y se golpearon la cabeza». Es costumbre común. Y tuvo gracia el filósofo Bión cuando dijo de aquel rey que de duelo se arrancaba el pelo: «¿Pensará este que la calvicie alivia la pena?». ¿Quién no ha visto a un jugador mascar y tragarse los naipes, o atiborrarse de dados, por tener en qué vengarse de la pérdida de su dinero? Jerjes azotó el mar y escribió un cartel de desafío al monte Atos; Ciro entretuvo a todo un ejército varios días en vengarse del río Gindes por el miedo que había pasado al cruzarlo; y Calígula arruinó una casa bellísima por el placer que su madre había tenido en ella. Decía la gente en mi juventud que un rey de nuestros vecinos, habiendo recibido de Dios un bastonazo, juró vengarse y ordenó que durante diez años nadie le rezara ni hablara de él, ni, hasta donde alcanzaba su autoridad, creyera en él. Con ello se quería pintar no tanto la necedad como la vanagloria natural de la nación de la que se contaba. Son vicios que van siempre juntos, pero tales acciones tienen, a decir verdad, algo más de soberbia que de estupidez. Augusto César, zarandeado en el mar por la tempestad, se puso a desafiar al dios Neptuno y, en la pompa de los juegos circenses, hizo quitar su imagen del lugar que ocupaba entre los demás dioses, para vengarse de él. En lo cual es aún menos excusable que los anteriores, y menos de lo que lo fue después, cuando, habiendo perdido una batalla bajo Quintilio Varo en Germania, iba de cólera y desesperación dándose con la cabeza contra la pared y gritando: «¡Varo, devuélveme mis soldados!». Porque estos sobrepasan toda locura, pues a ella se une la impiedad, ya que la toman con Dios mismo, o con la fortuna, como si esta tuviera oídos al alcance de nuestra artillería; como los tracios, que, cuando truena o relampaguea, se ponen a disparar contra el cielo con una venganza de titanes, para meter a Dios en razón a flechazos. Ahora bien, como dice aquel antiguo poeta en Plutarco: :::callout{type="quote"} :::paragraphs{style="verse"} *Point ne se faut couroucer aux affaires,* *Il ne leur chault de toutes nos choleres.* ::: :::paragraphs{style="gloss"} No hay que enojarse con las cosas del mundo, que a ellas nada les importan todas nuestras cóleras. — Plutarco ::: ::: Pero nunca diremos bastantes injurias contra el desarreglo de nuestro espíritu. # Si el jefe de una plaza sitiada \\ debe salir a parlamentar {num="V"} Quinto Marcio, legado de los romanos en la guerra contra Perseo, rey de Macedonia, queriendo ganar el tiempo que aún le faltaba para poner a punto su ejército, sembró propuestas de acuerdo con las que el rey, adormecido, concedió una tregua de algunos días, dando así a su enemigo ocasión y tiempo para armarse, de donde le vino al rey su ruina final. Sin embargo, los ancianos del Senado, que guardaban memoria de las costumbres de sus padres, condenaron aquella práctica como contraria a su antiguo estilo, que era, decían, combatir con valor y no con astucia, ni con sorpresas y encuentros nocturnos, ni con fugas fingidas y acometidas inesperadas, y no emprender una guerra sino después de haberla declarado, y a menudo tras haber fijado la hora y el lugar de la batalla. :::paragraphs{style="end"} ———————— ::: :::paragraphs{style="colophon"} Libro primero, capítulos IV y V, de los ensayos de Montaigne, en traducción nueva del texto francés de 1595 (Project Gutenberg, libro electrónico n.º 48529). Compuesto en Baskervville, Libre Caslon Display y Alegreya SC (SIL Open Font License). :::`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses, loaded before the first build (gotcha: fonts-first). const FONTS = { // text, display and label faces (Libre Caslon Display has no italic) Baskervville: ['400', '400i'], 'Libre Caslon Display': ['400'], 'Alegreya SC': ['400', '700'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); // #region build: pages 29 to 33 of the book // 28 pages come before this one, so recto and verso, the mirrored margins and the odd/even // heads follow the book page (gotcha: parity-page1-recto); the folios start at 29. const continuation = { pageIndexOffset: 28, pageNumbering: { startAt: 29 } }; const doc = await buildWithFonts( () => buildDocument({ markdown, continuation }, config()), markdown); showPages(doc, { title: t({ en: 'Running heads by parity', es: 'Cabeceras según la paridad' }) }); // #endregionKit · core, fonts, viewer: igual a totes les receptes · 235 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 ───────────────────────────────────────────────────────────────────────
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
#Posa un titolet curt als títols llargs
Escriu el titolet a la línia del títol, {num="IV" short="Los objetos falsos"}, i imprimeix l'atribut en lloc del títol sencer; un capítol sense short no hi imprimeix res.
- { kind: 'text', id: 'essay', content: '{chapterTitle}', parity: 'odd', pages: 'body',
+ { kind: 'text', id: 'essay', content: '{attr.short}', parity: 'odd', pages: 'body',#Deixa que els assajos obrin a qualsevol pàgina
Amb 'any', cada assaig comença a la pàgina següent, parella o senar, i mai no s'hi afegeix una pàgina en blanc. La placa penja de la cantonada superior esquerra de la caixa de text i el foli del peu va centrat a sota, així que tots dos queden igual en parella que en senar.
- { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'odd' },
+ { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'any' },#Pinta de sèpia la pàgina en blanc
Un element box amb pages: 'blank' només s'imprimeix a les pàgines que afegeix el salt a senar: en aquesta edició omple de sèpia la pàgina 32, i l'edició anglesa, que no té pàgina en blanc, no canvia.
const footer = { elements: [folio({ id: 'drop-folio', parity: 'all', pages: 'opener',
- anchor: { to: 'container', edge: 'bottom' }, y: -HEAD })] };
+ anchor: { to: 'container', edge: 'bottom' }, y: -HEAD }),
+ { kind: 'box', id: 'blank-leaf', pages: 'blank', style: { backgroundColor: col('sepia') },
+ placement: { anchor: page('top-left'),
+ size: { width: mm(TRIM.width), height: mm(TRIM.height) } } }] };Errors freqüents
Error freqüent
Els punts suspensius tallen a mig mot i conserven l'espai anterior
A postext 1.4.1, overflow: 'ellipsis-end' talla després de l'últim caràcter que hi cap, no en un límit de paraula, i conserva l'espai si cau just darrere d'un, de manera que una capçalera pot acabar en «fals…» o «false …». Ajusta maxWidth al final d'una paraula si coneixes el títol, o dona als títols llargs un títol curt de capçalera en un atribut. Textos, filets i caixes als dissenys de pàgina →
Error freqüent
El desbordament del text de disseny és 'ellipsis-end' per defecte
Un element de text de disseny que no cap en la seva amplada acaba en punts suspensius per defecte. Posa overflow: 'wrap' als títols que hagin de passar a més línies. Textos, filets i caixes als dissenys de pàgina →
Error freqüent
Els desplaçaments negatius relatius al contenidor no pinten res
El text de disseny d'amplada automàtica es limita al seu contenidor, de manera que un desplaçament negatiu respecte al contenidor el treu fora i no es pinta res. Ancora aquests elements a la pàgina o a la sang amb desplaçaments explícits en mm, o dona'ls una amplada fixa. Ancoratge d'elements de disseny →
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
Els elements de capçalera i peu es pinten damunt del text
Els elements de capçalera i peu es pinten sobre la pàgina i l'àrea de text no els deixa lloc. Mantén-los dins dels marges, que són els que els reserven l'espai. Capçaleres i folis →
Error freqüent
La pàgina 1 és senar: planifica amb números físics
La pàgina 1 queda a la dreta i la 2 és la primera pàgina parella, així que planifica els plecs amb números de pàgina físics: una obertura en pàgina parella queda davant de la senar que la segueix. Salts de pàgina i de columna →
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
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 →
Error freqüent
Un estil de paràgraf no té color de cursiva
A postext 1.4.1 un estil de paràgraf fixa color i boldColor, però no italicColor: les seves cursives prenen bodyText.italicColor. Un estil atenuat (la lletra petita, la línia «Font:» d'una taula) imprimeix els seus títols en cursiva més foscos que el text que els envolta. Deixa aquests estils en el color del text o evita-hi les cursives. Estils de paràgraf →
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 →
CAPestà ajustat al títol de l'assaig IV a cada edició, l'únic que mostra aquí una capçalera. La de l'assaig V quedaria tallada a mitja paraula («Si el jefe de una plaza sitiada debe salir a parlam…»), així que un llibre de títols llargs necessita el titolet curt de les variants.- A l'edició anglesa, pujar
minWordSpacingde 0,7 a 0,75 deixa la primera línia de l'assaig V («Quintus Marcius, the Roman legate…») sense cap tall dins de la forquilla, i els seus espais entre paraules s'obren fins a 2,4 vegades la seva amplada normal. A l'espanyola no passa. Després de qualsevol canvi, revisa les línies més obertes que llista la captura.
Crèdits
- Recepta
- Ignacio Ferro
- Text
- Essays, Book I, chapters IV and V, in Charles Cotton’s translation edited by William Carew Hazlitt (1877) · Michel de Montaigne; Charles Cotton; William Carew Hazlitt · domini públic
- Essais, livre I, chapitres IV et V: the 1595 French text in the Général Michaud edition (Firmin-Didot, 1907) · Michel de Montaigne · domini públic
- Spanish translation, with its verse glosses · Postext Cookbook · original
- Fonts
- Baskervville (SIL OFL 1.1) · Libre Caslon Display (SIL OFL 1.1) · Alegreya SC (SIL OFL 1.1)
- Codi
- MIT, com Postext
Editar aquest text ↗ (s'obre en una pestanya nova)Carpeta de la recepta a GitHub ↗ (s'obre en una pestanya nova)

