En poques paraules
Els mateixos dos paràgrafs sobre tipografia, amb les mateixes fonts, impresos quatre vegades. Cada pàgina fa servir una manera diferent de citar, i les citacions i la llista final hi canvien, tot i que el text es va escriure un sol cop.
Què compondràs
Un assaig de dos paràgrafs sobre per què la bona tipografia passa inadvertida, amb sis fonts: una citació narrativa, una amb pàgina, una que omet l'autor perquè la frase ja l'anomena i dues obres citades juntes. El text es compon quatre vegades en una pàgina de 155 × 235 mm, i l'únic que canvia entre les quatre composicions és citations.style: APA 7, Chicago autor-any, IEEE i ISO 690. Cada pàgina porta el nom de l'estil en una banda del seu propi color, de manera que, posades l'una al costat de l'altra, es llegeixen com una taula comparativa. Les citacions canvien de forma, d'ordre i de puntuació, la llista canvia d'ordre i de noms, i l'assaig és el mateix fitxer.
Aquesta recepta respon a
- Com passo un llibre d'un estil de cita a un altre sense tocar el text?
- Com cito obres i componc la bibliografia en APA, IEEE o un altre estil de cita?
La resposta curta
// Citations are written once, Pandoc's way: [@key], [@a; @b], [-@key] without the author,
// @key [p. 17] inside the sentence. citeproc-js writes them, and the list, in the style named.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const SHOWN = [ // the same text, built once per entry
{ style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) },
{ style: 'chicago-author-date', band: 'chicago', name: 'Chicago',
system: t({ en: 'author-date', es: 'autor-año' }) },
{ style: 'ieee', band: 'ieee', name: 'IEEE', system: t({ en: 'numbered', es: 'numérico' }) },
{ style: t({ en: 'iso690-author-date-en', es: 'iso690-author-date-es' }), band: 'iso',
name: 'ISO 690', system: t({ en: 'author-date', es: 'autor-año' }) },
];
const citations = (style) => ({
style, // 'apa' → '(Warde, 1955)'; 'ieee' → '[1]'; the list is sorted and set to match
link: true,
bibliography: { fontSize: em(0.85), lineHeight: pt(11.2), hangingIndent: mm(6),
entrySpacing: pt(2), labelWidth: mm(6), doi: 'hide' },
});
Ingredients
- Funcions
- Cites en un estil de citaBibliografia a partir de les referènciesObertures dissenyadesPaleta de color semàntica
- Tipografia
- Source Serif 4, Source Sans 3 (SIL OFL 1.1)
- Recursos
- Cap: totes les imatges es dibuixen en codi
Elaboració
#1 · Una sola opció
El codi és la resposta curta de més amunt. Les citacions s'escriuen una vegada al contingut, amb la sintaxi de Pandoc, i les referències van en un bloc BibTeX al final. citations.style anomena un estil CSL, i citeproc-js l'utilitza per escriure cada citació i cada entrada: APA posa coma abans de l'any i ordena dues obres per autor, Chicago respecta l'ordre en què es van escriure, IEEE les converteix en números per ordre de primera citació, ISO 690 posa en majúscules els cognoms de la llista. En un llibre en curs, el canvi és aquest: un valor de la configuració, sense tocar el text.
#2 · Quatre composicions, dos plecs
// The style is set for the whole document, so each style is a build of its own.
const docs = [];
for (at = 0; at < SHOWN.length; at++) {
// pageIndexOffset makes each page the next one of a book, so the desk pairs them:
// APA beside Chicago, IEEE beside ISO 690.
const content = { markdown, continuation: { pageIndexOffset: at + 1 } };
docs.push(await buildWithFonts(() => buildDocument(content, config()), markdown));
}
L'estil val per a tot el document, així que les quatre versions són quatre composicions del mateix Markdown. Cadascuna rep un pageIndexOffset una unitat més gran que l'anterior, que converteix la seva pàgina en la següent d'un llibre imaginari; la taula les aparella llavors en dos plecs, amb els dos estils autor-any que menys es diferencien l'un al costat de l'altre.
#3 · El nom de l'estil en una banda
const opener = (s, i) => ({
enabled: true,
minHeight: mm(41), // from the top margin to under the title, 63 mm from the trim
slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col(s.band) },
placement: { anchor: { to: 'page', edge: 'top-left' },
size: { width: 'fill', height: mm(44) } } },
{ kind: 'text', id: 'kicker', content: `${t({ en: 'Citation style', es: 'Estilo de cita' })}`
+ ` · ${i + 1} / ${SHOWN.length} · ${s.system}`, fontFamily: SANS, fontSize: pt(8),
fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('paper'),
align: 'left',
placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(12) } } },
{ kind: 'text', id: 'name', content: s.name, fontFamily: SERIF, fontSize: pt(54),
fontWeight: 600, lineHeight: 1, color: col('paper'), align: 'left', overflow: 'clip',
placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(5) },
size: { width: mm(119), height: 'auto' } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF, fontSize: pt(20),
fontWeight: 600, lineHeight: 1.15, color: col('ink'), align: 'left', overflow: 'wrap',
placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(52) },
size: { width: mm(119), height: 'auto' } } },
] },
});
El disseny del títol dibuixa la banda des del tall, l'avanttítol amb el número i el sistema de l'estil, i el nom a 54 pt. El nom i el sistema surten de l'script i no del Markdown, perquè el text és el mateix als quatre documents.
const palette = {
ink: '#1d1b19', muted: '#66605a', rule: '#cfc8bf', paper: '#ffffff',
apa: '#2f5d8a', chicago: '#8c2f2b', ieee: '#24476b', iso: '#3c6b4f', // the four bands
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Each document points main-color at its own band, so any default left follows it.
const colorPalette = (band) => Object.entries({ ...palette, 'main-color': palette[band] })
.map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
Cada document apunta main-color a la seva pròpia banda, així que els quatre colors són entrades enllaçades de la paleta, i qualsevol valor per defecte que la configuració no fixi segueix la banda de la seva pàgina.
La recepta completa
// ═══ Postext Cookbook · Nº 093 · One text in four citation styles ════════════════ // https://postext.dev/en/cookbook/one-text-four-citation-styles // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Source Serif 4, Source Sans 3 (SIL OFL 1.1) · Needs postext ≥ 1.12.0 import { buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine, } from 'https://esm.sh/postext'; import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc'; const LANG = 'es'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'one-text-four-citation-styles'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: one ink, and a colour for each style's band const palette = { ink: '#1d1b19', muted: '#66605a', rule: '#cfc8bf', paper: '#ffffff', apa: '#2f5d8a', chicago: '#8c2f2b', ieee: '#24476b', iso: '#3c6b4f', // the four bands }; const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); // Each document points main-color at its own band, so any default left follows it. const colorPalette = (band) => Object.entries({ ...palette, 'main-color': palette[band] }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const [SERIF, SANS] = ['Source Serif 4', 'Source Sans 3']; const LEAD = 14; // pt // #region answer: the style is one setting; everything else stays the same // Citations are written once, Pandoc's way: [@key], [@a; @b], [-@key] without the author, // @key [p. 17] inside the sentence. citeproc-js writes them, and the list, in the style named. registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const SHOWN = [ // the same text, built once per entry { style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) }, { style: 'chicago-author-date', band: 'chicago', name: 'Chicago', system: t({ en: 'author-date', es: 'autor-año' }) }, { style: 'ieee', band: 'ieee', name: 'IEEE', system: t({ en: 'numbered', es: 'numérico' }) }, { style: t({ en: 'iso690-author-date-en', es: 'iso690-author-date-es' }), band: 'iso', name: 'ISO 690', system: t({ en: 'author-date', es: 'autor-año' }) }, ]; const citations = (style) => ({ style, // 'apa' → '(Warde, 1955)'; 'ieee' → '[1]'; the list is sorted and set to match link: true, bibliography: { fontSize: em(0.85), lineHeight: pt(11.2), hangingIndent: mm(6), entrySpacing: pt(2), labelWidth: mm(6), doi: 'hide' }, }); // #endregion // #region band: the style's name, large, in a band of its colour const opener = (s, i) => ({ enabled: true, minHeight: mm(41), // from the top margin to under the title, 63 mm from the trim slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col(s.band) }, placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(44) } } }, { kind: 'text', id: 'kicker', content: `${t({ en: 'Citation style', es: 'Estilo de cita' })}` + ` · ${i + 1} / ${SHOWN.length} · ${s.system}`, fontFamily: SANS, fontSize: pt(8), fontWeight: 600, letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('paper'), align: 'left', placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(12) } } }, { kind: 'text', id: 'name', content: s.name, fontFamily: SERIF, fontSize: pt(54), fontWeight: 600, lineHeight: 1, color: col('paper'), align: 'left', overflow: 'clip', placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(5) }, size: { width: mm(119), height: 'auto' } } }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF, fontSize: pt(20), fontWeight: 600, lineHeight: 1.15, color: col('ink'), align: 'left', overflow: 'wrap', placement: { anchor: { to: 'page', edge: 'top-left' }, offset: { x: mm(18), y: mm(52) }, size: { width: mm(119), height: 'auto' } } }, ] }, }); // #endregion let at = 0; // the entry of SHOWN being built const config = () => ({ // a factory: the engine caches resolved configs per object locale: t({ en: 'en-us', es: 'es' }), colorPalette: colorPalette(SHOWN[at].band), citations: citations(SHOWN[at].style), page: { sizePreset: 'custom', width: mm(155), height: mm(235), dpi: 150, margins: { top: mm(22), bottom: mm(20), left: mm(18), right: mm(18) } }, layout: { layoutType: 'single' }, bodyText: { fontFamily: SERIF, fontSize: pt(10), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true, avoidWidows: true, avoidOrphans: true, avoidRunts: true, }, headings: { fontFamily: SERIF, color: col('ink'), levels: [ { level: 1, breakBefore: { enabled: true, parity: 'any' }, // gotcha: headings-drop-h1-break advancedDesign: opener(SHOWN[at], at) }, ] }, header: { elements: [] }, footer: { elements: [ { kind: 'text', id: 'colophon', content: t({ en: 'Set in Source Serif 4 and Source Sans 3 (SIL OFL) · Text: original, CC BY 4.0', es: 'Compuesto en Source Serif 4 y Source Sans 3 (SIL OFL) · Texto: original, CC BY 4.0' }), fontFamily: SANS, fontSize: pt(7), color: col('muted'), placement: { anchor: { to: 'page', edge: 'bottom-left' }, offset: { x: mm(18), y: mm(-11) } } }, ] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Mostra en Markdown · 33 línies · content.es.md
title: "La página invisible" author: "Recetario de Postext" --- # La página que no se ve La buena tipografía está hecha para pasar inadvertida. @warde1955 lo dijo con una imagen que los impresores aún citan: la página debe ser una copa de cristal, un vidrio transparente que deja ver el vino, y no un cáliz de oro que lo esconde. Tschichold defendió algo parecido sobre el libro, que en sus ensayos sirve a su lector antes que a su diseñador [-@tschichold1991]. El argumento es tan moral como práctico, y @bringhurst2004 [pág. 17] lo convierte en el primer principio de su manual: la tipografía existe para honrar el contenido. La mitad práctica tiene pruebas a su favor. Los ensayos de legibilidad midieron cómo cambian la velocidad de lectura el tamaño de la letra, la longitud de la línea y el interlineado, y el estudio de los movimientos oculares explicó después por qué importa una textura uniforme: el ojo capta unas pocas letras en cada fijación y planea el salto siguiente según la forma de la línea que tiene delante [@tinker1963; @rayner1998]. Una página que llama la atención sobre sí misma le cuesta algo al lector, por poco que sea. Nada de esto reduce el diseño a una cuestión de gusto. El lector que nunca repara en una página bien hecha repara enseguida en una mal hecha, y la gramática de una buena puede enseñarse [@lupton2010]. :::references{format=bibtex} @book{warde1955, author = {Warde, Beatrice}, title = {The Crystal Goblet: Sixteen Essays on Typography}, editor = {Jacob, Henry}, publisher = {Sylvan Press}, address = {London}, year = 1955} @book{tschichold1991, author = {Tschichold, Jan}, title = {The Form of the Book: Essays on the Morality of Good Design}, translator = {Hadeler, Hajo}, publisher = {Hartley \& Marks}, address = {Point Roberts, WA}, year = 1991} @book{bringhurst2004, author = {Bringhurst, Robert}, title = {The Elements of Typographic Style}, edition = {3}, publisher = {Hartley \& Marks}, address = {Point Roberts, WA}, year = 2004} @book{tinker1963, author = {Tinker, Miles A.}, title = {Legibility of Print}, publisher = {Iowa State University Press}, address = {Ames, IA}, year = 1963} @article{rayner1998, author = {Rayner, Keith}, title = {Eye movements in reading and information processing: 20 years of research}, journal = {Psychological Bulletin}, volume = 124, number = 3, pages = {372--422}, year = 1998, doi = {10.1037/0033-2909.124.3.372}} @book{lupton2010, author = {Lupton, Ellen}, title = {Thinking with Type}, edition = {2}, publisher = {Princeton Architectural Press}, address = {New York}, year = 2010} :::`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Source Serif 4': ['400', '400i', '600', '600i', '700', '700i'], // 700: the list's title 'Source Sans 3': ['400', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); // #region build: one text, four documents, shown as two spreads // The style is set for the whole document, so each style is a build of its own. const docs = []; for (at = 0; at < SHOWN.length; at++) { // pageIndexOffset makes each page the next one of a book, so the desk pairs them: // APA beside Chicago, IEEE beside ISO 690. const content = { markdown, continuation: { pageIndexOffset: at + 1 } }; docs.push(await buildWithFonts(() => buildDocument(content, config()), markdown)); } // #endregion showPages(docs, { title: t({ en: 'One text, four styles', es: 'Un texto, cuatro estilos' }) });Kit · core, fonts, viewer: igual a totes les receptes · 235 línies
// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook ───────── function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v1 ── the same in every recipe · postext.dev/cookbook ──────── // Postext measures text with the faces the browser has loaded, and caches the // widths, so every face must be ready before the first build. Faces come from // Fontsource: the same static files the PDF embeds, so screen and PDF agree. /** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With * `optional`, a face Fontsource does not ship is skipped instead of failing. * Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', }; const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin']; const jobs = []; let added = 0; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const meta = optional ? await fontsourceMeta(family) : null; for (const spec of new Set(specs)) { const weight = parseInt(spec, 10); const style = spec.endsWith('i') ? 'italic' : 'normal'; if (hasFace(family, weight, style)) continue; if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().then((ready) => { document.fonts.add(ready); added++; }, () => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return added; } /** Runs `build` (a buildDocument or buildBundle call) and checks the faces * the pages use. A regular face missing from FONTS is loaded with a warning; * bold and italic variants are loaded when the family ships them. Then the * measurement caches are cleared and the build runs again. */ async function buildWithFonts(build, text = '') { const tried = new Set(); for (let round = 0; round < 3; round++) { kitStatus('Laying out…'); await new Promise(requestAnimationFrame); // let the status paint first const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; }); const wanted = { base: {}, variants: {} }; for (const { font, base } of [result].flat().flatMap(fontStringsOf)) { const { family, weight, style } = parseFont(font); const key = `${family}|${weight}|${style}`; if (tried.has(key) || hasFace(family, weight, style)) continue; tried.add(key); (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`); } if (Object.keys(wanted.base).length) { console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`); } const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true }); if (added === 0) return result; clearMeasurementCache(); } throw new Error('The fonts did not settle after three builds.'); } /** Every font string of the layout. `base` marks a block's own face; its * bold, italic and bold-italic variants are listed whether or not used. */ function fontStringsOf(doc) { const found = new Map(); const walk = (node) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(walk); return; } for (const [key, value] of Object.entries(node)) { if (typeof value === 'string' && /fontString$/i.test(key)) { found.set(value, found.get(value) || key === 'fontString'); } else if (value && typeof value === 'object') walk(value); } }; walk(doc.pages); walk(doc.blocks); return [...found].map(([font, base]) => ({ font, base })); } /** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }. * A string with no weight ('95.8px Young Serif', from a design text) is 400. */ function parseFont(font) { const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim()); if (!m) throw new Error(`Unexpected font string: ${font}`); const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]); return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' }; } /** True when a loaded FontFace covers exactly this family, weight and style * (document.fonts.check() is also true for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v1 ── the same in every recipe · postext.dev/cookbook ─────── /** Shows the pages as facing spreads on a dark desk: the first page is a * recto on its own, then verso | recto pairs, as in a bound book. Pages * are painted when they scroll near the screen. */ function showPages(docs, { title, width = 460 } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
L'script.js compost funciona tal com és: enganxa'l com a script de mòdul en qualsevol pàgina o obre la recepta a CodePen. Carpeta de la recepta a GitHub ↗ (s'obre en una pestanya nova)
Variants
#Prova un estil de notes
Amb Chicago notes i bibliografia, cada citació es converteix en una nota a peu de pàgina i la llista en una bibliografia; el text continua sense canviar.
- { style: 'apa', band: 'apa', name: 'APA 7', system: t({ en: 'author-date', es: 'autor-año' }) },
+ { style: 'chicago-notes-bibliography', band: 'apa', name: 'Chicago', system: 'notas' },#Escriu els números volats
Un estil numèric pot conservar la seva llista i imprimir les citacions com a números volats, com fan Nature i Vancouver.
- link: true,
+ link: true, marker: 'superscript',Errors freqüents
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
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
Posa entre cometes cada valor del frontmatter
YAML llegeix title: 1984 com un nombre i una data com un objecte Date, i els valors que no són cadenes s'imprimeixen buits als marcadors i deixen el PDF sense títol. Posa entre cometes cada valor: title: "1984". Metadades del document →
Error freqüent
Una configuració es desa a la memòria cau per identitat: crea un objecte nou
El motor desa a la memòria cau les configuracions resoltes segons la identitat de l'objecte, de manera que modificar el mateix objecte i tornar a compondre reutilitza el resultat anterior. Crea un objecte nou a cada composició: per això la configuració d'una recepta és una funció, config(). Pàgines en un canvas →
- El localitzador s'escriu una vegada i cada estil imprimeix la seva pròpia paraula: aquí, «p. 17» als quatre. L'estil ISO 690 en castellà que porta el motor imprimeix «p.» per a qualsevol localitzador, també per a una secció o un capítol, i per això aquest text cita una pàgina.
- El número d'edició va a BibTeX com a número (
edition = {3}): així cada estil escriu «3rd ed.», «3.ª ed.» o «3.» a la seva manera. Escrit com a «3rd», s'imprimeix tal qual i es perd la forma pròpia de l'estil.
Crèdits
- Recepta
- Ignacio Ferro
- Text
- Text original, CC BY 4.0
- Fonts
- Source Serif 4 (SIL OFL 1.1) · Source Sans 3 (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)


