En pocas palabras
Un libro de ensayos de autores distintos. Cada ensayo lleva el nombre de su autor bajo el título y termina con su propia lista de referencias, como en las obras colectivas.
Lo que vas a componer
La página en obra, una pequeña colección de ensayos sobre historia de la imprenta, con dos editores y tres autores. Cada autor entrega su ensayo en un archivo Markdown con su propio bloque BibTeX, y así lo conserva el libro: un volumen de 155 × 235 mm en Libre Caslon, una portada roja y capítulos que se abren con el número, el título y el nombre del autor. Cada capítulo se cierra con su lista de Referencias en Chicago autor-fecha, que recoge solo las obras citadas en ese ensayo. Dos autores citan The Printing Press as an Agent of Change, de Eisenstein, y la obra aparece en las dos listas; con la New Introduction to Bibliography de Gaskell pasa lo mismo en los capítulos 1 y 3.
Esta receta responde a
- ¿Cómo doy a cada capítulo de una obra colectiva su propio autor y su propia bibliografía?
- ¿Cómo cito obras y compongo la bibliografía en APA, IEEE u otro estilo de cita?
La respuesta corta
// Citations are [@key] in the text; each contributor's BibTeX goes in a :::references block
// in their own chapter. Register the engine once, before the first build.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
style: 'chicago-author-date', // the humanities' author-date: (Eisenstein 1979)
bibliography: {
// 'chapter': a :::bibliography lists the works its own document cites, so a work two
// contributors cite appears in both lists. 'book' (the default) would print one list,
// after the last chapter. The scope is per document, not per H1: each essay must be a
// document of its own in buildBundle (see the chapters region).
scope: 'chapter',
title: '', // no bold title paragraph: each chapter has its own ## References heading
fontSize: em(0.9), lineHeight: pt(12.4), hangingIndent: mm(5), entrySpacing: pt(2.6),
},
};
// Numbering and disambiguation still see the whole book: two Smiths of 2001 in different
// chapters would print 2001a and 2001b, as one bibliography would.
Ingredientes
- Funciones
- Bibliografía a partir de las referenciasCitas en un estilo de citaLibros construidos capítulo a capítuloAtributos de títuloAperturas diseñadasÍndice de contenidosCabeceras y foliosCabeceras según el tipo de páginaEstilos de títuloCapítulos sin númeroCubiertas, portadas y colofonesAnclaje de elementos de diseñoRecuadros flotantesMárgenes simétricosPaleta de color semánticaExportación a PDF
- También usa
- RecuadrosEquilibrado de columnasTítulos numeradosColor del papelFuentes incrustadas en el PDFCabeceras por sección
- Tipografía
- Libre Caslon Text, Libre Caslon Display, IBM Plex Sans Condensed (SIL OFL 1.1)
- Recursos
- Ninguno: todas las imágenes se dibujan en código
Elaboración
#1 · Un documento por autor
// A chapter's :::bibliography lists what that document cites; one Markdown file holding all
// three essays would make every list repeat the works of the chapters before it.
const chapters = [front, lowe, okafor, ferrer].map((markdown) => ({ markdown }));
const book = () => buildBundle({ chapters, config: config(), resources: [] });
El alcance de la respuesta corta es el de un documento. buildBundle compone los cuatro documentos como un solo libro, de modo que los folios, los números de capítulo y el índice siguen la secuencia, mientras cada :::bibliography reúne las citas de su propio archivo. Las referencias se quedan donde las escribió cada autor. Una clave definida en cualquier capítulo vale para todo el libro, así que habría bastado con una sola entrada compartida, pero con capítulos autónomos puedes cambiar el orden o sustituir un ensayo sin romper la lista de otro.
#2 · La firma es un atributo del título
// # The Compositor's Day {author="Margaret Lowe" short="The Compositor's Day"}
const byline = text('byline', '{attr.author}', { fontFamily: TEXT, italic: true,
fontSize: pt(12), color: col('ink'), align: 'left' }, below('title', 4));
const versoAuthor = head('verso-author', '{attr.author}', 'even',
at('page', 'top-left', MARGIN.outer + 8, 13)); // in the running head
const tocAuthor = { enabled: true, attr: 'author', fontFamily: TEXT, fontSize: pt(9.5),
color: col('muted') }; // a line under each title in the contents (italic by default)
El título de cada ensayo lleva author="…" y un título corto, short="…", para la cabecera. La apertura imprime el autor bajo el título, la cabecera de la página par nombra al autor del ensayo al que pertenece la página y el índice pone el nombre en una línea bajo cada entrada. Los preliminares no tienen que repetir la lista de autores.
#3 · La apertura
const opener = { enabled: true, minHeight: mm(48), slot: { elements: [
text('kicker', t({ en: 'Chapter {number}', es: 'Capítulo {number}' }), { ...label,
fontSize: pt(8.5), letterSpacing: pt(1.7), color: col('accent') },
at('container', 'top-left', 0, 4)),
text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(28), lineHeight: 1.08,
color: col('ink'), align: 'left', overflow: 'wrap' }, below('kicker', 3, { width: 'fill' })),
byline,
{ kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.2), color: col('accent'),
placement: { ...below('byline', 6), size: { width: mm(16) } } },
] } };
Antetítulo, título y firma se apilan desde la cabeza de la caja, cada uno anclado debajo del anterior, así que un título de dos líneas como el del capítulo 3 baja con él la firma y el filete. La reserva de 48 mm admite dos líneas de título, y el texto empieza en la misma línea de la rejilla en todos los capítulos.
#4 · Cabeceras para un libro de varios autores
function head(id, content, parity, placement, look = {}) { // hoisted: the byline uses it
return { ...text(id, content, { ...label, fontWeight: 500, fontSize: pt(7.5),
letterSpacing: pt(1.1), color: col('muted'), ...look }, placement), parity, pages: 'body' };
}
const folio = { fontWeight: 600, color: col('ink') };
const header = { elements: [
head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, 13), folio),
versoAuthor,
head('recto-title', '{attr.short}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), 13),
{ align: 'right' }),
head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, 13),
{ ...folio, align: 'right' }),
] };
const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all',
at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] };
Una obra colectiva nombra al autor en la página par y el capítulo en la impar, donde una monografía pondría el título del libro y el del capítulo. Las cabeceras se limitan a pages: 'body', de modo que las aperturas solo llevan un folio al pie.
#5 · Los preliminares
const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } };
const cover = { enabled: true, slot: { elements: [
{ kind: 'box', id: 'field', style: { backgroundColor: col('accent') },
placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: mm(150) } } },
text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(58), lineHeight: 1,
color: col('paper'), align: 'left', overflow: 'wrap' },
{ ...at('page', 'top-left', MARGIN.inner, 64), size: { width: mm(118) } }),
text('subtitle', '{subtitle}', { fontFamily: TEXT, italic: true, fontSize: pt(15),
color: col('paper'), align: 'left' }, below('title', 5)),
text('editors', '{attr.editors}', { ...label, fontSize: pt(9), letterSpacing: pt(1.8),
color: col('accent') }, at('page', 'top-left', MARGIN.inner, 162)),
text('contributors', '{attr.contributors}', { fontFamily: TEXT, italic: true, fontSize: pt(12),
color: col('ink'), align: 'left' }, below('editors', 3)),
text('imprint', '{attr.imprint}', { ...label, fontWeight: 500, fontSize: pt(7.5),
letterSpacing: pt(1.5), color: col('muted') }, at('page', 'bottom-left', MARGIN.inner, -16)),
] } };
const contentsOpener = { enabled: true, minHeight: mm(40), slot: { elements: [
text('kicker', '{title}', { ...label, fontSize: pt(8.5), letterSpacing: pt(1.7),
color: col('accent') }, at('container', 'top-left', 0, 4)),
text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(28), color: col('ink'),
align: 'left' }, below('kicker', 3)),
] } };
const contents = {
levels: [{ level: 1, fontFamily: TEXT, fontSize: pt(12.5), lineHeight: pt(LEAD),
numberFontFamily: LABEL, numberFontSize: pt(10), numberFontWeight: 600,
numberColor: col('accent'), numberWidth: mm(6), numberGap: mm(3), marginBottom: pt(LEAD) }],
pageNumber: { fontFamily: LABEL, fontSize: pt(9), fontWeight: 600, color: col('ink'),
width: mm(7) },
leader: { char: '. ', gap: mm(2) },
subtitle: tocAuthor,
};
La portada y el índice son dos títulos sin número en el primer documento, sin cabeceras y sin entrada en el índice. Sus diseños leen {title} y {subtitle} del frontmatter, y los editores y autores, de los atributos del propio título.
La receta completa
// ═══ Postext Cookbook · Nº 098 · An edited volume with a bibliography per chapter ═════ // https://postext.dev/en/cookbook/edited-volume-chapter-bibliographies // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Libre Caslon Text & Display, IBM Plex Sans Condensed (OFL 1.1) · Needs postext ≥ 1.12.1 // Three essays by three contributors, each a Markdown document of its own with its own // BibTeX block, laid out by buildBundle as one volume: every chapter closes on its own list. import { buildBundle, renderPageToCanvas, clearMeasurementCache, registerCitationEngine, } from 'https://esm.sh/postext'; import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf'; import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc'; const LANG = 'es'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'edited-volume-chapter-bibliographies'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: printer's red on a warm paper, the rest in ink const palette = { ink: '#211c19', // text: a warm near-black accent: '#8e2b1f', // printer's red: the cover field, kickers, the references heading rule: '#cdbfb2', // hairlines muted: '#6b5f57', // running heads, the contents' contributors, the colophon paper: '#fcfaf6', // the page, and the type on the red field }; const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const TEXT = 'Libre Caslon Text', DISPLAY = 'Libre Caslon Display'; const LABEL = 'IBM Plex Sans Condensed'; const MARGIN = { top: 23, bottom: 22, inner: 20, outer: 18 }; // mm, mirrored const LEAD = 13.8; // body leading in pt: the baseline grid const label = { fontFamily: LABEL, fontWeight: 600, textTransform: 'uppercase', align: 'left' }; const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); const below = (id, y, size) => ({ ...at(`#${id}`, 'below', 0, y), ...(size && { size }) }); const text = (id, content, look, placement) => ({ kind: 'text', id, content, ...look, placement }); // #region answer: one list per chapter: scope 'chapter' over a book of separate documents // Citations are [@key] in the text; each contributor's BibTeX goes in a :::references block // in their own chapter. Register the engine once, before the first build. registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const citations = { style: 'chicago-author-date', // the humanities' author-date: (Eisenstein 1979) bibliography: { // 'chapter': a :::bibliography lists the works its own document cites, so a work two // contributors cite appears in both lists. 'book' (the default) would print one list, // after the last chapter. The scope is per document, not per H1: each essay must be a // document of its own in buildBundle (see the chapters region). scope: 'chapter', title: '', // no bold title paragraph: each chapter has its own ## References heading fontSize: em(0.9), lineHeight: pt(12.4), hangingIndent: mm(5), entrySpacing: pt(2.6), }, }; // Numbering and disambiguation still see the whole book: two Smiths of 2001 in different // chapters would print 2001a and 2001b, as one bibliography would. // #endregion // #region byline: the contributor, from the heading, in three places // # The Compositor's Day {author="Margaret Lowe" short="The Compositor's Day"} const byline = text('byline', '{attr.author}', { fontFamily: TEXT, italic: true, fontSize: pt(12), color: col('ink'), align: 'left' }, below('title', 4)); const versoAuthor = head('verso-author', '{attr.author}', 'even', at('page', 'top-left', MARGIN.outer + 8, 13)); // in the running head const tocAuthor = { enabled: true, attr: 'author', fontFamily: TEXT, fontSize: pt(9.5), color: col('muted') }; // a line under each title in the contents (italic by default) // #endregion // #region opener: kicker, title and byline over a short red rule, sunk 48 mm const opener = { enabled: true, minHeight: mm(48), slot: { elements: [ text('kicker', t({ en: 'Chapter {number}', es: 'Capítulo {number}' }), { ...label, fontSize: pt(8.5), letterSpacing: pt(1.7), color: col('accent') }, at('container', 'top-left', 0, 4)), text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(28), lineHeight: 1.08, color: col('ink'), align: 'left', overflow: 'wrap' }, below('kicker', 3, { width: 'fill' })), byline, { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1.2), color: col('accent'), placement: { ...below('byline', 6), size: { width: mm(16) } } }, ] } }; // #endregion // #region running-heads: contributor on the verso, short title on the recto, folios outside function head(id, content, parity, placement, look = {}) { // hoisted: the byline uses it return { ...text(id, content, { ...label, fontWeight: 500, fontSize: pt(7.5), letterSpacing: pt(1.1), color: col('muted'), ...look }, placement), parity, pages: 'body' }; } const folio = { fontWeight: 600, color: col('ink') }; const header = { elements: [ head('verso-folio', '{pageNumber}', 'even', at('page', 'top-left', MARGIN.outer, 13), folio), versoAuthor, head('recto-title', '{attr.short}', 'odd', at('page', 'top-right', -(MARGIN.outer + 8), 13), { align: 'right' }), head('recto-folio', '{pageNumber}', 'odd', at('page', 'top-right', -MARGIN.outer, 13), { ...folio, align: 'right' }), ] }; const footer = { elements: [{ ...head('drop-folio', '{pageNumber}', 'all', at('container', 'top', 0, 9), { ...folio, align: 'center' }), pages: 'opener' }] }; // #endregion // #region front: the title page on a red field, and the contents const bare = { numbered: false, toc: false, header: { elements: [] }, footer: { elements: [] } }; const cover = { enabled: true, slot: { elements: [ { kind: 'box', id: 'field', style: { backgroundColor: col('accent') }, placement: { ...at('bleed', 'top-left'), size: { width: 'fill', height: mm(150) } } }, text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(58), lineHeight: 1, color: col('paper'), align: 'left', overflow: 'wrap' }, { ...at('page', 'top-left', MARGIN.inner, 64), size: { width: mm(118) } }), text('subtitle', '{subtitle}', { fontFamily: TEXT, italic: true, fontSize: pt(15), color: col('paper'), align: 'left' }, below('title', 5)), text('editors', '{attr.editors}', { ...label, fontSize: pt(9), letterSpacing: pt(1.8), color: col('accent') }, at('page', 'top-left', MARGIN.inner, 162)), text('contributors', '{attr.contributors}', { fontFamily: TEXT, italic: true, fontSize: pt(12), color: col('ink'), align: 'left' }, below('editors', 3)), text('imprint', '{attr.imprint}', { ...label, fontWeight: 500, fontSize: pt(7.5), letterSpacing: pt(1.5), color: col('muted') }, at('page', 'bottom-left', MARGIN.inner, -16)), ] } }; const contentsOpener = { enabled: true, minHeight: mm(40), slot: { elements: [ text('kicker', '{title}', { ...label, fontSize: pt(8.5), letterSpacing: pt(1.7), color: col('accent') }, at('container', 'top-left', 0, 4)), text('title', '{titleText}', { fontFamily: DISPLAY, fontSize: pt(28), color: col('ink'), align: 'left' }, below('kicker', 3)), ] } }; const contents = { levels: [{ level: 1, fontFamily: TEXT, fontSize: pt(12.5), lineHeight: pt(LEAD), numberFontFamily: LABEL, numberFontSize: pt(10), numberFontWeight: 600, numberColor: col('accent'), numberWidth: mm(6), numberGap: mm(3), marginBottom: pt(LEAD) }], pageNumber: { fontFamily: LABEL, fontSize: pt(9), fontWeight: 600, color: col('ink'), width: mm(7) }, leader: { char: '. ', gap: mm(2) }, subtitle: tocAuthor, }; // #endregion const config = () => ({ // a factory: the engine caches resolved configs per object locale: t({ en: 'en-us', es: 'es' }), colorPalette, citations, page: { sizePreset: 'custom', width: mm(155), height: mm(235), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true } }, layout: { layoutType: 'single' }, bodyText: { fontFamily: TEXT, fontSize: pt(9.6), 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, maxWordSpacing: 1.7, avoidWidows: true, avoidOrphans: true, avoidRunts: true }, headings: { fontFamily: DISPLAY, fontWeight: 400, color: col('ink'), // A page that ends short may open at most one extra line above a subhead. balancing: { maxLinesPerHeading: 1 }, levels: [ // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break). // 'any': an essay opens on the next page, recto or verso, as in most edited volumes. { level: 1, numberingTemplate: '{1}', marginBottom: pt(0), breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener }, { level: 2, fontFamily: TEXT, italic: true, fontSize: pt(11), lineHeight: pt(LEAD * 1.5), numberingTemplate: '', marginTop: pt(LEAD * 0.5), marginBottom: pt(0) }, ] }, headingStyles: [ { id: 'cover', ...bare, span: 'page', advancedDesign: cover }, { id: 'contents', ...bare, advancedDesign: contentsOpener }, // The references heading: the label face in red, out of the contents. { id: 'references', toc: false, fontFamily: LABEL, fontWeight: 600, italic: false, fontSize: pt(8.5), letterSpacing: pt(1.7), textTransform: 'uppercase', color: col('accent'), lineHeight: pt(LEAD * 2), marginTop: pt(LEAD), marginBottom: pt(0) }, ], toc: contents, calloutStyles: [{ id: 'colophon', placement: 'bottom', backgroundEnabled: false, stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') }, padding: { top: mm(2.5), right: pt(0), bottom: pt(0), left: pt(0) }, marginBottom: pt(0), body: { fontFamily: LABEL, fontSize: pt(7.5), lineHeight: pt(10.5), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0) } }], header, footer, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const front = String.raw`---Muestra en Markdown · 13 líneas · content.es.md
title: "La página en obra" subtitle: "Ensayos sobre imprenta, tipos y lectura" --- # La página en obra {style="cover" editors="Edición de Helen Ashby y Tomás Ribera" contributors="Margaret Lowe · Daniel Okafor · Lucía Ferrer" imprint="Quire Editions · Edimburgo · 2026"} # Índice {style="contents"} :::toc :::callout{type="colophon"} Compuesto en Libre Caslon Text, Libre Caslon Display e IBM Plex Sans Condensed (SIL OFL). Texto: original, CC BY 4.0. La obra, sus editores y sus autores son ficticios; las obras que citan son reales. :::`; // title page and contents (content.<lang>.md) const lowe = String.raw`# La jornada del cajista {author="Margaret Lowe" short="La jornada del cajista"}Muestra en Markdown · 44 líneas · content.lowe.es.md
Toda página impresa a mano pasaba por las manos de un cajista antes de llegar a la prensa. El cajista trabajaba de pie ante un par de cajas, las mayúsculas en la de arriba y las minúsculas en la de abajo; leía unas pocas palabras del original y sacaba los tipos uno a uno para ir colocándolos en el componedor que sostenía con la mano izquierda. Cuando el componedor estaba lleno, pasaba las líneas a la galera; cuando había líneas suficientes para una página, la ataba con un cordel y la apartaba hasta que pudiera imponerse la forma entera y acuñarla en su rama. Joseph Moxon describió esta rutina en sus *Mechanick Exercises* de 1683-1684, el primer manual completo de imprenta en inglés, y casi todo lo que los bibliógrafos escribieron después sobre el cajista parte de su relato [@gaskell1972]. El trabajo se pagaba a destajo. Al cajista se le contaban los cuadratines que componía, y su rapidez, su ortografía y su cuidado decidían lo que recibía el lector. Por eso importa el cajista a quien edita hoy un texto antiguo: entre el manuscrito del autor y el pliego impreso había un hombre con sus propias costumbres, que trabajaba contra el reloj y podía leer mal una palabra, quedarse sin una letra o cambiar una grafía para que la línea cuadrase. ## Lo que cuentan los tipos Los propios tipos guardan memoria de ese trabajo. Una letra con el remate mellado o el asta rota imprime el mismo defecto allí donde se use, y como el cajista devolvía los tipos a la caja cuando la página ya estaba tirada, la letra dañada reaparece unas páginas más adelante. Charlton Hinman siguió cientos de esas letras a lo largo del Primer Folio de Shakespeare, de 1623, y reconstruyó el orden en que se compusieron sus páginas, qué cajista compuso cada una y cómo pasaba el trabajo de una caja a otra [@hinman1963]. Su método exigía paciencia y una máquina de cotejo que lleva su nombre, pero partía de una idea sencilla: los tipos de un taller son finitos y sus movimientos dejan rastro. La ortografía ofrecía una segunda clase de indicios. Un cajista prefería *doe* y *goe*; otro, *do* y *go*; uno ponía dos puntos donde su compañero ponía una coma. Allí donde el original no imponía una forma, asomaba la preferencia del cajista, y una página compuesta por uno se lee distinta de la compuesta por otro. Los bibliógrafos reunieron esas preferencias en perfiles y los usaron para atribuir páginas y para pesar la autoridad de una lectura [@gaskell1972]. ## Impresores imaginarios El punto débil de estos métodos estaba en lo que suponían del taller. Los análisis de recurrencia de tipos solían dar por hecho que un taller trabajaba en un libro cada vez, con el cajista componiendo las páginas en orden y la prensa tirando cada forma en cuanto estaba lista. D. F. McKenzie contrastó esa imagen con los registros de la imprenta de la Universidad de Cambridge entre 1696 y 1712, que se conservan con un detalle poco común, y comprobó que no se sostenía [@mckenzie1969]. Había varios libros en curso a la vez. Los cajistas pasaban de un trabajo a otro según llegaba el original; los prensistas tiraban en la misma semana pliegos de libros distintos; el trabajo se paraba y se reanudaba según lo permitían el papel, el dinero o las correcciones del autor. McKenzie concluyó que los bibliógrafos habían construido sus argumentos sobre «impresores de la mente», un taller ordenado que existía en sus razonamientos y no en el archivo. Una pauta hallada en los tipos podía explicarse con más de una secuencia de trabajo, y no cabía deducir de ella una sola sin pruebas externas. Su artículo no acabó con el estudio de los cajistas, pero le cambió el tono: desde entonces, cada inferencia declara las condiciones en que vale. ## Lo que hacía el cajista El cajista producía además algo mayor que una página. Como una misma composición imprimía todos los ejemplares de un pliego, una edición de varios cientos de ejemplares llevaba el mismo texto, página a página, a lectores que nunca se conocerían. Elizabeth Eisenstein hizo de esa repetición el centro de su relato sobre la Europa moderna: el ejemplar idéntico, sostenía, permitió a estudiosos de ciudades distintas comparar las mismas tablas, los mismos mapas y los mismos textos, corregirlos en ediciones posteriores y apoyarse en el trabajo de los demás [@eisenstein1979]. El segundo ensayo de este volumen vuelve sobre su tesis y sobre las objeciones que encontró. Y, sin embargo, los ejemplares de una edición nunca eran del todo idénticos. Muchas erratas se descubrían con el pliego ya en la prensa; se aflojaba la forma, se corregía y no se tiraban los pliegos ya impresos, de modo que estados corregidos y sin corregir acababan encuadernados en la misma edición [@gaskell1972]. Hinman encontró esas variantes de prensa por todo el Folio [@hinman1963]. La jornada del cajista producía, pues, la estabilidad que describió Eisenstein y también las pequeñas inestabilidades que los bibliógrafos se pasan la vida cotejando. ## Referencias {style="references"} :::bibliography :::references{format=bibtex} @book{gaskell1972, author = {Gaskell, Philip}, title = {A New Introduction to Bibliography}, publisher = {Clarendon Press}, address = {Oxford}, year = 1972} @book{hinman1963, author = {Hinman, Charlton}, title = {The Printing and Proof-Reading of the First Folio of Shakespeare}, publisher = {Clarendon Press}, address = {Oxford}, year = 1963} @article{mckenzie1969, author = {McKenzie, D. F.}, title = {Printers of the Mind: Some Notes on Bibliographical Theories and Printing-House Practices}, journal = {Studies in Bibliography}, volume = 22, pages = {1--75}, year = 1969} @book{eisenstein1979, author = {Eisenstein, Elizabeth L.}, title = {The Printing Press as an Agent of Change: Communications and Cultural Transformations in Early-Modern Europe}, publisher = {Cambridge University Press}, address = {Cambridge}, year = 1979} :::`; // content.lowe.<lang>.md: chapter 1, and so on const okafor = String.raw`# Lo que significaba la fijeza {author="Daniel Okafor" short="Lo que significaba la fijeza"}Muestra en Markdown · 48 líneas · content.okafor.es.md
Pocos libros de historia del libro han dado tanto que discutir como el de Elizabeth Eisenstein. *The Printing Press as an Agent of Change* apareció en dos volúmenes en 1979 y sostenía que el paso del manuscrito a la imprenta fue por sí mismo una causa del renacer del saber en el Renacimiento, de la Reforma y de la nueva ciencia del siglo XVII [@eisenstein1979]. La tesis descansaba en unos pocos rasgos que la imprenta tenía y el manuscrito no: la difusión de cientos de ejemplares a la vez, la normalización del texto y de la imagen y, sobre todo, lo que ella llamó fijeza tipográfica, la capacidad de una edición para conservar un texto sin cambios y permitir que se corrigiera y mejorara en la siguiente. La tesis era ambiciosa, y su recepción se dividió según una línea conocida. Los historiadores de las ideas encontraron en ella una explicación de por qué el saber se volvió acumulativo después de 1500. Los historiadores de la imprenta señalaron la variedad de lo que salía de las prensas, desde almanaques plagados de erratas hasta ediciones piratas, y preguntaron si la fijeza era una propiedad de lo impreso o un resultado que algunos impresores lograban algunas veces. ## Crédito y piratería Adrian Johns dio a la segunda respuesta su forma más completa [@johns1998]. Al estudiar el comercio del libro en el Londres del siglo XVII, comprobó que los lectores tenían buenos motivos para desconfiar de lo que compraban. Las ediciones se reimprimían sin permiso, se atribuían a autores que no las habían escrito o salían con la fecha y el pie de imprenta equivocados. La Stationers' Company llevaba un registro de títulos para resolver las disputas entre sus miembros, pero ese registro protegía los acuerdos del gremio, no el texto del autor. Quien quería saber si un libro impreso era lo que decía ser dependía de la reputación del impresor y del librero, de quienes respondían por la edición y de los usos de la tienda donde se vendía. Para Johns, por tanto, la fijeza era algo que había que fabricar. Exigía el trabajo de autores que vigilaban sus pruebas, de libreros que cuidaban su nombre y de instituciones como la Royal Society, que autorizaba y supervisaba los libros que patrocinaba. Su ejemplo más citado es el del astrónomo real John Flamsteed, que vio impresas en 1712 sus observaciones en una edición que no había aprobado y pasó años intentando recuperarla y suprimirla. Donde Eisenstein veía un rasgo de la técnica, Johns veía un conjunto de prácticas que había que aprender y que podían fallar. ## Circuitos y modelos El debate entre ambas posturas se sigue mejor con un mapa de lo que atraviesa un libro. Robert Darnton trazó uno en 1982: un circuito de comunicación que va del autor al editor, a los impresores, a los transportistas, a los libreros y a los lectores, y de nuevo al autor, con cada etapa marcada por condiciones económicas, políticas e intelectuales [@darnton1982]. El modelo situó la historia del libro entre las personas que lo manejaban, y muchos estudios de los años ochenta lo recorrieron etapa por etapa. Thomas Adams y Nicolas Barker objetaron que el circuito de Darnton describía el comercio y perdía de vista el objeto. Propusieron un modelo centrado en el propio libro y en los momentos de su vida: la publicación, la fabricación, la distribución, la recepción y la supervivencia [@adams1993]. La diferencia pesa en la discusión sobre la fijeza. En el circuito de Darnton, la fijeza pertenece a las relaciones entre impresores, vendedores y lectores; en el modelo de Adams y Barker es también cuestión de qué ejemplares sobrevivieron, dónde y en qué estado. ## La forma del texto D. F. McKenzie amplió todavía más la pregunta en sus Panizzi Lectures, publicadas en 1986 [@mckenzie1986]. La bibliografía, sostuvo, debía estudiar los textos como formas registradas y los procesos de su transmisión, producción y recepción incluidas, y tratar la forma de un texto como parte de su sentido. Su ejemplo fue William Congreve, cuyas comedias cambiaron de carácter al recomponerse para las *Works* reunidas de 1710: la nueva disposición dividía las escenas de otro modo, marcaba las entradas de otra manera e invitaba a otra clase de lectura. Desde la posición de McKenzie, la disputa sobre la fijeza se parece menos a una disputa. Una edición puede fijar las palabras de un texto y cambiar a la vez su manera de leerse; una reimpresión puede conservar el sentido y perder la forma que lo sostenía. Eisenstein describió lo que la imprenta hacía posible, y Johns, lo que costaba que esa posibilidad se cumpliera [@eisenstein1979; @johns1998]. Las dos son historias de las mismas páginas, escritas desde extremos opuestos del circuito. ## Referencias {style="references"} :::bibliography :::references{format=bibtex} @book{eisenstein1979, author = {Eisenstein, Elizabeth L.}, title = {The Printing Press as an Agent of Change: Communications and Cultural Transformations in Early-Modern Europe}, publisher = {Cambridge University Press}, address = {Cambridge}, year = 1979} @book{johns1998, author = {Johns, Adrian}, title = {The Nature of the Book: Print and Knowledge in the Making}, publisher = {University of Chicago Press}, address = {Chicago}, year = 1998} @article{darnton1982, author = {Darnton, Robert}, title = {What Is the History of Books?}, journal = {Daedalus}, volume = 111, number = 3, pages = {65--83}, year = 1982} @incollection{adams1993, author = {Adams, Thomas R. and Barker, Nicolas}, title = {A New Model for the Study of the Book}, booktitle = {A Potencie of Life: Books in Society}, editor = {Barker, Nicolas}, publisher = {British Library}, address = {London}, pages = {5--43}, year = 1993} @book{mckenzie1986, author = {McKenzie, D. F.}, title = {Bibliography and the Sociology of Texts}, series = {The Panizzi Lectures}, publisher = {British Library}, address = {London}, year = 1986} :::`; const ferrer = String.raw`# Punzones, matrices y el nombre de Garamond {author="Lucía Ferrer" short="Punzones y matrices"}Muestra en Markdown · 45 líneas · content.ferrer.es.md
Antes de que un cajista pudiera componer una página, alguien tenía que fabricar las letras que sacaba de la caja. Lucien Febvre y Henri-Jean Martin calcularon que antes de 1500 se habían impreso en Europa entre quince y veinte millones de ejemplares de libros [@febvre1976]. Cada tipo de letra empezó como una serie de punzones de acero tallados a mano al tamaño que vería el lector. La secuencia de trabajo está bien documentada, aunque la destreza manual que exigía se ha perdido casi del todo [@carter1969]. El grabador de punzones daba forma al extremo de una barra de acero hasta obtener la letra invertida; la limaba, la grababa y la comprobaba con pruebas de humo. Los blancos interiores de la letra, los ojos de la *a*, la *e* o la *o*, solían abrirse en el punzón con un contrapunzón, lo que permitía repetir el mismo ojo en varias letras de la fundición. Fred Smeijers, que ha tallado punzones con sus manos, sostiene que el contrapunzón explica buena parte de la coherencia de las letras del siglo XVI: las formas se parecían porque se hacían con las mismas herramientas [@smeijers1996]. ## Del punzón al tipo El punzón templado se hundía en una barra de cobre para obtener una matriz. Una vez justificada, se encajaba en un molde manual, una caja ajustable de acero y madera en la que el fundidor vertía una aleación de plomo, estaño y antimonio. Cada colada daba un tipo, y la fundición terminada, frotada y repasada, se vendía a los impresores al peso [@gaskell1972]. Los punzones eran el capital del fundidor: se prestaban, se heredaban o se vendían, y una serie de matrices batidas con ellos permitía a otra fundición fundir la misma letra. Por eso las letras de unos pocos grabadores franceses de mediados del siglo XVI se extendieron por Europa en una generación. ## De quién son las letras Eso mismo complicaba las atribuciones. Una letra aparece en el libro de un impresor mucho después de haberse tallado, con el nombre del impresor y no con el del grabador. Harry Carter ordenó las pruebas disponibles: muestrarios, inventarios, contratos y los propios libros [@carter1969]. Hendrik Vervliet las aplicó durante décadas al Renacimiento francés, y sus catálogos devolvieron a otros grabadores de su tiempo, entre ellos Robert Granjon y Pierre Haultin, muchos tipos que se habían atribuido a Claude Garamont [@vervliet2008]. El nombre de Garamond tuvo además una historia moderna propia. Cuando las fundiciones de principios del siglo XX recuperaron la «Garamond» para la composición mecánica, varias tomaron como modelo unos tipos que no había tallado Garamont, sino Jean Jannon, impresor en Sedán, unos sesenta años después de la muerte de Garamont. Beatrice Warde lo demostró en 1926. El nombre se quedó. ## Lo que ve el lector Nada de esto se ve en la página tal como la encuentra el lector. El lector juzga un libro por lo bien que se lee. El bibliógrafo ve, en esa misma página, la huella de las limas de un grabador, el desgaste de una matriz y las costumbres del fundidor que la justificó. Los impresores antiguos compraban sus tipos por cuerpos con nombre propio, como la lectura o el entredós, y los juzgaban igual que el lector, por la página que hacían [@gaskell1972]. La historia de los tipos empieza en el acero, pero siempre se escribió para esa página. ## Referencias {style="references"} :::bibliography :::references{format=bibtex} @book{febvre1976, author = {Febvre, Lucien and Martin, Henri-Jean}, title = {The Coming of the Book: The Impact of Printing, 1450--1800}, translator = {Gerard, David}, publisher = {NLB}, address = {London}, year = 1976} @book{carter1969, author = {Carter, Harry}, title = {A View of Early Typography up to about 1600}, publisher = {Clarendon Press}, address = {Oxford}, year = 1969} @book{smeijers1996, author = {Smeijers, Fred}, title = {Counterpunch: Making Type in the Sixteenth Century, Designing Typefaces Now}, publisher = {Hyphen Press}, address = {London}, year = 1996} @book{gaskell1972, author = {Gaskell, Philip}, title = {A New Introduction to Bibliography}, publisher = {Clarendon Press}, address = {Oxford}, year = 1972} @book{vervliet2008, author = {Vervliet, Hendrik D. L.}, title = {The Palaeotypography of the French Renaissance: Selected Papers on Sixteenth-Century Typefaces}, publisher = {Brill}, address = {Leiden}, year = 2008} :::`; // #region chapters: one document per contributor, each with its own references // A chapter's :::bibliography lists what that document cites; one Markdown file holding all // three essays would make every list repeat the works of the chapters before it. const chapters = [front, lowe, okafor, ferrer].map((markdown) => ({ markdown })); const book = () => buildBundle({ chapters, config: config(), resources: [] }); // #endregion // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { // text, display and label faces (gotcha: fonts-first) 'Libre Caslon Text': ['400', '400i', '700'], 'Libre Caslon Display': ['400'], 'IBM Plex Sans Condensed': ['400', '500', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── const all = chapters.map((chapter) => chapter.markdown).join('\n'); await loadFonts(FONTS, all); const docs = await buildWithFonts(book, all); // one VDTDocument per Markdown document showPages(docs, { title: t({ en: 'The Working Page', es: 'La página en obra' }) }); offerPdf(() => renderToPdf(docs, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);Kit · core, fonts, viewer, pdf: igual en todas las recetas · 275 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 · pdf v1 ── the same in every recipe that exports a PDF ────────────── /** postext-pdf embeds TrueType bytes. Fetch the Fontsource file the screen * used, snapping to a weight the family ships and falling back to upright * when it has no italic: the PDF asks for every face a block could use. */ async function fontsourceProvider(family, weight, style) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} (${res.status})`); return decompressWoff2(new Uint8Array(await res.arrayBuffer())); } /** A "Build the PDF" button in the bar. Once built: "Open the PDF" (a new * tab, since CodePen's preview frame cannot show PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── /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
#Una sola lista para todo el volumen
Algunas editoriales reúnen todas las obras en una bibliografía final. Cambia el alcance y quita los tres títulos ## Referencias; la lista pasa a ir detrás del último capítulo.
- scope: 'chapter',
- title: '', // no bold title paragraph: each chapter has its own ## References heading
+ scope: 'book',
+ title: 'Bibliografía',#Cita en APA
Un volumen de ciencias sociales cambia el estilo y nada más.
- style: 'chicago-author-date', // the humanities' author-date: (Eisenstein 1979)
+ style: 'apa',Errores frecuentes
Error frecuente
Una clave definida dos veces conserva su primera entrada, sin aviso
Las referencias valen para todo el libro: una clave escrita en dos capítulos conserva la primera definición y la segunda se ignora sin aviso. En un libro de varios autores, pon un prefijo a las claves de cada uno. Bibliografía a partir de las referencias →
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
buildBundle no pasa metadatos de un capítulo a otro; {totalPages} es por capítulo
buildBundle no pasa metadatos de un capítulo al siguiente: {title}, {author} y los demás salen solo del frontmatter del propio capítulo, así que todo capítulo sin él los deja vacíos (y el Sandbox descarta el frontmatter de todos los capítulos menos el primero), y {totalPages} cuenta las páginas de cada capítulo. Escribe el título del libro literalmente en las cabeceras. Libros construidos capítulo a capítulo →
Error frecuente
Valores de atributo: sin { ni }; comillas simples si llevan "
Un valor de atributo termina en la llave de cierre, así que no puede contener { ni }. Un valor que lleve comillas dobles va entre comillas simples; el signo de dólar no da problemas. Atributos de título →
Error frecuente
Un estilo de título hereda el salto de página de su nivel
Una entrada de headingStyles toma de su nivel de título todo lo que no fija, también breakBefore. Un índice o un colofón con estilo sobre un H1 tras un :::pagebreak hereda la paridad 'odd' y cae detrás de una página en blanco. Dale a ese estilo breakBefore: { enabled: false }. Estilos de título →
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 →
scope: 'chapter'lista lo que el documento cita antes del:::bibliography, no lo que cita su sección H1. Con todos los ensayos en un mismo archivo Markdown, la segunda lista repite las obras del primer ensayo y la tercera, las de los dos anteriores.- En todo el libro manda la primera definición de cada clave. Si dos autores usan
smith2001para obras distintas, el ensayo posterior cita sin aviso el libro del primero: pon un prefijo a las claves de cada autor o coteja las listas con los originales. - La desambiguación se calcula para el libro entero. Dos obras distintas de Smith de 2001 en capítulos diferentes pueden salir como 2001a y 2001b, aunque la lista de un capítulo solo contenga una de ellas.
Créditos
- Receta
- Ignacio Ferro
- Texto
- Texto original, CC BY 4.0
- Fuentes
- Libre Caslon Text (SIL OFL 1.1) · Libre Caslon Display (SIL OFL 1.1) · IBM Plex Sans Condensed (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)


