En poques paraules
Quatre pàgines d'un antic diccionari de mariners. Unes pestanyes amb lletres baixen per la vora exterior, i la de la lletra actual va en fosc i més avall a mesura que avança l'alfabet.
Què compondràs
Quatre pàgines d'una nova edició del Sailor’s Word-Book de l'almirall Smyth (1867), de la A a la C, en una pàgina de 150 × 200 mm. Les entrades van en dues columnes justificades d'Alegreya de 8 pt, separades per un filet, amb el lema en negreta blau marí i sagnat francès d'1 em. Cada lletra s'obre dins de la seva columna amb una inicial blava de quatre línies sobre un filet color llautó, i les entrades continuen sense salt de pàgina. Al tall davanter, cada pàgina imprimeix l'índex sencer, 22 pestanyes en un to pàl·lid. La de la lletra en curs és blau marí, 3,2 mm més ampla que les altres, i baixa un pas amb cada lletra. L'edició castellana conserva els lemes anglesos de Smyth i tradueix les entrades.
Aquesta recepta respon a
- Com afegeixo un índex de pestanyes: una pestanya al tall davanter que baixa per la pàgina d'una lletra o d'un capítol al següent?
- Com componc una bibliografia o un glossari (sagnat francès, cos més petit)?
- Com dono color als termes clau (en negreta o cursiva) en el text o dins dels requadres?
- 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?
La resposta curta
// 22 tabs share the 44 lines of the text block, two lines each; letters with few words
// share a tab, as in most thumb-indexed dictionaries.
const TABS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'QR', 'S', 'T', 'UV', 'W', 'XYZ'];
const STEP = 2 * LEAD * PT; // mm: 7.34
const EDGE = { odd: 'top-right', even: 'top-left' }; // the fore-edge: right on a recto
// A cell of the index, `width` mm inside the trim and 3 mm past it, so that only its
// inner corners show their rounding; its label is centred on the part inside the trim.
const cell = (id, label, i, parity, width, fill, ink, size) => {
const y = mm(TOP + i * STEP + 0.3); // a 0.6 mm gap between neighbours
const at = (x, w) => ({ anchor: { to: 'page', edge: EDGE[parity] }, offset: { x: mm(x), y },
size: { width: mm(w), height: mm(STEP - 0.6) } });
return [
{ kind: 'box', id, parity, style: { backgroundColor: col(fill), borderRadius: mm(1.2) },
placement: at(parity === 'odd' ? 3 : -3, width + 3) },
{ kind: 'text', id: `${id}-label`, parity, content: label, fontFamily: LABEL, fontWeight: 700,
fontSize: pt(size), lineHeight: 1, color: col(ink), align: 'center',
verticalAlign: 'middle', placement: at(0, width) },
];
};
const sides = (make) => ['odd', 'even'].flatMap(make);
// Every page prints the whole index, pale; the section's own letter stands out of it.
const ladder = sides((p) => TABS.flatMap((label, i) =>
cell(`index-${i}-${p}`, label, i, p, 5.8, 'tint', 'slate', 7.5)));
const letterStyles = () => TABS.flatMap((label, i) => [...label].map((letter) => ({
id: letter,
// '# B {style="B"}' opens the section of B: its pages take this header, and a page
// where A ends and B begins takes B's (gotcha: section-last-wins).
header: { elements: [...runningHeads, ...ladder,
...sides((p) => cell(`tab-${p}`, label, i, p, 9, 'navy', 'paper', 8.5))] },
})));
Ingredients
- Funcions
- Bandes a sang i pestanyesCapçaleres per seccióNegreta, cursiva i els seus colorsEstils de títolObertures dissenyadesAncoratge d'elements de dissenyImatges als dissenys de pàginaAtributs de títolEstils de paràgrafBibliografies i glossarisCapçaleres segons el tipus de pàginaCapçaleres i folisFilet de columnaEquilibri de columnesVídues, òrfenes i línies curtesPartició de mots i llengua del documentPaleta de color semànticaColor del paperMarges simètricsFigures i taules com a recursos
- Tipografia
- Alegreya, Alegreya SC, Alegreya Sans SC (SIL OFL 1.1)
- Recursos
- Cap: totes les imatges es dibuixen en codi
Elaboració
#1 · Un estil de títol per lletra, cadascun amb la seva pestanya
El codi és a la resposta curta, més amunt. TABS enumera 22 pestanyes; les lletres amb poques entrades en comparteixen una (QR, UV, XYZ). Cada pestanya fa dues línies de text (7,34 mm), així que entre totes cobreixen les 44 línies de la caixa. El bucle dona a cada lletra un estil de títol el header del qual afegeix a les capçaleres i a l'índex pàl·lid la pestanya blava d'aquella lletra, un pas més avall que l'anterior (estils d'encapçalament). Cada cel·la s'escriu dues vegades, una per parity, i s'ancora a la vora exterior de la pàgina, no al contenidor propi de la capçalera. Aquest contenidor és el marge superior, de l'amplada de la caixa i des de la vora superior de la pàgina fins al text, i les pestanyes han d'arribar al tall davanter i baixar per les 44 línies. Una pàgina pren la capçalera de l'última secció que hi comença, així que la pàgina 2 comença a la A i porta la pestanya de la B.
#2 · Les capçaleres, fora de la primera pàgina
const HEAD = 11; // mm from the trim's top to the heads' top
const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity,
pages: 'body', fontFamily: LABEL, fontSize: pt(8), fontWeight: 500, letterSpacing: pt(1),
color: col('muted'),
placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD) } }, ...extra });
const folio = { fontWeight: 700, color: col('ink'), letterSpacing: pt(0) };
const runningHeads = [
head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
head('verso-title', '{title}', 'even', 'top-left', OUTER + 8),
head('recto-letter', t({ en: 'Letter {chapterTitle}', es: 'Letra {chapterTitle}' }), 'odd',
'top-right', -(OUTER + 8)),
head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
];
// The first page drops its folio to the foot, under the text block.
const footer = { elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
...folio, pages: 'opener',
placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(6) } } })] };
Les capçaleres són elements de text ancorats a la pàgina i filtrats per parity i per pages: 'body' (elements de text). El títol de la pàgina 1 abasta les dues columnes, i això la converteix en pàgina d'obertura. Les capçaleres no hi surten, i el foli el posa el peu, un element amb pages: 'opener' que s'imprimeix 6 mm per sota de la caixa. A les senars, {chapterTitle} imprimeix la lletra de l'últim títol de nivell 1 que hi ha a la pàgina o abans, Letra C. Des de postext 1.5.0, {firstMark.<clave>} i {lastMark.<clave>} imprimeixen en el seu lloc el primer i l'últim lema de la pàgina, les paraules guia que porten gairebé tots els diccionaris; aquesta recepta es va compondre amb la 1.4.1 i conserva la lletra.
#3 · Color per als lemes i res més
// Bold and italic default to the engine's blue: the body sets them to ink, and only the
// entry style prints its bold, the headwords, in navy.
const BODY = 8; // pt: a reference size, about 50 characters to the 60 mm column
const bodyText = { fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'),
boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(0),
minWordSpacing: 0.7, maxWordSpacing: 1.7,
maxRuntTracking: 0 }; // gotcha: runt-tracking-unpainted
const paragraphStyles = [
{ id: 'entry', hangingIndent: em(1), boldColor: col('navy') }, // :::paragraphs{style="entry"}
{ id: 'colophon', fontSize: pt(7), color: col('muted'), textAlign: 'center',
marginTop: pt(LEAD) },
];
Cada entrada és un paràgraf d'un bloc :::paragraphs{style="entry"}, amb sagnat francès d'1 em (estils de paràgraf). El boldColor del cos deixa el Nota. de la pàgina 1 en el color del text, i el de l'estil d'entrada posa els lemes en blau marí. Un estil de paràgraf no té italicColor, així que les cursives de les entrades conserven el color del cos.
#4 · Una inicial de quatre línies dins de la columna
// Its cap line meets the first body line's and it stands on the fourth baseline. Cap
// heights measured on the glyph H: 0.652 em in Alegreya SC 900, 0.646 em in Alegreya.
const DROP = 4; // body lines the letter spans
const LETTER = ((DROP - 1) * LEAD + 0.646 * BODY) / 0.652; // pt: 55.8
// A design line sets its baseline 0.8 of its height below its top; a body line does too.
const letterHead = { enabled: true, slot: { elements: [
{ kind: 'text', id: 'letter', content: '{titleText}', fontFamily: DISPLAY, fontWeight: 900,
fontSize: pt(LETTER), lineHeight: 1, color: col('navy'), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { y: pt((DROP - 0.2) * LEAD - 0.8 * LETTER) } } },
{ kind: 'rule', id: 'hairline', direction: 'horizontal', thickness: pt(0.6), color: col('brass'),
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { y: pt((DROP + 0.55) * LEAD) }, size: { width: 'fill' } } },
] } };
LETTER, 55,8 pt, és el cos amb què una majúscula d'Alegreya SC 900 (0,652 em d'alçada) s'assenta a la quarta línia de base i arriba a l'altura de les majúscules de la primera línia de text. La lletra i el filet color llautó acaben dins de la cinquena línia, i el bloc del títol baixa fins a la línia següent de la retícula de base, així que cada inicial ocupa cinc línies i les entrades de sota continuen a la retícula. A la pàgina 3, la C que encapçala la segona columna mesura el mateix que les quatre primeres línies de la primera. El nivell 1 declara breakBefore: { enabled: false }, i els estils de les lletres l'hereten. A la 1.4.1 qualsevol objecte headings ja treu el salt de pàgina del nivell 1; amb el valor escrit, les lletres continuaran a la columna encara que una versió posterior recuperi aquest salt.
#5 · El títol a l'amplada de les dues columnes
const centred = (id, content, family, size, lineHeight, y, width, extra) => ({ kind: 'text', id,
content, fontFamily: family, fontSize: pt(size), lineHeight, color: col('navy'), align: 'center',
overflow: 'wrap', ...extra, placement: { anchor: { to: 'container', edge: 'top' },
offset: { y: mm(y) }, size: { width: width ? mm(width) : 'fill' } } });
const rule = (id, y, thickness) => ({ kind: 'rule', id, direction: 'horizontal',
thickness: pt(thickness), color: col('navy'), placement: { anchor: { to: 'container',
edge: 'top-left' }, offset: { y: mm(y) }, size: { width: 'fill' } } });
const titleBand = { enabled: true, slot: { elements: [
// The column rule starts at the top of the text block, under the band too: a field of
// paper down to the thin rule covers it, so it hangs from the double rule.
{ kind: 'box', id: 'field', style: { backgroundColor: col('paper') }, placement: {
anchor: { to: 'container', edge: 'top-left' }, size: { width: 'fill', height: mm(45) } } },
{ kind: 'image', id: 'anchor', resourceId: 'anchor', placement: {
anchor: { to: 'container', edge: 'top' }, size: { width: 'auto', height: mm(18) } } },
centred('name', '{titleText}', DISPLAY, 22, 1.1, 20, 0, { fontWeight: 900 }),
centred('subtitle', '{attr.subtitle}', TEXT, 9, 1.25, 29.5, 92, { italic: true,
color: col('ink') }),
centred('byline', '{attr.byline}', LABEL, 7, 1.2, 38.8, 0, { fontWeight: 500,
letterSpacing: pt(1.2), color: col('muted') }),
rule('thick', 43.6, 1.2),
rule('thin', 45, 0.4),
] } };
// '# The Sailor’s Word-Book {style="title" subtitle="…" byline="…"}' opens page 1. The band's
// {titleText} joins the hidden heading's wrapped lines with a space, and a page-span heading
// wraps at the column width: at the default H1 size the band printed 'Word- Book'. At body
// size the hidden title fits one line of the 60 mm column.
const titleStyle = { id: 'title', span: 'page', advancedDesign: titleBand, fontSize: pt(BODY) };
L'estil del títol abasta la pàgina amb una banda que reuneix l'àncora, el nom, un subtítol i una signatura presos d'atributs del títol, i un filet doble (span i disseny avançat). El filet fi acaba 45,1 mm per sota de la vora superior de la caixa, així que la banda s'ajusta a 13 línies de la retícula, 47,7 mm. Sota un títol a tota l'amplada, el filet de columna comença igualment a la vora superior de la caixa i travessaria el títol. Un rectangle del color del paper el tapa fins al filet fi, i el filet de columna només treu el cap per sota. {titleText} uneix amb un espai les línies del títol ocult, i la 1.4.1 talla un títol a tota l'amplada a la mesura d'una columna. En anglès, amb el cos de l'H1 per defecte, la banda imprimia Word- Book; per això l'estil del títol dona al títol ocult el cos del text, amb el qual el nom cap en una línia de la columna de 60 mm.
La recepta completa
// ═══ Postext Cookbook · Nº 046 · Dictionary with a moving thumb index ═══════════════ // https://postext.dev/en/cookbook/dictionary-thumb-index // Code: MIT · Text: W. H. Smyth, 1867 (PD); Spanish translation CC BY 4.0 · Pictures: code // Fonts: Alegreya, Alegreya SC, Alegreya Sans SC (SIL OFL 1.1) · Needs postext ≥ 1.4.1 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerResourceImage, } from 'https://esm.sh/postext'; const LANG = 'es'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'dictionary-thumb-index'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: ink and navy on a warm paper, brass for the rules and the rope const palette = { ink: '#1b1f24', // text navy: '#1f3a5f', // headwords, letters and tabs brass: '#a4834f', // hairlines and the rope of the vignette rule: '#cfccc3', // the column rule tint: '#e4e9f0', // the pale cells of the index slate: '#56657b', // their letters muted: '#62666b', // running heads paper: '#fbfaf6', // the page, and the letter reversed out of each tab }; 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' } })); // #endregion const TEXT = 'Alegreya', DISPLAY = 'Alegreya SC', LABEL = 'Alegreya Sans SC'; const PT = 25.4 / 72; // mm in a point const LEAD = 10.4; // pt: the body's leading, the grid every column is set on const LINES = 44; // lines in a full column const TOP = 18, INNER = 12, OUTER = 14; // mm; the foot margin makes the block whole lines const BOTTOM = 200 - TOP - LINES * LEAD * PT; // #region answer: one heading style per letter, each with its tab one step down the fore-edge // 22 tabs share the 44 lines of the text block, two lines each; letters with few words // share a tab, as in most thumb-indexed dictionaries. const TABS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'QR', 'S', 'T', 'UV', 'W', 'XYZ']; const STEP = 2 * LEAD * PT; // mm: 7.34 const EDGE = { odd: 'top-right', even: 'top-left' }; // the fore-edge: right on a recto // A cell of the index, `width` mm inside the trim and 3 mm past it, so that only its // inner corners show their rounding; its label is centred on the part inside the trim. const cell = (id, label, i, parity, width, fill, ink, size) => { const y = mm(TOP + i * STEP + 0.3); // a 0.6 mm gap between neighbours const at = (x, w) => ({ anchor: { to: 'page', edge: EDGE[parity] }, offset: { x: mm(x), y }, size: { width: mm(w), height: mm(STEP - 0.6) } }); return [ { kind: 'box', id, parity, style: { backgroundColor: col(fill), borderRadius: mm(1.2) }, placement: at(parity === 'odd' ? 3 : -3, width + 3) }, { kind: 'text', id: `${id}-label`, parity, content: label, fontFamily: LABEL, fontWeight: 700, fontSize: pt(size), lineHeight: 1, color: col(ink), align: 'center', verticalAlign: 'middle', placement: at(0, width) }, ]; }; const sides = (make) => ['odd', 'even'].flatMap(make); // Every page prints the whole index, pale; the section's own letter stands out of it. const ladder = sides((p) => TABS.flatMap((label, i) => cell(`index-${i}-${p}`, label, i, p, 5.8, 'tint', 'slate', 7.5))); const letterStyles = () => TABS.flatMap((label, i) => [...label].map((letter) => ({ id: letter, // '# B {style="B"}' opens the section of B: its pages take this header, and a page // where A ends and B begins takes B's (gotcha: section-last-wins). header: { elements: [...runningHeads, ...ladder, ...sides((p) => cell(`tab-${p}`, label, i, p, 9, 'navy', 'paper', 8.5))] }, }))); // #endregion // #region running-heads: the book on the verso, the letter on the recto, folios outside const HEAD = 11; // mm from the trim's top to the heads' top const head = (id, content, parity, edge, x, extra) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontSize: pt(8), fontWeight: 500, letterSpacing: pt(1), color: col('muted'), placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD) } }, ...extra }); const folio = { fontWeight: 700, color: col('ink'), letterSpacing: pt(0) }; const runningHeads = [ head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio), head('verso-title', '{title}', 'even', 'top-left', OUTER + 8), head('recto-letter', t({ en: 'Letter {chapterTitle}', es: 'Letra {chapterTitle}' }), 'odd', 'top-right', -(OUTER + 8)), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio), ]; // The first page drops its folio to the foot, under the text block. const footer = { elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, { ...folio, pages: 'opener', placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(6) } } })] }; // #endregion // #region entries: one paragraph per entry, the headword bold in navy, the turnovers hanging // Bold and italic default to the engine's blue: the body sets them to ink, and only the // entry style prints its bold, the headwords, in navy. const BODY = 8; // pt: a reference size, about 50 characters to the 60 mm column const bodyText = { fontFamily: TEXT, fontSize: pt(BODY), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), firstLineIndent: mm(0), minWordSpacing: 0.7, maxWordSpacing: 1.7, maxRuntTracking: 0 }; // gotcha: runt-tracking-unpainted const paragraphStyles = [ { id: 'entry', hangingIndent: em(1), boldColor: col('navy') }, // :::paragraphs{style="entry"} { id: 'colophon', fontSize: pt(7), color: col('muted'), textAlign: 'center', marginTop: pt(LEAD) }, ]; // #endregion // #region letter-heads: a four-line initial over a brass hairline, in the column, running on // Its cap line meets the first body line's and it stands on the fourth baseline. Cap // heights measured on the glyph H: 0.652 em in Alegreya SC 900, 0.646 em in Alegreya. const DROP = 4; // body lines the letter spans const LETTER = ((DROP - 1) * LEAD + 0.646 * BODY) / 0.652; // pt: 55.8 // A design line sets its baseline 0.8 of its height below its top; a body line does too. const letterHead = { enabled: true, slot: { elements: [ { kind: 'text', id: 'letter', content: '{titleText}', fontFamily: DISPLAY, fontWeight: 900, fontSize: pt(LETTER), lineHeight: 1, color: col('navy'), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: pt((DROP - 0.2) * LEAD - 0.8 * LETTER) } } }, { kind: 'rule', id: 'hairline', direction: 'horizontal', thickness: pt(0.6), color: col('brass'), placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: pt((DROP + 0.55) * LEAD) }, size: { width: 'fill' } } }, ] } }; // #endregion // #region title: the book's name across both columns, under a fouled anchor const centred = (id, content, family, size, lineHeight, y, width, extra) => ({ kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight, color: col('navy'), align: 'center', overflow: 'wrap', ...extra, placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(y) }, size: { width: width ? mm(width) : 'fill' } } }); const rule = (id, y, thickness) => ({ kind: 'rule', id, direction: 'horizontal', thickness: pt(thickness), color: col('navy'), placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(y) }, size: { width: 'fill' } } }); const titleBand = { enabled: true, slot: { elements: [ // The column rule starts at the top of the text block, under the band too: a field of // paper down to the thin rule covers it, so it hangs from the double rule. { kind: 'box', id: 'field', style: { backgroundColor: col('paper') }, placement: { anchor: { to: 'container', edge: 'top-left' }, size: { width: 'fill', height: mm(45) } } }, { kind: 'image', id: 'anchor', resourceId: 'anchor', placement: { anchor: { to: 'container', edge: 'top' }, size: { width: 'auto', height: mm(18) } } }, centred('name', '{titleText}', DISPLAY, 22, 1.1, 20, 0, { fontWeight: 900 }), centred('subtitle', '{attr.subtitle}', TEXT, 9, 1.25, 29.5, 92, { italic: true, color: col('ink') }), centred('byline', '{attr.byline}', LABEL, 7, 1.2, 38.8, 0, { fontWeight: 500, letterSpacing: pt(1.2), color: col('muted') }), rule('thick', 43.6, 1.2), rule('thin', 45, 0.4), ] } }; // '# The Sailor’s Word-Book {style="title" subtitle="…" byline="…"}' opens page 1. The band's // {titleText} joins the hidden heading's wrapped lines with a space, and a page-span heading // wraps at the column width: at the default H1 size the band printed 'Word- Book'. At body // size the hidden title fits one line of the 60 mm column. const titleStyle = { id: 'title', span: 'page', advancedDesign: titleBand, fontSize: pt(BODY) }; // #endregion const config = () => ({ // a factory, never a shared object (gotcha: config-cache-identity) locale: t({ en: 'en-us', es: 'es' }), // exact codes (gotcha: hyphenation-locales) colorPalette, footer, header: { elements: [] }, // every page takes the header of its letter's style page: { sizePreset: 'custom', width: mm(150), height: mm(200), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(TOP), bottom: mm(BOTTOM), left: mm(INNER), right: mm(OUTER), mirror: true } }, layout: { gutterWidth: mm(4), // two columns is the default layout columnRule: { enabled: true, color: col('rule'), lineWidth: pt(0.4) } }, bodyText, paragraphStyles, // The designs paint every heading; the hidden ones are still measured, and in the face // FONTS loads, or the kit would fetch Open Sans 700 for text nobody sees. headings: { fontFamily: DISPLAY, fontWeight: 900, levels: [ // Letters run on in the column. Any headings object already drops the H1 break // (gotcha: headings-drop-h1-break); stating it keeps them running on if that default // returns, and the letter styles inherit it (gotcha: style-inherits-break). { level: 1, breakBefore: { enabled: false }, marginTop: pt(LEAD), marginBottom: pt(0), advancedDesign: letterHead }, ] }, headingStyles: [ titleStyle, ...letterStyles(), ], }); // #region art: a fouled anchor, the Admiralty's badge, for the title band // Drawn in the page's navy and brass; the rope's lay is a dashed navy stroke over it. const anchorSvg = () => { const { navy, brass } = palette; const rope = 'M66.5 19 C88 26 86 44 60 52 C34 60 32 76 60 82 C88 88 86 104 60 110 ' + 'C44 114 38 122 42 136'; const fluke = (s) => `<path transform="translate(60 0) scale(${s} 1)" fill="${navy}" d="M-44 92 L-50 78 L-36 70 L-34 88 Z"/>`; return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 150" width="120" height="150"> <circle cx="60" cy="13" r="8.5" fill="none" stroke="${navy}" stroke-width="4"/> <path d="M56.5 21 L63.5 21 L64.5 124 L55.5 124 Z" fill="${navy}"/> <rect x="20" y="27" width="80" height="6.5" rx="3.2" fill="${navy}"/> <circle cx="19" cy="30.2" r="5" fill="${navy}"/> <circle cx="101" cy="30.2" r="5" fill="${navy}"/> <path d="M16 84 Q20 132 60 134 Q100 132 104 84" fill="none" stroke="${navy}" stroke-width="7" stroke-linecap="round"/> ${fluke(1)}${fluke(-1)} <path d="M60 126 L66 134 L60 142 L54 134 Z" fill="${navy}"/> <path d="${rope}" fill="none" stroke="${brass}" stroke-width="4.2" stroke-linecap="round"/> <path d="${rope}" fill="none" stroke="${navy}" stroke-opacity="0.5" stroke-width="4.2" stroke-dasharray="1 1.8"/> </svg>`; }; // #endregion // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Mostra en Markdown · 115 línies · content.es.md
title: "Vocabulario del marinero" author: "W. H. Smyth" --- # Vocabulario del marinero {style="title" subtitle="Compendio alfabético de voces náuticas, con algunas más propiamente militares y científicas, pero útiles a la gente de mar" byline="Almirante W. H. Smyth · Londres, 1867"} **Nota.** El almirante William Henry Smyth dejó manuscrito este vocabulario al morir, el 8 de septiembre de 1865. El vicealmirante sir Edward Belcher lo revisó para la imprenta, y Blackie and Son lo publicó en Londres en 1867 con el título de *The Sailor’s Word-Book*. Esta selección traduce entradas de la A, la B y la C, algunas abreviadas. Los lemas siguen en inglés y en el orden de Smyth, casi todos con su equivalencia castellana en cursiva; entre ellos están los que pasaron al inglés corriente: *A1*, *taken aback*, *aloof*, *bitter end*, *wide berth*, *clear the decks*, *cut and run*. Las entradas a las que remite *véase* no están en esta selección. # A {style="A"} :::paragraphs{style="entry"} **A.** La clase más alta de calidad de los buques mercantes en el registro del Lloyd’s, dividida en A1 y A2, tras las cuales se desciende por las vocales, y A1 es lo mejor de la primera clase. Antes, un buque construido en el río (el Támesis) obtenía la primera clase por 12 años, uno de Bristol por 11 y los de los puertos del norte por 10. Algunos buques construidos en puertos menores conservan su clase de 6 a 8 años, y los inferiores solo 4. Pero las mejoras de la construcción naval, y el uso cada vez mayor del hierro, piden ya plazos más largos. **Aback.** *En facha.* Situación de las velas de un buque cuando el viento les da por la cara de proa. Se *ponen en facha* a propósito, para quitarle arrancada cobrando las brazas de barlovento, y *quedan en facha* cuando las coge un salto de viento imprevisto o un descuido del timonel. – *All aback forward*, aviso que se da desde el castillo cuando un salto de viento pone en facha las velas de proa. (*Véase* Work aback.) – *Taken aback*, en lenguaje llano, verse sorprendido o descubierto de pronto. **Abeam.** *Por el través.* En línea perpendicular a la eslora del buque; frente al centro de su costado. **About.** *Virado.* En redondo; situación del buque después de haber virado y mareado las velas en la otra amura. – *Ready about!* y *About-ship!* son las voces con que se ordena a la dotación prepararse para virar por avante, cada cual en su puesto. **Adrift.** *A la deriva.* Flotando al azar; estado de un bote o de un buque que ha roto sus amarras y va de acá para allá sin gobierno, a merced de los vientos y las olas. Suelto; cortado de sus amarras. **Afloat.** *A flote.* Sostenido y llevado por el agua; boyante, libre del fondo; se dice también de quien vive embarcado. **Ahead.** *Avante; por la proa.* Término que se aplica a todo objeto situado más adelante, o justo delante del buque, o en el rumbo que se gobierna, y por tanto lo opuesto de *astern*. – *Ahead of the reckoning*, ir adelantado a la estima. – *Ahead* se usa también para el progreso, como en *cannot get ahead*, «no poder avanzar», y en general por adelante. **A-hull.** *A palo seco.* Buque sin vela alguna y con el timón a sotavento, que deriva ante el viento y la mar con la popa por delante. También, buque abandonado y expuesto a los temporales. **A-lee.** *A sotavento.* Lo contrario de *a-weather*: la posición del timón cuando su caña se lleva a la banda de sotavento para virar por avante o llevar la proa a barlovento. – *Hard a-lee!* o *luff a-lee!* es la voz al timonel para que lleve la caña a sotavento. – *Helm’s a-lee!*, la voz de mando que se da al llevarla, y que hace flamear las velas de proa. **Aloof.** *Orza; a distancia.* La voz antigua por «keep your luff», al navegar ciñendo el viento. (*Véase* Luff.) – *Keep aloof*, mantenerse a distancia. **Anchor.** *Ancla.* Instrumento grande y pesado, en uso desde la Antigüedad para sujetar y retener los buques, y lo hace con una fuerza admirable. Salvo excepciones, consta de una larga caña de hierro que tiene en un extremo un arganeo, al que se afirma el cable, y en el otro se abre en dos brazos con uñas, o mapas, en sus picos. Un cepo de madera o de hierro, fijo en ángulo recto con los brazos, sirve para que las uñas entren perpendiculares al fondo. Según su forma y tamaño, las anclas reciben los nombres de *sheet*, *best bower*, *small bower*, *spare*, *stream*, *kedge* y *grapling* (véanse en su lugar). **Apple-pie order.** *Como una patena.* Término extraño, pero corriente, para un buque en excelente estado y bien cuidado; limpio y ordenado. Se dice, sin fundamento, que viene de *du pol au pied*. **Ashore.** *En tierra.* Varado, en tierra. – *To go ashore*, desembarcar de un bote. Lo opuesto de *aboard*. **Astern.** *Por la popa.* A cualquier distancia detrás de un buque; en la parte de popa; en dirección a la popa, y por tanto lo opuesto de *ahead*. – *To drop astern*, quedarse atrás: cuando un buque queda a popa de la perpendicular a la quilla por el palo mayor, se dice que *drops astern*. **Athwart.** *De través.* La dirección transversal; todo lo que se extiende atravesado en la derrota de un buque. – *Athwart hawse*, buque, bote o madera flotante que la deriva ha atravesado por delante de la roda de un buque; se entiende la posición transversal de lo que deriva. – *Athwart the fore-foot*, justo por delante de la roda; los buques disparan un cañonazo en esa dirección para detener a un desconocido y obligarlo a ponerse en facha. – *Athwart ships*, en la dirección de los baos; de banda a banda: lo opuesto de *fore-and-aft*. **Avast.** *¡Basta!* Voz para parar, aguantar, cesar o suspender cualquier faena: su derivación del italiano *basta* es más verosímil que la de *have fast*. **Awning.** *Toldo.* Cubierta o dosel de lona suspendido de una araña y tendido sobre un buque, un bote o cualquier embarcación para resguardar las cubiertas y la gente del sol y del tiempo. (*Véase* Euphroe.) También, la parte de la toldilla que se prolonga a proa más allá del mamparo de la cámara. **Azure.** *Azur.* El azul intenso del cielo cuando está del todo despejado. ::: # B {style="B"} :::paragraphs{style="entry"} **Ballast.** *Lastre.* Como verbo, significa afirmar; como sustantivo, un juicio sólido. Se dice que un hombre «loses his ballast», pierde el lastre, cuando le falla el juicio o, envanecido, se vuelve pesado de arriba. **Beam-ends.** *Tumbado.* Se dice que un buque está *on her beam-ends* cuando escora tanto que sus baos se acercan a la vertical; de ahí que también se diga, en sentido figurado, de una persona tendida. **Berth.** *Fondeadero; puesto.* El lugar en que un buque está fondeado, solo o en una escuadra; así, está en buen fondeadero, es decir, en buen tenedero, abrigado del viento y la mar y a la debida distancia de la costa y de otros buques. – *Snug berth*, un buen acomodo, un puesto o empleo. Una litera. – *To berth a vessel*, elegirle a un buque su puesto y llevarlo a él. – *To berth a ship’s company*, señalar a cada hombre el sitio donde ha de colgar su coy, con las 14 pulgadas de ancho de costumbre. – *To give a berth*, apartarse; dar a una punta de tierra un *wide berth* es mantenerse bien lejos de ella. **Bitter-end.** *Chicote.* La parte del cable que queda a popa de las bitas, y por tanto dentro de la borda, cuando el buque está fondeado. Dicen «Bend to the bitter-end» cuando quieren entalingar ese chicote al ancla; y cuando una cadena o un cabo se han filado hasta el *bitter-end*, no queda más por largar. El *bitter-end* es el chicote de la talinga: a veces se entalinga ese al ancla, porque no se ha usado nunca y es más de fiar. De un cable de 115 brazas, las primeras 40 suelen estar gastadas cuando el chicote de dentro está casi nuevo. **Booby.** *Piquero.* Conocida ave marina tropical, *Sula fusca*, de la familia de los *Pelecanidae*. Gusta de descansar fuera del agua por la noche, y hasta prefiere una percha inestable en la verga de un buque. El nombre inglés, que vale tanto como «bobo», le viene de lo fácil que se deja atrapar recién posado. La dirección en que vuelan al caer la tarde indica a menudo dónde hay tierra. **Brace of shakes.** *Un santiamén.* Un momento, tomado del flamear de una vela: «Estaré contigo antes de que flamee tres veces». **Bring up with a round turn.** *Parar en seco.* Detener de golpe un cabo que corre dándole una vuelta redonda a un bolardo, una cabeza de bita o una cornamusa. Se dice de lo que se hace con eficacia, aunque con brusquedad, y también de hacer entrar en razón a alguien con una buena reprimenda. **Broach-to, to.** *Atravesarse.* Orzar bruscamente hasta quedar proa al viento. Suele ocurrir cuando un buque lleva mucho trapo con el viento por la aleta y bastante vela a popa. La arboladura corre peligro, porque el cambio de rumbo la opone más al viento y aumenta su presión. En casos extremos las velas quedan en facha, y entonces los palos pueden ceder, o el buque irse a pique de popa. **Broadside.** *Andanada; costado.* Todo el aparato, o la descarga simultánea, de la artillería de una banda de un buque de guerra, en todas sus baterías. Designa también todo el costado del buque sobre el agua que queda entre la amura y la aleta, casi perpendicular al horizonte. También, nombre que se daba a los antiguos pliegos en folio en que se imprimían coplas y bandos (*broad-sheet*). **By the board.** *Por la borda.* Por el costado del buque. Cuando un palo se rompe cerca de la cubierta, se dice que se va *by the board*. ::: # C {style="C"} :::paragraphs{style="entry"} **Cabin.** *Cámara.* Aposento o compartimento separado a bordo, donde viven los oficiales y los pasajeros. En un buque de guerra, la cámara principal, donde vive el comandante o el almirante, está en la parte alta de popa. **Cabin-boy.** *Paje de cámara.* Muchacho encargado de atender y servir a los oficiales y a los pasajeros en la cámara. **Cable’s length.** *Cable* (medida). Medida de unas cien brazas, con la que se suele estimar la distancia entre los buques de una escuadra. El término se entiende mal a menudo. En todas las cartas náuticas un cable vale 607,56 pies, la décima parte de una milla marina. En cordelería, el cable tiene entre 100 y 115 brazas de largo; el *cablet*, 120 brazas, y el cabo *hawser-laid*, 130, según fijó el Almirantazgo en 1830. **Caboose.** *Fogón.* La cocina de los mercantes, sobre cubierta; sustituto menor de la cocina de un buque de guerra. Suele tener aparatos de hierro fundido para guisar. **Careen, to.** *Escorar.* Se dice que un buque escora al inclinarse sobre una banda, o cuando se tumba al navegar ciñendo el viento, apartado de su quilla, en latín *carina*. **Cat o’ nine tails.** *Gato de nueve colas.* Instrumento de castigo usado a bordo en la marina de guerra; suele ser de nueve trozos de piola o cordel, de media yarda de largo, fijos en un trozo de cabo grueso que hace de mango, con tres nudos cada uno, a cortos intervalos, cerca de un extremo; con él se azota en la espalda desnuda a los marineros que faltan. **Caulk, to.** *Calafatear.* (*Véase* Caulking.) Echarse a dormir vestido en cubierta. **Chart.** *Carta náutica.* Mapa hidrográfico, o proyección de alguna parte de la superficie terrestre *in plano*, para uso de los navegantes; se distinguen además las cartas planas, las de Mercator, las globulares y la carta de botellas o de corrientes, que ayuda a estudiar las corrientes de superficie (véanse todas). Una carta selenográfica representa la Luna, sobre todo tal como se ve con ayuda de la fotografía y del aparato del señor De la Rue. **Cheer, to.** *Saludar a la voz.* Saludar a un buque *en passant*: la gente sube a cubierta y lanza tres hurras; significa también alentar o animar. (*Véase también* Hearty y Man ship!) **Chock-a-block, or chock and block.** *A besar.* Igual que *two-blocks* y *block-a-block* (véanse). Cuando el motón de abajo de un aparejo sube hasta tocar el de arriba, de modo que ya no se puede izar más, los motones quedan juntos. **Clear, to.** *Despejar; zafar.* Tiene varias acepciones, sobre todo librarse de, descargar, vaciar o preparar, como en: – *To clear for action*, hacer zafarrancho de combate. – *To clear away*, quitar de en medio lo que estorba para una faena. – *To clear the decks*, despejar las cubiertas: retirar los trastos, poner cada cosa en su sitio y adujar los cabos; también, levantar la mesa después de comer. – *To clear goods*, despachar las mercancías pagando los derechos de aduana. – *To clear the land*, librarse de la costa. – *To clear a lighter, or the hold*, vaciar una gabarra o la bodega. **Coil.** *Adujada.* Porción de cabo recogida en forma de anillo; así se disponen a bordo todos los cabos, para estibarlos con comodidad. Se recogen en redondo, una aduja sobre otra, o en vueltas concéntricas, llamadas *Flemish coil*, que forman una sola capa y quedan planas sobre la cubierta, con el chicote en el centro, como se enroscan la culebra y el gusano. **Convoy.** *Convoy; conserva.* Flota de mercantes con un mismo destino, protegida por una fuerza armada. También, el buque o buques que la conducen y defienden en la travesía, y la escolta de tropas que lleva pertrechos a una fuerza destacada. **Cross-trees.** *Crucetas.* Maderos que, apoyados en las cacholas y los baos, se cruzan en lo alto de los palos machos y de los masteleros para sostener el armazón de la cofa en los primeros y abrir los obenques de juanete en los segundos. **Cuddy.** *Camareta.* Especie de camarote o de cocina, por lo general a proa, aunque a veces cerca de la popa, de las gabarras y barcazas de carga. En los buques de travesía oceánica es una cámara a popa, bajo la toldilla, destinada al capitán y a sus pasajeros. También, el camarote pequeño de un bote. **Cut and run, to.** *Picar el cable.* Cortar el cable para huir. También, largarse; dejar el empleo; desaparecer. ::: :::paragraphs{style="colophon"} Tipos: Alegreya, Alegreya SC y Alegreya Sans SC (SIL OFL) Texto: W. H. Smyth, 1867 (Proyecto Gutenberg, n.º 26000) Traducción, nota y ancla: Postext Cookbook (CC BY 4.0) :::`; // content.<lang>.md, inlined by the Cookbook // No paragraph cites the anchor, so it is never placed as a figure: only the title band's // image element draws it. const resources = [{ id: 'anchor', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0, altText: t({ en: 'An anchor with a rope wound round its shank.', es: 'Un ancla con un cabo enrollado en la caña.' }), svg: { fileId: 'anchor.svg', width: 120, height: 150 } }]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { Alegreya: ['400', '400i', '700'], 'Alegreya SC': ['900'], 'Alegreya Sans SC': ['500', '700'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); await loadSvg('anchor.svg', anchorSvg()); const doc = await buildWithFonts(() => buildDocument({ markdown, resources }, config()), markdown); showPages(doc, { title: t({ en: 'The Sailor’s Word-Book', es: 'Vocabulario del marinero' }) });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
#Imprimeix només la pestanya en curs
Els índexs de pestanyes impresos solen mostrar només la de la lletra en curs; treu ladder de la capçalera i la pestanya blava continuarà baixant de la A a la C.
- header: { elements: [...runningHeads, ...ladder,
+ header: { elements: [...runningHeads,Errors freqüents
Error freqüent
Una pàgina pren la capçalera de l'última secció que hi comença
Quan dos estils de títol comencen a la mateixa pàgina, aquesta pren la capçalera, el peu i la paleta de l'últim. Una pàgina en blanc de separació pertany al capítol anterior, i l'emplenament de paritat, al següent. Capçaleres per secció →
Error freqüent
Un estil de títol hereta el salt de pàgina del seu nivell
Una entrada de headingStyles pren del seu nivell de títol tot allò que no fixa, també breakBefore. Un índex o un colofó amb estil sobre un H1 després d'un :::pagebreak hereta la paritat 'odd' i cau darrere d'una pàgina en blanc. Dona a aquest estil breakBefore: { enabled: false }. Estils de títol →
Error freqüent
Qualsevol objecte headings desactiva el salt de pàgina de l'H1
Per defecte un H1 salta a una pàgina senar (always-odd), però qualsevol objecte headings anul·la aquest valor, de manera que els capítols van seguits i span: 'page' no fa res. Torna a declarar headings.levels[0].breakBefore: { enabled: true, parity } a cada configuració. Capítols que obren en pàgina senar →
Error freqüent
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
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
Un espai de no separació continua partint la línia
A postext 1.4.1 l'algorisme de tall tracta U+00A0 com un espai normal, de manera que 0,08 %, 2,006 s o secció 2 poden quedar en dues línies. Ajunta els dos elements (0,08%) o reescriu la frase. Escapaments i caràcters literals →
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
Només 8 llengües tenen partició de mots, amb el codi exacte
La partició de mots existeix per a en-us, es, fr, de, it, pt, ca i nl, amb el codi exacte: 'es-ES' o qualsevol altra llengua passa sense avís a l'anglès americà. Partició de mots i llengua del document →
Error freqüent
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 →
- A postext 1.4.1 el filet de columna sota un títol a tota l'amplada comença a la vora superior de la caixa encara que la banda faci un nombre enter de línies de la retícula, com aquesta. El tapa un rectangle del color de la pàgina, pintat per sota dels altres elements del disseny.
- Una columna de 60 mm admet uns 50 caràcters, i l'anglès de Smyth no es pot reescriure. A la captura, 18 de les 218 línies justificades angleses (un 8 %) estiren l'espai més d'1,7 vegades la seva amplada normal, davant de menys d'un 2 % a l'edició castellana, la traducció de la qual es va ajustar a la columna. L'anglès arribava al 17 % abans de canviar per unes altres les cinc entrades que pitjor componien (Aground, Aloft, Belay, Cockpit, Crow’s nest). Tria entrades que componguin bé, o eixampla la columna.
Crèdits
- Recepta
- Ignacio Ferro
- Text
- The Sailor’s Word-Book (London: Blackie and Son, 1867), entries from A, B and C · W. H. Smyth; Sir Edward Belcher · domini públic
- Spanish translation, editor’s note and colophon · Postext Cookbook · original
- Fonts
- Alegreya (SIL OFL 1.1) · Alegreya SC (SIL OFL 1.1) · Alegreya Sans 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)


