En poques paraules
Un llibret de tres assaigs sobre caminar. L'autor i l'any de cada assaig s'escriuen una vegada i després apareixen on el llibre els necessita, com a l'índex.
Què compondràs
Nou pàgines d'Afoot, una antologia de butxaca amb tres assaigs sobre caminar, de Hazlitt (1822), Thoreau (1862) i Stevenson (1876), en l'anglès original. Una pintura d'un bruguerar al capvespre, un JPEG generat amb models de difusió, ocupa la coberta. L'índex dona a cada assaig un numeral en vermell òxid, el títol en Gloock, una línia de punts espaiats fins a la seva pàgina i, a sota, el nom de l'autor en cursiva morada. Cada assaig s'obre amb el numeral i el títol, una signatura en majúscules espaiades i la revista i l'any en què es va publicar per primera vegada. Les pàgines parelles porten l'autor a la capçalera; les senars, el títol de l'assaig. El títol de cada assaig porta com a atributs l'autor, l'any i la revista, i l'obertura, les capçaleres i l'índex els prenen d'aquí, de manera que per afegir un quart assaig no cal tocar la configuració.
Aquesta recepta respon a
- Com afegeixo un índex que s'actualitzi sol (línies de punts, números de pàgina, autors, files de part)?
- Com afegeixo a una obertura una línia d'autor, una entradeta o un primer paràgraf amb caplletra?
- 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?
La resposta curta
// Each essay's heading carries its credits, and every {attr.…} below reads them:
// # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"}
// (a value cannot hold { or }, and one with " goes in single quotes; gotcha: attr-values)
// 1 · The opener: inside a heading's design, {attr.author} is that heading's attribute.
const byline = [
text('byline', '{attr.author}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
color: col('heather') }, below('title', 5)),
text('dateline', '{attr.source}, {attr.year}', { fontFamily: 'Spectral', italic: true,
fontSize: pt(9.5), color: col('muted'), align: 'left' }, below('byline', 1.2)),
];
// 2 · The verso running head (head() is in the running-heads region): in the page header,
// {attr.author} is the attribute of the essay the page belongs to.
const versoAuthor = head('verso-author', '{attr.author}', 'even',
at('page', 'top-left', MARGIN.outer + HEAD.gap, HEAD.y));
// 3 · The contents: toc.subtitle prints the attribute it names as a line under each title.
// 'author' and italic are the defaults; attr is written out so it can become 'source'.
const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
color: col('heather') };
// Hooked up below: byline → the opener, versoAuthor → header, authorLine → toc.subtitle.
// A heading without author="…" gets an empty byline and running head and no line in the
// contents, and the build gives no warning: check every heading.
Ingredients
- Funcions
- Índex de contingutsAtributs de títolCapçaleres i folisObertures dissenyadesTítols numeratsEstils de títolCapítols sense númeroCobertes, portades i colofonsImatges als dissenys de pàginaAncoratge d'elements de dissenyCapçaleres segons el tipus de pàginaMarges simètricsEstils de paràgrafRequadres fixos i insígniesRequadresEspai vertical explícitMetadades del documentPaleta de color semànticaFigures i taules com a recursosPàgines en un canvas
- També fa servir
- Color del paperCapçaleres per secció
- Tipografia
- Spectral, Gloock, Hanken Grotesk (SIL OFL 1.1)
- Recursos
cover-1200.jpg- The cover: a heath and hills at dusk, a painting (Generated With Diffusion Models, original)
Elaboració
#1 · Numerar els assaigs i baixar-ne el text
const opener = { enabled: true,
// The hairline ends 7 mm above the foot of this reserve, so the text starts on the same grid
// line under every opener whose title fits on one line (SINK = 14 holds a two-line title).
minHeight: pt(SINK * LEAD),
slot: { elements: [
// {number} prints numberingTemplate '{1:I}': I, II, III. {numberRoman} would print
// nothing here: it is filled on part pages only (gotcha: heading-number-placeholders).
text('number', '{number}', { ...gloock, fontSize: pt(34), lineHeight: 1, color: col('rust') },
at('container', 'top-left', 0, 8)),
text('title', '{titleText}', { ...gloock, fontSize: pt(26), lineHeight: 1.08,
color: col('ink') }, below('number', 3, { width: 'fill' })),
...byline,
{ kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'),
placement: below('dateline', 6) }, // a horizontal rule runs to the column's edge
] } };
const essays = { level: 1, numberingTemplate: '{1:I}', advancedDesign: opener,
marginBottom: pt(0), // the heading's default margin would add to minHeight
// Restated (gotcha: headings-drop-h1-break); the cover and contents styles inherit it too.
breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page
El numberingTemplate: '{1:I}' del nivell numera els assaigs I, II i III, i {number} imprimeix el numeral damunt del títol (span i disseny avançat). minHeight reserva 12 línies de 14 pt i el filet fi acaba 7 mm per sobre d'aquest límit, així que, amb un títol d'una línia, el text comença a 79 mm de la vora superior en totes les obertures. Una segona línia de títol, a 26 pt, hi afegeix 9,9 mm i baixa el text una línia de la retícula; amb SINK = 14, el text comença a la mateixa alçada sota títols d'una i de dues línies. El marginBottom: pt(0) del nivell impedeix que el marge per defecte del títol se sumi a aquesta reserva.
#2 · Un índex que s'omple sol
const ENTRY = 15; // pt: the essay titles in the contents
const MIDDLE = 0.3125; // em: how far Chrome's textBaseline 'middle' sits above Gloock's baseline
const contents = { // passed to the config as `toc`
// 1.4.1 centres an entry's number 0.3 × the entry size above its baseline (gotcha:
// toc-number-baseline): at 0.3 × 15 ÷ 0.3125 = 14.4 pt a Gloock numeral stands on it.
levels: [{ level: 1, fontFamily: 'Gloock', fontSize: pt(ENTRY), color: col('ink'),
numberFontSize: pt((0.3 * ENTRY) / MIDDLE), numberFontWeight: 400, // Gloock has one weight
numberColor: col('rust'), numberWidth: mm(8), numberGap: mm(3), marginBottom: pt(LEAD) }],
pageNumber: { fontFamily: 'Hanken Grotesk', fontSize: pt(8.5), fontWeight: 600,
color: col('muted'), width: mm(6) }, // the leader dots take this face and colour too
leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up
subtitle: authorLine,
};
:::toc al Markdown imprimeix una entrada per cada títol inclòs, amb el número, el text del títol, la línia de punts, l'etiqueta de la pàgina i, gràcies a subtitle, l'autor. buildDocument torna a compondre el llibre fins que les etiquetes de pàgina deixen de canviar, de manera que, si un assaig canvia de títol o d'extensió, l'índex s'actualitza (índex de continguts). Postext 1.4.1 col·loca el centre del número de cada entrada 0,3 vegades el cos de l'entrada per sobre de la línia de base, i Chrome situa el centre de Gloock 0,3125 em per sobre de la seva, així que un numeral de 0,3 × 15 ÷ 0,3125 = 14,4 pt s'assenta a la línia de base del títol de 15 pt; un de 12 pt queda 0,3 mm més amunt. Un llibre amb parts hi afegeix, a més, una fila per cada :::part, amb el disseny de toc.parts.design, com a Parts en color.
#3 · L'autor a la pàgina parella, l'assaig a la senar
// Each head: the label face, the pages of its parity, never an opener (pages: 'body'), where
// the byline names the author. A function declaration, so the answer above can call it.
function head(id, content, parity, placement, look = {}) {
return { ...text(id, content, { ...label, fontSize: pt(7.5), letterSpacing: pt(1.3),
color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { color: col('ink') };
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD.y), folio),
versoAuthor,
head('recto-title', '{chapterTitle}', 'odd',
at('page', 'top-right', -(MARGIN.outer + HEAD.gap), HEAD.y), { align: 'right' }),
head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD.y),
{ ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', // an opener's folio
at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };
Cada capçalera s'ancora a la pàgina, a 11 mm de la vora superior, i parity tria el costat: el foli s'alinea amb la vora exterior del text, i el nom o el títol queda 7 mm més endins. El pages: 'body' de head() deixa les capçaleres fora de les obertures, on la signatura ja anomena l'autor, i l'element del peu amb pages: 'opener' hi imprimeix el foli, centrat sota el text (elements de text). {chapterTitle} imprimeix el títol de l'assaig sense el numeral.
#4 · La coberta i l'índex no compten
// Unnumbered, so the first essay is I; unlisted; and with no running heads or folio. They
// inherit the level's page break, 'any' (gotcha: style-inherits-break).
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
{ kind: 'image', id: 'art', resourceId: 'cover',
placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } },
text('title', '{titleText}', { ...gloock, fontSize: pt(80), lineHeight: 1, color: col('ink') },
at('page', 'top-left', MARGIN.inner, 22)), // page 1 is a recto: the inner margin is left
text('subtitle', '{subtitle}', { fontFamily: 'Spectral', italic: true, fontSize: pt(14),
color: col('ink'), align: 'left' }, below('title', 1)),
text('authors', '{attr.authors}', { ...label, fontSize: pt(8.5), letterSpacing: pt(2),
color: col('heather') }, below('subtitle', 5)),
text('imprint', '{attr.imprint}', { ...label, fontSize: pt(7.5), letterSpacing: pt(1.8),
color: col('paper') }, at('page', 'bottom-left', MARGIN.inner, -12)),
] } };
// span: 'page' in a one-column book: a design kept in the column is clipped at the column's top
// and bottom edges, which would leave bands of paper above and below the dusk.
const coverStyle = { id: 'cover', ...bare, span: 'page', advancedDesign: cover };
const contentsOpener = { enabled: true, slot: { elements: [ // {title}, {subtitle}: frontmatter
text('kicker', '{title} · {subtitle}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6),
color: col('heather') }, at('container', 'top-left', 0, 8)),
text('title', '{titleText}', { ...gloock, fontSize: pt(26), color: col('ink') },
below('kicker', 2.5)),
] } };
Amb numbered: false, la coberta i l'índex no fan avançar el comptador, així que Hazlitt és el I, i toc: false els deixa fora de la llista (estils de títol). Els dos estils hereten el breakBefore del nivell, amb paritat 'any', i per això l'índex segueix la coberta a la pàgina 2 sense cap :::pagebreak. L'estil de la coberta ocupa tota la pàgina encara que el llibre tingui una sola columna, perquè un disseny que es queda a la columna es retalla per les vores superior i inferior i deixaria 20 mm de paper per sobre del paisatge.
#5 · Cada color escrit amb el seu enllaç a la paleta
const palette = {
ink: '#241f26', // text: a plum-tinted near-black
paper: '#f7f2e8', // the page
heather: '#6b4468', // the bylines and the authors in the contents
rust: '#a4502a', // the essay numbers
rule: '#d5cabd', // hairlines
muted: '#6d6570', // running heads, datelines, page numbers in the contents
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
// The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue.
{ id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } },
];
A la 1.4.1 colorPalette arriba als estils de text però no als elements de disseny, així que col() escriu el valor hexadecimal de cada color al costat del seu paletteId, i aquest valor és el que pinten les signatures, les capçaleres i la coberta. Amb main-color en el color de la tinta, els valors per defecte que la configuració no redefineix surten en aquest color i no en el blau del motor, #295AA3.
La recepta completa
// ═══ Postext Cookbook · Nº 030 · Anthology with bylines ═══════════════════════════════ // https://postext.dev/en/cookbook/anthology-with-bylines // Code: MIT · Text: Hazlitt, Thoreau, Stevenson (public domain) · Cover: diffusion models // Fonts: Spectral, Gloock, Hanken Grotesk (SIL OFL 1.1) · Needs postext ≥ 1.4.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage } from 'https://esm.sh/postext'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'anthology-with-bylines'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: heather for the credits, rust for the numbers, plum ink on warm paper const palette = { ink: '#241f26', // text: a plum-tinted near-black paper: '#f7f2e8', // the page heather: '#6b4468', // the bylines and the authors in the contents rust: '#a4502a', // the essay numbers rule: '#d5cabd', // hairlines muted: '#6d6570', // running heads, datelines, page numbers in the contents }; // A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs). const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = [ ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })), // The engine's defaults link to 'main-color': point it at the ink, so nothing prints blue. { id: 'main-color', name: 'ink (defaults)', value: { hex: palette.ink, model: 'hex' } }, ]; // #endregion const TRIM = { width: 135, height: 180 }; // a pocket book, 3 : 4 const MARGIN = { top: 20, bottom: 22, inner: 19, outer: 15 }; // mirrored const LEAD = 14; // body leading in pt: the baseline grid const SINK = 12; // grid lines an essay opener reserves above its first line of text const HEAD = { y: 11, gap: 7 }; // running heads: mm from the top edge, folio to words const label = { fontFamily: 'Hanken Grotesk', fontWeight: 600, textTransform: 'uppercase', align: 'left' }; // design text is centred by default // One weight, no italic; 'wrap' breaks a long title (gotcha: overflow-ellipsis-default). const gloock = { fontFamily: 'Gloock', overflow: 'wrap', align: 'left' }; const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); const below = (id, y, size) => ({ ...at(`#${id}`, 'below', 0, y), ...(size && { size }) }); const text = (id, content, look, placement) => ({ kind: 'text', id, content, ...look, placement }); // #region answer: one set of heading attributes, read in three places // Each essay's heading carries its credits, and every {attr.…} below reads them: // # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"} // (a value cannot hold { or }, and one with " goes in single quotes; gotcha: attr-values) // 1 · The opener: inside a heading's design, {attr.author} is that heading's attribute. const byline = [ text('byline', '{attr.author}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6), color: col('heather') }, below('title', 5)), text('dateline', '{attr.source}, {attr.year}', { fontFamily: 'Spectral', italic: true, fontSize: pt(9.5), color: col('muted'), align: 'left' }, below('byline', 1.2)), ]; // 2 · The verso running head (head() is in the running-heads region): in the page header, // {attr.author} is the attribute of the essay the page belongs to. const versoAuthor = head('verso-author', '{attr.author}', 'even', at('page', 'top-left', MARGIN.outer + HEAD.gap, HEAD.y)); // 3 · The contents: toc.subtitle prints the attribute it names as a line under each title. // 'author' and italic are the defaults; attr is written out so it can become 'source'. const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10), color: col('heather') }; // Hooked up below: byline → the opener, versoAuthor → header, authorLine → toc.subtitle. // A heading without author="…" gets an empty byline and running head and no line in the // contents, and the build gives no warning: check every heading. // #endregion // #region opener: the essay's number and title, then the byline, over a sunk first line const opener = { enabled: true, // The hairline ends 7 mm above the foot of this reserve, so the text starts on the same grid // line under every opener whose title fits on one line (SINK = 14 holds a two-line title). minHeight: pt(SINK * LEAD), slot: { elements: [ // {number} prints numberingTemplate '{1:I}': I, II, III. {numberRoman} would print // nothing here: it is filled on part pages only (gotcha: heading-number-placeholders). text('number', '{number}', { ...gloock, fontSize: pt(34), lineHeight: 1, color: col('rust') }, at('container', 'top-left', 0, 8)), text('title', '{titleText}', { ...gloock, fontSize: pt(26), lineHeight: 1.08, color: col('ink') }, below('number', 3, { width: 'fill' })), ...byline, { kind: 'rule', id: 'rule', thickness: pt(0.5), color: col('rule'), placement: below('dateline', 6) }, // a horizontal rule runs to the column's edge ] } }; const essays = { level: 1, numberingTemplate: '{1:I}', advancedDesign: opener, marginBottom: pt(0), // the heading's default margin would add to minHeight // Restated (gotcha: headings-drop-h1-break); the cover and contents styles inherit it too. breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page // #endregion // #region contents: the essays' numbers, titles, leaders and page labels, from the headings const ENTRY = 15; // pt: the essay titles in the contents const MIDDLE = 0.3125; // em: how far Chrome's textBaseline 'middle' sits above Gloock's baseline const contents = { // passed to the config as `toc` // 1.4.1 centres an entry's number 0.3 × the entry size above its baseline (gotcha: // toc-number-baseline): at 0.3 × 15 ÷ 0.3125 = 14.4 pt a Gloock numeral stands on it. levels: [{ level: 1, fontFamily: 'Gloock', fontSize: pt(ENTRY), color: col('ink'), numberFontSize: pt((0.3 * ENTRY) / MIDDLE), numberFontWeight: 400, // Gloock has one weight numberColor: col('rust'), numberWidth: mm(8), numberGap: mm(3), marginBottom: pt(LEAD) }], pageNumber: { fontFamily: 'Hanken Grotesk', fontSize: pt(8.5), fontWeight: 600, color: col('muted'), width: mm(6) }, // the leader dots take this face and colour too leader: { char: '. ', gap: mm(2) }, // spaced dots, right-aligned so they line up subtitle: authorLine, }; // #endregion // #region running-heads: author on the verso, essay title on the recto, folios outside // Each head: the label face, the pages of its parity, never an opener (pages: 'body'), where // the byline names the author. A function declaration, so the answer above can call it. function head(id, content, parity, placement, look = {}) { return { ...text(id, content, { ...label, fontSize: pt(7.5), letterSpacing: pt(1.3), color: col('muted'), ...look }, placement), parity, pages: 'body' }; } const folio = { color: col('ink') }; const header = { elements: [ head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, HEAD.y), folio), versoAuthor, head('recto-title', '{chapterTitle}', 'odd', at('page', 'top-right', -(MARGIN.outer + HEAD.gap), HEAD.y), { align: 'right' }), head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, HEAD.y), { ...folio, align: 'right' }), ] }; const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', // an opener's folio at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] }; // #endregion // #region front: the cover and the contents page, two headings kept out of the count // Unnumbered, so the first essay is I; unlisted; and with no running heads or folio. They // inherit the level's page break, 'any' (gotcha: style-inherits-break). const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } }; const cover = { enabled: true, slot: { elements: [ { kind: 'image', id: 'art', resourceId: 'cover', placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: 'fill' } } }, text('title', '{titleText}', { ...gloock, fontSize: pt(80), lineHeight: 1, color: col('ink') }, at('page', 'top-left', MARGIN.inner, 22)), // page 1 is a recto: the inner margin is left text('subtitle', '{subtitle}', { fontFamily: 'Spectral', italic: true, fontSize: pt(14), color: col('ink'), align: 'left' }, below('title', 1)), text('authors', '{attr.authors}', { ...label, fontSize: pt(8.5), letterSpacing: pt(2), color: col('heather') }, below('subtitle', 5)), text('imprint', '{attr.imprint}', { ...label, fontSize: pt(7.5), letterSpacing: pt(1.8), color: col('paper') }, at('page', 'bottom-left', MARGIN.inner, -12)), ] } }; // span: 'page' in a one-column book: a design kept in the column is clipped at the column's top // and bottom edges, which would leave bands of paper above and below the dusk. const coverStyle = { id: 'cover', ...bare, span: 'page', advancedDesign: cover }; const contentsOpener = { enabled: true, slot: { elements: [ // {title}, {subtitle}: frontmatter text('kicker', '{title} · {subtitle}', { ...label, fontSize: pt(8), letterSpacing: pt(1.6), color: col('heather') }, at('container', 'top-left', 0, 8)), text('title', '{titleText}', { ...gloock, fontSize: pt(26), color: col('ink') }, below('kicker', 2.5)), ] } }; // #endregion const config = () => ({ // a factory: the engine caches resolved configs per object colorPalette, page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true } }, // left = inner layout: { layoutType: 'single' }, bodyText: { fontFamily: 'Spectral', fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), firstLineIndent: mm(4), indentAfterHeading: false, minWordSpacing: 0.7, maxWordSpacing: 1.6, // tighter than the 0.6–2 defaults maxRuntTracking: 0 }, // tracking 1.4.1 never paints (gotcha: runt-tracking-unpainted) // The hidden heading line is still measured, in this face; otherwise the build needs Open Sans. headings: { fontFamily: 'Gloock', fontWeight: 400, levels: [essays] }, headingStyles: [coverStyle, { id: 'contents', ...bare, advancedDesign: contentsOpener }], toc: contents, // Quoted verse, one paragraph per line (a paragraph keeps no line breaks, and 1.4.1 prints a // Markdown blockquote in a fixed #666666 grey); 'runon' resumes the sentence after it. paragraphStyles: [{ id: 'verse', firstLineIndent: mm(8), textAlign: 'left' }, { id: 'runon', firstLineIndent: pt(0) }, // In the note, under a :::space: a style's margins do not count inside a box (gotcha: // box-paragraph-margins). It takes the note body's indent, 0. { id: 'colophon', fontSize: pt(7.5), lineHeight: pt(10.5), color: col('muted') }], calloutStyles: [{ id: 'note', placement: 'fixed', backgroundEnabled: false, // the page foot stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') }, padding: { top: mm(3), right: pt(0), bottom: pt(0), left: pt(0) }, titleStyle: { ...label, fontSize: pt(7.5), letterSpacing: pt(1.5), color: col('heather') }, body: { fontSize: pt(9), lineHeight: pt(12.5), firstLineIndent: pt(0) } }], header, footer, }); // #region art: the cover, a painting: a JPEG in assets/ cut to the trim's 3 : 4 // The design's image element names the resource by id; loadImage() below registers the file // under its fileId. A bitmap is declared at its pixels. const resources = [{ id: 'cover', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0, bitmap: { fileId: 'cover-1200.jpg', format: 'jpeg', width: 1200, height: 1600 }, altText: 'Heath and hills at dusk under a pale apricot sky, a low sun on the horizon, three ' + 'trees on a ridge and a footpath winding up from the foot of the page through the ' + 'heather.' }]; // #endregion // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Mostra en Markdown · 67 línies · content.en.md
title: "Afoot" subtitle: "Three essays on walking" --- # Afoot {style="cover" authors="Hazlitt · Thoreau · Stevenson" imprint="The Fieldpath Library"} # Contents {style="contents"} :::toc :::callout{type="note" title="A note on the texts"} Each of these essays was first printed in a magazine, two in London and the third in Boston. Hazlitt’s appeared in *The New Monthly Magazine* in January 1822. Thoreau worked his up from a lecture he first gave in 1851, and *The Atlantic Monthly* printed it in June 1862, a month after his death. Stevenson’s came out in *The Cornhill Magazine* in 1876, and in its second paragraph he quotes Hazlitt by name. Each essay starts at its first line and stops well short of its last, in its author’s own spelling; […] marks a cut within a paragraph. :::space{lines=1} :::paragraphs{style="colophon"} Set in Spectral, Gloock and Hanken Grotesk (SIL Open Font License). The essays are in the public domain, from Project Gutenberg eBooks #3020, #1022 and #386; this note is CC BY 4.0; the cover is a painting. ::: ::: # On Going a Journey {author="William Hazlitt" year="1822" source="The New Monthly Magazine"} One of the pleasantest things in the world is going a journey; but I like to go by myself. I can enjoy society in a room; but out of doors, nature is company enough for me. I am then never less alone than when alone. :::paragraphs{style="verse"} *The fields his study, nature was his book.* ::: I cannot see the wit of walking and talking at the same time. When I am in the country I wish to vegetate like the country. I am not for criticising hedge-rows and black cattle. I go out of town in order to forget the town and all that is in it. There are those who for this purpose go to watering-places, and carry the metropolis with them. I like more elbow-room and fewer encumbrances. I like solitude, when I give myself up to it, for the sake of solitude; nor do I ask for :::paragraphs{style="verse"} *A friend in my retreat,* *Whom I may whisper solitude is sweet.* ::: The soul of a journey is liberty, perfect liberty, to think, feel, do, just as one pleases. We go a journey chiefly to be free of all impediments and of all inconveniences; to leave ourselves behind much more to get rid of others. It is because I want a little breathing-space to muse on indifferent matters, where Contemplation :::paragraphs{style="verse"} *May plume her feathers and let grow her wings,* *That in the various bustle of resort* *Were all too ruffled, and sometimes impair’d,* ::: :::paragraphs{style="runon"} that I absent myself from the town for a while, without feeling at a loss the moment I am left by myself. Instead of a friend in a postchaise or in a Tilbury, to exchange good things with, and vary the same stale topics over again, for once let me have a truce with impertinence. Give me the clear blue sky over my head, and the green turf beneath my feet, a winding road before me, and a three hours’ march to dinner—and then to thinking! It is hard if I cannot start some game on these lone heaths. I laugh, I run, I leap, I sing for joy. […] ::: # Walking {author="Henry David Thoreau" year="1862" source="The Atlantic Monthly"} I wish to speak a word for Nature, for absolute Freedom and Wildness, as contrasted with a freedom and culture merely civil,—to regard man as an inhabitant, or a part and parcel of Nature, rather than a member of society. I wish to make an extreme statement, if so I may make an emphatic one, for there are enough champions of civilization: the minister and the school committee and every one of you will take care of that. I have met with but one or two persons in the course of my life who understood the art of Walking, that is, of taking walks—who had a genius, so to speak, for sauntering, which word is beautifully derived “from idle people who roved about the country, in the Middle Ages, and asked charity, under pretense of going à la Sainte Terre,” to the Holy Land, till the children exclaimed, “There goes a Sainte-Terrer,” a Saunterer, a Holy-Lander. They who never go to the Holy Land in their walks, as they pretend, are indeed mere idlers and vagabonds; but they who do go there are saunterers in the good sense, such as I mean. Some, however, would derive the word from sans terre without land or a home, which, therefore, in the good sense, will mean, having no particular home, but equally at home everywhere. For this is the secret of successful sauntering. He who sits still in a house all the time may be the greatest vagrant of all; but the saunterer, in the good sense, is no more vagrant than the meandering river, which is all the while sedulously seeking the shortest course to the sea. But I prefer the first, which, indeed, is the most probable derivation. For every walk is a sort of crusade, preached by some Peter the Hermit in us, to go forth and reconquer this Holy Land from the hands of the Infidels. It is true, we are but faint-hearted crusaders, even the walkers, nowadays, who undertake no persevering, never-ending enterprises. Our expeditions are but tours, and come round again at evening to the old hearth-side from which we set out. Half the walk is but retracing our steps. We should go forth on the shortest walk, perchance, in the spirit of undying adventure, never to return,—prepared to send back our embalmed hearts only as relics to our desolate kingdoms. If you are ready to leave father and mother, and brother and sister, and wife and child and friends, and never see them again,—if you have paid your debts, and made your will, and settled all your affairs, and are a free man; then you are ready for a walk. To come down to my own experience, my companion and I, for I sometimes have a companion, take pleasure in fancying ourselves knights of a new, or rather an old, order—not Equestrians or Chevaliers, not Ritters or Riders, but Walkers, a still more ancient and honorable class, I trust. The chivalric and heroic spirit which once belonged to the Rider seems now to reside in, or perchance to have subsided into, the Walker—not the Knight, but Walker Errant. He is a sort of fourth estate, outside of Church and State and People. We have felt that we almost alone hereabouts practiced this noble art; though, to tell the truth, at least if their own assertions are to be received, most of my townsmen would fain walk sometimes, as I do, but they cannot. No wealth can buy the requisite leisure, freedom, and independence which are the capital in this profession. It comes only by the grace of God. It requires a direct dispensation from Heaven to become a walker. You must be born into the family of the Walkers. Ambulator nascitur, non fit. Some of my townsmen, it is true, can remember and have described to me some walks which they took ten years ago, in which they were so blessed as to lose themselves for half an hour in the woods; but I know very well that they have confined themselves to the highway ever since, whatever pretensions they may make to belong to this select class. No doubt they were elevated for a moment as by the reminiscence of a previous state of existence, when even they were foresters and outlaws. # Walking Tours {author="Robert Louis Stevenson" year="1876" source="The Cornhill Magazine"} It must not be imagined that a walking tour, as some would have us fancy, is merely a better or worse way of seeing the country. There are many ways of seeing landscape quite as good; and none more vivid, in spite of canting dilettantes, than from a railway train. But landscape on a walking tour is quite accessory. He who is indeed of the brotherhood does not voyage in quest of the picturesque, but of certain jolly humours—of the hope and spirit with which the march begins at morning, and the peace and spiritual repletion of the evening’s rest. He cannot tell whether he puts his knapsack on, or takes it off, with more delight. […] Now, to be properly enjoyed, a walking tour should be gone upon alone. If you go in a company, or even in pairs, it is no longer a walking tour in anything but name; it is something else and more in the nature of a picnic. A walking tour should be gone upon alone, because freedom is of the essence; because you should be able to stop and go on, and follow this way or that, as the freak takes you; and because you must have your own pace, and neither trot alongside a champion walker, nor mince in time with a girl. And then you must be open to all impressions and let your thoughts take colour from what you see. You should be as a pipe for any wind to play upon. “I cannot see the wit,” says Hazlitt, “of walking and talking at the same time. When I am in the country I wish to vegetate like the country,”—which is the gist of all that can be said upon the matter. There should be no cackle of voices at your elbow, to jar on the meditative silence of the morning. And so long as a man is reasoning he cannot surrender himself to that fine intoxication that comes of much motion in the open air, that begins in a sort of dazzle and sluggishness of the brain, and ends in a peace that passes comprehension.`; // content.<lang>.md, inlined by the Cookbook // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces (gotcha: fonts-first) Spectral: ['400', '400i'], Gloock: ['400'], 'Hanken Grotesk': ['600'] }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadImage('cover-1200.jpg', asset('cover-1200.jpg')); await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'Anthology with bylines', es: 'Antología con firmas de autor' }) });Kit · core, fonts, viewer, images: igual a totes les receptes · 270 línies
// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ───────── function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · images v1 ── recipes with pictures · postext.dev/cookbook ────────── /** Registers a photo or PNG for the canvas and keeps its bytes for the PDF. * fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */ async function loadImage(fileId, url) { const res = await fetch(url); if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`); const bytes = new Uint8Array(await res.arrayBuffer()); registerResourceImage(fileId, await createImageBitmap(new Blob([bytes]))); (loadImage.bytes ??= new Map()).set(fileId, bytes); } /** Registers SVG markup (drawn in code, or fetched) as a vector image. */ async function loadSvg(fileId, svg) { const img = new Image(); img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; await img.decode(); registerResourceImage(fileId, img); (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg)); } /** renderToPdf({ resourceBytes: imageBytes }) */ function imageBytes(fileId) { return loadImage.bytes?.get(fileId); } /** renderToHtml({ resourceImageUrl: imageUrl }) */ function imageUrl(fileId) { const bytes = imageBytes(fileId); if (!bytes) return undefined; imageUrl.urls ??= new Map(); if (!imageUrl.urls.has(fileId)) { const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg'; imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type }))); } return imageUrl.urls.get(fileId); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
L'script.js compost funciona tal com és: enganxa'l com a script de mòdul en qualsevol pàgina o obre la recepta a CodePen. Carpeta de la recepta a GitHub ↗ (s'obre en una pestanya nova)
Variants
#Indicar on es va publicar cada assaig
toc.subtitle imprimeix qualsevol atribut del títol, així que el mateix Markdown pot posar sota cada títol la revista on es va publicar per primera vegada.
-const authorLine = { enabled: true, attr: 'author', fontFamily: 'Spectral', fontSize: pt(10),
+const authorLine = { enabled: true, attr: 'source', fontFamily: 'Spectral', fontSize: pt(10),#Posar el títol del llibre a la pàgina parella
Molts llibres posen el seu propi títol a les pàgines parelles i el del capítol a les senars; {title} el pren del frontmatter del Markdown i imprimeix AFOOT.
-const versoAuthor = head('verso-author', '{attr.author}', 'even',
+const versoAuthor = head('verso-author', '{title}', 'even',#Obrir cada assaig en pàgina senar
Amb la paritat 'odd', Stevenson passa a la pàgina 9, després d'una pàgina 8 en blanc. Els dos estils dels preliminars necessiten llavors el seu propi 'any'; si no, l'índex hereta 'odd' i s'obre a la pàgina 3, després d'una pàgina parella en blanc.
- breakBefore: { enabled: true, parity: 'any' } }; // 'any': each piece opens on the next page
+ breakBefore: { enabled: true, parity: 'odd' } };
-const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
+const bare = { numbered: false, toc: false, breakBefore: { enabled: true, parity: 'any' },
+ header: { elements: [] }, footer: { elements: [] } };Errors freqüents
Error freqüent
Valors d'atribut: sense { ni }; cometes simples si porten "
Un valor d'atribut acaba a la clau de tancament, així que no pot contenir { ni }. Un valor que porti cometes dobles va entre cometes simples; el signe de dòlar no dona problemes. Atributs de títol →
Error freqüent
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
Un estil de títol hereta el salt de pàgina del seu nivell
Una entrada de headingStyles pren del seu nivell de títol tot allò que no fixa, també breakBefore. Un índex o un colofó amb estil sobre un H1 després d'un :::pagebreak hereta la paritat 'odd' i cau darrere d'una pàgina en blanc. Dona a aquest estil breakBefore: { enabled: false }. Estils de títol →
Error freqüent
Els números de l'índex queden una mica per sobre de la línia de base de l'entrada
A postext 1.4.1, :::toc pinta el número de cada entrada centrat a la línia i no sobre la línia de base del text, de manera que els números de capítol queden una mica alts al costat dels seus títols, uns 0,7 mm al costat d'un títol de 16 pt, sigui quina sigui la font o la mida. Encara no hi ha cap opció de toc que els mogui, així que revisa l'índex a mida real abans d'imprimir. Índex de continguts →
Error freqüent
{number}/{chapterNumber} donen el número de l'H1; {numberRoman} només en parts
{number} i {chapterNumber} imprimeixen el número ja formatat del títol, però {numberRoman}, {numberDecimal} i les altres variants numèriques només s'emplenen a les pàgines de part. Dona format al número de capítol al seu numberingTemplate ({1:I}) o passa'l com a atribut. Títols numerats →
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 marges d'un estil de paràgraf no compten dins d'un requadre
A postext 1.4.1, un contenidor :::paragraphs dins d'un :::callout no aplica el marginTop ni el marginBottom del seu estil, de manera que una línia en lletra petita sota el text d'una nota hi queda enganxada. Dona a l'estil un lineHeight més gran, que deixa aire sobre la primera línia, o treu aquesta línia del requadre. Estils de paràgraf →
Error freqüent
La correcció de les línies curtes pot estrènyer un espaiat entre lletres que no es pinta mai
A postext 1.4.1, quan un paràgraf acaba en una línia curta, el motor el compon amb una línia menys: primer estreny l'espai entre paraules i després aplica fins a maxRuntTracking mil·lèsimes d'em d'espaiat negatiu entre lletres. Els renderitzadors de canvas i PDF només pinten l'espaiat entre lletres més gran que zero, així que el paràgraf s'imprimeix sense aquest espaiat: les seves línies justificades perden aquesta diferència en els espais entre paraules, que surten aixafats, i l'última línia pot passar-se de la mesura i quedar tallada a la vora de la columna. Posa bodyText.maxRuntTracking: 0, que conserva la correcció amb l'espai entre paraules, i reescriu els paràgrafs que tornin a acabar en una línia curta. Vídues, òrfenes i línies curtes →
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
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 →
- Hazlitt cita versos enmig de les frases. Un paràgraf no conserva els salts de línia, així que cada vers és un paràgraf propi dins d'un contenidor
:::paragraphs{style="verse"}, i la frase que continua després va en un estilrunon, sense sagnat. Una cita en bloc de Markdown sortiria en cursiva d'un gris fix,#666666, amb el sagnat de primera línia del cos i sense sagnat lateral, i cap opció de configuració ho canvia a la 1.4.1. - Un atribut que falta en un títol no imprimeix res ni avisa. Sense
author="…", la signatura i la capçalera parella d'aquell assaig surten buides i la seva línia sota el títol desapareix de l'índex. - Els 14,4 pt del pas 2 només valen per a Gloock. Amb una altra font, esbrina a quina alçada sobre la línia de base posa Chrome el seu centre:
measureText('I').alphabeticBaselineambtextBaseline = 'middle'la retorna en píxels, amb el signe canviat. Després dona anumberFontSizeel valor de 0,3 vegades el cos de l'entrada dividit per aquesta distància en em (la de Hanken Grotesk és 0,2675 em).
Crèdits
- Recepta
- Ignacio Ferro
- Text
- “On Going a Journey” (1822), from its first line to “I sing for joy”, in Table Talk: Essays on Men and Manners · William Hazlitt · domini públic
- “Walking” (1862), its first five paragraphs · Henry David Thoreau · domini públic
- “Walking Tours” (1876), the opening of its first paragraph and the whole of its second, in Virginibus Puerisque · Robert Louis Stevenson · domini públic
- The note on the texts and the colophon · Ignacio Ferro · CC BY 4.0
- Imatges
- The cover: a heath and hills at dusk, a painting · Generated With Diffusion Models · original
- Fonts
- Spectral (SIL OFL 1.1) · Gloock (SIL OFL 1.1) · Hanken Grotesk (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)


