En pocas palabras
Los mismos dos párrafos sobre tipografía, con las mismas fuentes, impresos cuatro veces. Cada página usa una manera distinta de citar, y las citas y la lista final cambian con ella, aunque el texto se escribió una sola vez.
Lo que vas a componer
Un ensayo de dos párrafos sobre por qué la buena tipografía pasa inadvertida, con seis fuentes: una cita narrativa, una con página, una que omite el autor porque la frase ya lo nombra y dos obras citadas juntas. El texto se compone cuatro veces en una página de 155 × 235 mm, y lo único que cambia entre las cuatro composiciones es citations.style: APA 7, Chicago autor-año, IEEE e ISO 690. Cada página lleva el nombre del estilo en una banda de su propio color, de modo que, puestas una junto a otra, se leen como una tabla comparativa. Las citas cambian de forma, de orden y de puntuación, la lista cambia de orden y de nombres, y el ensayo es el mismo archivo.
Esta receta responde a
- ¿Cómo paso un libro de un estilo de cita a otro sin tocar el texto?
- ¿Cómo cito obras y compongo la bibliografía en APA, IEEE u otro estilo de cita?
La respuesta corta
// 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' },
});
Ingredientes
- Funciones
- Citas en un estilo de citaBibliografía a partir de las referenciasAperturas diseñadasPaleta de color semántica
- Tipografía
- Source Serif 4, Source Sans 3 (SIL OFL 1.1)
- Recursos
- Ninguno: todas las imágenes se dibujan en código
Elaboración
#1 · Un solo ajuste
El código es la respuesta corta de arriba. Las citas se escriben una vez en el contenido, con la sintaxis de Pandoc, y las referencias van en un bloque BibTeX al final. citations.style nombra un estilo CSL, y citeproc-js escribe con él cada cita y cada entrada: APA pone coma antes del año y ordena dos obras por autor, Chicago respeta el orden en que se escribieron, IEEE las convierte en números por orden de primera cita, ISO 690 pone en mayúsculas los apellidos de la lista. En un libro en marcha, el cambio es este: un valor de la configuración, sin tocar el texto.
#2 · Cuatro composiciones, dos pliegos
// 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));
}
El estilo vale para todo el documento, así que las cuatro versiones son cuatro composiciones del mismo Markdown. Cada una recibe un pageIndexOffset una unidad mayor que la anterior, que convierte su página en la siguiente de un libro imaginario; la mesa las empareja entonces en dos pliegos, con los dos estilos autor-año que menos se diferencian uno al lado del otro.
#3 · El nombre del estilo 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 diseño del título dibuja la banda desde el corte, el antetítulo con el número y el sistema del estilo, y el nombre a 54 pt. El nombre y el sistema salen del script y no del Markdown, porque el texto es el mismo en los cuatro documentos.
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 documento apunta main-color a su propia banda, así que los cuatro colores son entradas enlazadas de la paleta, y cualquier valor por defecto que la configuración no fije sigue la banda de su página.
La receta 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`---Muestra en Markdown · 33 líneas · 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 en todas las recetas · 235 líneas
// ─── 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 ───────────────────────────────────────────────────────────────────────
El script.js compuesto funciona tal cual: pégalo como script de módulo en cualquier página o abre la receta en CodePen. Carpeta de la receta en GitHub ↗ (se abre en una nueva pestaña)
Variantes
#Prueba un estilo de notas
Con Chicago notas y bibliografía, cada cita se convierte en una nota al pie y la lista en una bibliografía; el texto sigue sin cambiar.
- { 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' },#Escribe los números volados
Un estilo numérico puede conservar su lista e imprimir las citas como números volados, como hacen Nature y Vancouver.
- link: true,
+ link: true, marker: 'superscript',Errores frecuentes
Error frecuente
Cualquier objeto headings desactiva el salto de página del H1
Por defecto un H1 salta a una página impar (always-odd), pero cualquier objeto headings anula ese valor, así que los capítulos van seguidos y span: 'page' no hace nada. Vuelve a declarar headings.levels[0].breakBefore: { enabled: true, parity } en cada configuración. Capítulos que abren en página impar →
Error frecuente
Carga todas las fuentes antes de componer
La composición mide el texto con las fuentes que el navegador ha cargado y guarda los anchos, así que una fuente que llega después de la primera composición deja cortes de línea erróneos y un PDF que ya no coincide con la pantalla. Carga antes todos los pesos y estilos, y llama a clearMeasurementCache() antes de recomponer si alguna llega tarde. Fuentes antes de componer →
Error frecuente
Entrecomilla cada valor del frontmatter
YAML lee title: 1984 como un número y una fecha como un objeto Date, y los valores que no son cadenas se imprimen vacíos en los marcadores y dejan el PDF sin título. Entrecomilla cada valor: title: "1984". Metadatos del documento →
Error frecuente
Una configuración se cachea por identidad: crea un objeto nuevo
El motor guarda en caché las configuraciones resueltas según la identidad del objeto, así que modificar el mismo objeto y volver a componer reutiliza el resultado anterior. Crea un objeto nuevo en cada composición: por eso la configuración de una receta es una función, config(). Páginas en un canvas →
- El localizador se escribe una vez y cada estilo imprime su propia palabra: aquí, «p. 17» en los cuatro. El estilo ISO 690 en español que trae el motor imprime «p.» para cualquier localizador, también para una sección o un capítulo, y por eso este texto cita una página.
- El número de edición va en BibTeX como número (
edition = {3}): así cada estilo escribe «3rd ed.», «3.ª ed.» o «3.» a su manera. Escrito como «3rd», se imprime tal cual y se pierde la forma propia del estilo.
Créditos
- Receta
- Ignacio Ferro
- Texto
- Texto original, CC BY 4.0
- Fuentes
- Source Serif 4 (SIL OFL 1.1) · Source Sans 3 (SIL OFL 1.1)
- Código
- MIT, como Postext
Editar este texto ↗ (se abre en una nueva pestaña)Carpeta de la receta en GitHub ↗ (se abre en una nueva pestaña)


