En poques paraules
Un assaig sobre com les ciutats britàniques van adoptar l'hora de Londres. Cada font que cita l'autora es converteix en una nota numerada a peu de pàgina, i la llista d'obres va al final.
Què compondràs
Una separata de la sèrie d'història de la tècnica d'una revista: un assaig d'unes 1.100 paraules sobre com les ciutats britàniques van deixar el seu propi migdia pel de Londres entre 1840 i 1880. La pàgina fa 156 × 234 mm, amb marges en mirall, en Crimson Pro d'11/14 pt, i una banda verd ferrocarril cobreix la meitat superior de la primera. L'autora escriu cada font com una clau, [@howse1980, 87], i Postext compon cadascuna com una nota numerada a peu de pàgina en l'estil de notes i bibliografia de Chicago. La primera nota d'una obra dona autor, títol i editorial; les següents, la forma breu. Les notes pròpies de l'autora prenen el número de la mateixa sèrie, i la bibliografia tanca l'assaig.
Aquesta recepta respon a
- Com poso les cites en notes a peu de pàgina, a l'estil de Chicago, amb forma breu en les següents?
- Com cito obres i componc la bibliografia en APA, IEEE o un altre estil de cita?
La resposta curta
// The text cites with [@howse1980, 87]; a note style turns every citation into a
// footnote. The first note for a work gives it in full, later ones in the short form
// (author, short title, page), and the bibliography lists the works by author.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
style: 'chicago-notes-bibliography', // CMOS 18th edition, notes and bibliography
notes: 'footnote', // each citation in a note at the foot of the page
link: true, // a note's citation jumps to its bibliography entry
bibliography: { fontSize: em(0.92), lineHeight: pt(13), hangingIndent: em(1.8),
entrySpacing: pt(3) },
};
// The citation notes share the numbering and the look of the author's own [^notes].
const footnotes = {
placement: 'column', numbering: 'chapter',
fontSize: pt(8.5), lineHeight: pt(10.75), spaceBetween: pt(2),
textAlign: 'left', // ragged: the gap after a note's number stays the same in every note
separator: { width: 0.3, lineWidth: pt(0.5), color: col('rule') },
};
Ingredients
- Funcions
- Cites en notesCites en un estil de citaBibliografia a partir de les referènciesNotes a peu de pàginaObertures dissenyadesAtributs de títolCapçaleres i folisMarges simètricsEstils de paràgrafPaleta de color semànticaExportació a PDF
- També fa servir
- Referències creuadesColor del paperCapçaleres segons el tipus de pàginaFonts incrustades al PDF
- Tipografia
- Crimson Pro, IBM Plex Sans Condensed (SIL OFL 1.1)
- Recursos
- Cap: totes les imatges es dibuixen en codi
Elaboració
#1 · Una nota per a cada citació
El codi és la resposta curta de més amunt. chicago-notes-bibliography és un estil de notes: una citació deixa al text un número volat i les seves paraules passen a una nota a peu de pàgina, numerada juntament amb les [^notas] de l'autora; notes: 'footnote' ho diu de manera explícita. citeproc-js porta el compte de les obres ja citades, així que la nota 3 diu Derek Howse, Greenwich Time and the Discovery of the Longitude (Oxford University Press, 1980), 87 i la nota 4, que torna al mateix llibre, Howse, Greenwich Time and the Discovery of the Longitude, 90–91. L'estil inclòs és la 18a edició del Manual de Chicago, que abandona ibid. i repeteix la forma breu, de manera que les notes 11 i 12 la imprimeixen dues vegades.
#2 · Les notes de l'autora també citen
Una citació dins d'una [^nota] es queda en aquella nota i es compon dins de la línia, completa si és la primera vegada que apareix l'obra: la nota 1 és la frase de l'autora seguida de la referència completa de Schivelbusch. Tota citació acaba amb el punt de l'estil, així que una nota que acaba en una citació no porta punt propi al Markdown.
#3 · Prefixos, sufixos i localitzadors
[véase @simmons1991, 346–47; y @bartky2000, cap. 9] posa véase davant de la primera obra, separa les dues amb punt i coma i conserva el localitzador de cadascuna. Un número tot sol és una pàgina; cap. i chap. anomenen un capítol en qualsevol dels dos idiomes. El text que segueix el localitzador és un sufix: [@howse1980, 82–84, sobre los relojes de las diligencias] imprimeix la frase darrere de les pàgines.
#4 · L'obertura va en una banda
const BAND = 122; // mm from the trim's top
const opener = {
enabled: true,
minHeight: mm(BAND - MARGIN.top + 9), // the text starts 9 mm under the band
slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col('green') },
placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } },
{ kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontWeight: 600,
fontSize: pt(7.5), letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('tint'),
align: 'left', placement: at('container', 'top-left', 0, 4) },
{ kind: 'text', id: 'years', content: '{attr.years}', fontFamily: TEXT, fontWeight: 300,
fontSize: pt(54), lineHeight: 1, color: col('tint'), align: 'left',
placement: at('container', 'top-left', -1, 16) },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontWeight: 600,
fontSize: pt(25), lineHeight: 1.08, color: col('paper'), align: 'left', overflow: 'wrap',
placement: { ...at('container', 'top-left', 0, 47),
size: { width: mm(112), height: 'auto' } } },
{ kind: 'text', id: 'byline', content: '{attr.byline}', fontFamily: LABEL, fontWeight: 600,
fontSize: pt(9), letterSpacing: pt(0.6), color: col('tint'), align: 'left',
placement: at('container', 'top-left', 0, 92) },
] },
};
La banda baixa 122 mm des del tall i el text comença 9 mm per sota. Les dates surten d'un atribut del títol, years="1840–1880", en Crimson Pro Light de 54 pt, perquè la targeta mostri una xifra tan gran com un títol; l'atribut de la signatura alimenta a més la capçalera de les pàgines parelles.
const palette = {
ink: '#211d1a', green: '#24493d', tint: '#dfe8e2', rule: '#b7b0a4', muted: '#6a645b',
paper: '#fbf9f4',
};
// A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs).
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.green })
.map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
La recepta completa
// ═══ Postext Cookbook · Nº 089 · A history essay with Chicago notes ═══════════════ // https://postext.dev/en/cookbook/history-essay-chicago-notes // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Crimson Pro, IBM Plex Sans Condensed (SIL OFL 1.1) · Needs postext ≥ 1.12.0 import { buildDocument, 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 = 'history-essay-chicago-notes'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: a railway green for the opener band, ink and warm paper for the rest const palette = { ink: '#211d1a', green: '#24493d', tint: '#dfe8e2', rule: '#b7b0a4', muted: '#6a645b', paper: '#fbf9f4', }; // A design element paints the hex written beside its paletteId (gotcha: palette-skips-designs). const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries({ ...palette, 'main-color': palette.green }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const TEXT = 'Crimson Pro', LABEL = 'IBM Plex Sans Condensed'; const TRIM = { width: 156, height: 234 }; // a journal offprint, trimmed like a trade book const MARGIN = { top: 22, bottom: 22, inner: 20, outer: 17 }; const LEAD = 14; // pt const at = (to, edge, x = 0, y = 0) => ({ anchor: { to, edge }, offset: { x: mm(x), y: mm(y) } }); // #region answer: Chicago notes and bibliography, one note per citation // The text cites with [@howse1980, 87]; a note style turns every citation into a // footnote. The first note for a work gives it in full, later ones in the short form // (author, short title, page), and the bibliography lists the works by author. registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const citations = { style: 'chicago-notes-bibliography', // CMOS 18th edition, notes and bibliography notes: 'footnote', // each citation in a note at the foot of the page link: true, // a note's citation jumps to its bibliography entry bibliography: { fontSize: em(0.92), lineHeight: pt(13), hangingIndent: em(1.8), entrySpacing: pt(3) }, }; // The citation notes share the numbering and the look of the author's own [^notes]. const footnotes = { placement: 'column', numbering: 'chapter', fontSize: pt(8.5), lineHeight: pt(10.75), spaceBetween: pt(2), textAlign: 'left', // ragged: the gap after a note's number stays the same in every note separator: { width: 0.3, lineWidth: pt(0.5), color: col('rule') }, }; // #endregion // #region opener: the green band holds the kicker, the dates and the title const BAND = 122; // mm from the trim's top const opener = { enabled: true, minHeight: mm(BAND - MARGIN.top + 9), // the text starts 9 mm under the band slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col('green') }, placement: { ...at('page', 'top-left'), size: { width: 'fill', height: mm(BAND) } } }, { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: LABEL, fontWeight: 600, fontSize: pt(7.5), letterSpacing: pt(1.4), textTransform: 'uppercase', color: col('tint'), align: 'left', placement: at('container', 'top-left', 0, 4) }, { kind: 'text', id: 'years', content: '{attr.years}', fontFamily: TEXT, fontWeight: 300, fontSize: pt(54), lineHeight: 1, color: col('tint'), align: 'left', placement: at('container', 'top-left', -1, 16) }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontWeight: 600, fontSize: pt(25), lineHeight: 1.08, color: col('paper'), align: 'left', overflow: 'wrap', placement: { ...at('container', 'top-left', 0, 47), size: { width: mm(112), height: 'auto' } } }, { kind: 'text', id: 'byline', content: '{attr.byline}', fontFamily: LABEL, fontWeight: 600, fontSize: pt(9), letterSpacing: pt(0.6), color: col('tint'), align: 'left', placement: at('container', 'top-left', 0, 92) }, ] }, }; // #endregion const head = (id, content, parity, edge, x, align) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontWeight: 600, fontSize: pt(7.5), letterSpacing: pt(1.1), textTransform: 'uppercase', color: col('muted'), align, placement: at('page', edge, x, 12), }); const config = () => ({ // a factory: the engine caches resolved configs per object locale: t({ en: 'en-us', es: 'es' }), // exact codes (gotcha: hyphenation-locales) colorPalette, citations, footnotes, page: { sizePreset: 'custom', width: mm(TRIM.width), height: mm(TRIM.height), dpi: 150, backgroundColor: col('paper'), margins: { top: mm(MARGIN.top), bottom: mm(MARGIN.bottom), left: mm(MARGIN.inner), right: mm(MARGIN.outer), mirror: true }, }, layout: { layoutType: 'single' }, bodyText: { fontFamily: TEXT, fontSize: pt(11), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'), textAlign: 'justify', firstLineIndent: mm(5), indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true, avoidWidows: true, avoidOrphans: true, avoidRunts: true, }, headings: { fontFamily: TEXT, fontWeight: 600, color: col('ink'), levels: [ // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break). { level: 1, fontSize: pt(25), breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener }, { level: 2, fontFamily: LABEL, fontWeight: 600, fontSize: pt(8.5), letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('green'), lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2) }, ], }, paragraphStyles: [ { id: 'colophon', fontFamily: LABEL, fontSize: pt(7), lineHeight: pt(9.5), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) }, ], header: { elements: [ head('verso-folio', '{pageNumber}', 'even', 'top-left', MARGIN.outer, 'left'), head('verso-author', '{attr.byline}', 'even', 'top-left', MARGIN.outer + 8, 'left'), head('recto-title', '{title}', 'odd', 'top-right', -(MARGIN.outer + 8), 'right'), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -MARGIN.outer, 'right'), ] }, footer: { elements: [] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Mostra en Markdown · 104 línies · content.es.md
title: "La hora del ferrocarril" author: "Clara Whitcombe" --- # La hora del ferrocarril: cómo las ciudades británicas adoptaron la hora de Londres {#essay kicker="Ensayos de historia de la técnica · n.º 14" years="1840–1880" byline="Clara Whitcombe"} En el otoño de 1840, un viajero que salía de Paddington en el Great Western Railway llevaba consigo dos horas. El horario que guardaba en el bolsillo daba las horas de Londres; el reloj de la iglesia de Bath o de Bristol, al llegar, daba las del lugar, unos minutos atrasadas. Bristol está algo más de dos grados y medio al oeste de Greenwich, y su mediodía, el instante en que el sol cruza el meridiano local, llegaba unos diez minutos después que el de Londres. Durante siglos la diferencia no había importado. Una diligencia que tardaba un día en cubrir el trayecto perdía más de diez minutos por un caballo cojo. Un tren que lo cubría en cuatro horas, no.[^frame] Este ensayo sigue los cuarenta años en que se resolvió esa diferencia, desde los primeros horarios del Great Western hasta la ley que en 1880 hizo de la hora de Greenwich la hora legal de Gran Bretaña. La historia suele contarse como una victoria de la ingeniería sobre la costumbre. Se entiende mejor como una negociación, en la que compañías ferroviarias, compañías de telégrafos, ayuntamientos y tribunales resolvieron la cuestión cada uno a su manera, y no siempre en el mismo sentido. [^frame]: Sobre el efecto del ferrocarril en la percepción del tiempo y la distancia, véase [@schivelbusch1986, cap. 3] ## Las compañías deciden Los primeros ferrocarriles marcaban la marcha de sus trenes por los relojes de sus propias estaciones. En una línea de pocas millas aquello no causaba problemas, y los primeros horarios no decían a qué hora se referían. La dificultad llegó con las grandes líneas troncales de finales de la década de 1830, que unían ciudades cuyos relojes discrepaban en un cuarto de hora.[@simmons1991, 345] El Great Western imprimió la hora de Londres en sus horarios a partir de noviembre de 1840 y pidió a sus estaciones que la siguieran.[@howse1980, 87] En 1847 la Railway Clearing House, el organismo a través del cual las compañías liquidaban sus cuentas por el tráfico compartido, recomendó a todos sus miembros adoptar la hora de Greenwich desde el 1 de diciembre. A finales de 1848 casi todos lo habían hecho.[@howse1980, 90–91] La decisión fue comercial antes que científica. Una compañía que mantenía una sola hora en toda su línea necesitaba un solo horario y un solo reglamento para su personal, y podía publicar los enlaces con otras líneas sin una tabla de correcciones. Se eligió Greenwich porque en Londres tenían sus oficinas las compañías y el correo sus diligencias, no porque allí estuviera el observatorio.[^post] [^post]: El correo enviaba a sus guardas con relojes puestos en la hora de Londres desde la década de 1790; véase [@howse1980, 82–84, sobre los relojes de las diligencias] ## El hilo lleva la hora Al principio la hora viajaba en el reloj de un guarda del primer tren, puesto en Paddington, y los jefes de estación ajustaban los suyos por él. El telégrafo eléctrico cambió el medio. Desde 1852 el Observatorio Real enviaba cada día una señal por los hilos de la Electric Telegraph Company, que la transmitía a las estaciones y oficinas de correos de su red.[@standage1998, 59] La señal ponía la hora de Greenwich al alcance de cualquier lugar al que llegaran los hilos; no obligaba a nadie a seguirla.[@schivelbusch1986, 43–44] ## Las ciudades dudan La mayoría de las ciudades de las líneas principales cedieron en pocos años, porque un reloj que no coincidía con el de la estación hacía perder trenes. Algunas resistieron. En 1852 el reloj de la Lonja de Granos de Bristol recibió una segunda aguja de minutos, una para Bristol y otra para Londres, y conserva las dos.[@simmons1991, 346] En Exeter, adonde el ferrocarril llegó en 1844, el reloj de la catedral siguió marcando la hora de la ciudad durante algunos años después de que cambiara el de la estación.[^exeter] [^exeter]: Sobre las ciudades británicas, y sobre la misma disputa en las ciudades estadounidenses treinta años después, véase [@simmons1991, 346–47; y @bartky2000, cap. 9] La hora media local seguía al sol, y el sol aún ordenaba la jornada de campesinos, pescadores y de cualquiera cuyo trabajo dependiera de la luz del día. Una hora fijada en Londres era para ellos una imposición. El historiador de la disciplina fabril ha sostenido que el reloj se había convertido ya en el siglo XVIII en un medio de controlar el trabajo;[@thompson1967, 80–82] la disputa por la hora del ferrocarril extendió ese argumento a ciudades enteras, a las que se pedía aceptar una hora fijada en otra parte. ## Los tribunales se resisten Las leyes que mencionaban una hora se referían a la del lugar, y esa hora era la hora media local. En 1858 un juicio en Dorchester empezó a las diez según el reloj del tribunal, que seguía la hora de Londres, y se dictó sentencia contra un demandado que llegó a las diez según la hora de la ciudad, unos minutos más tarde. En la apelación, los jueces sostuvieron que la hora fijada era la local, y la sentencia se anuló.[@howse1980, 113] La Statutes (Definition of Time) Act de 1880 cerró la brecha. Declaraba que toda expresión de tiempo en una ley, una escritura u otro instrumento jurídico significaba, salvo indicación en contrario, la hora media de Greenwich en Gran Bretaña y la de Dublín en Irlanda.[@howse1980, 114] ## Un acuerdo, no una conquista Vista desde 1840, la hora del ferrocarril era uno de varios arreglos posibles. Las compañías podrían haber mantenido las horas locales e impreso tablas de correcciones, como hicieron algunos ferrocarriles estadounidenses hasta 1883,[@bartky2000, 140–45] y el sistema internacional de husos que siguió a la conferencia de Washington de 1884 tardó medio siglo en llegar a la mayor parte del mundo.[@ogle2015, cap. 1] En Gran Bretaña las distancias eran cortas, las compañías eran pocas y podían ponerse de acuerdo en una sola cámara de compensación, y el telégrafo llegó a las estaciones al mismo tiempo que los trenes. Esas condiciones, más que ningún principio, explican la rapidez con que un país de mediodías locales pasó a guardar una sola hora.[@ogle2015, 25; véase también @schivelbusch1986, cap. 3] ## Bibliografía :::bibliography{title=""} :::paragraphs{style="colophon"} Ensayo de muestra para el Recetario de Postext (CC BY 4.0). Obras citadas reales, páginas ilustrativas. Crimson Pro e IBM Plex Sans Condensed (SIL OFL). ::: :::references{format=csl-yaml} - id: schivelbusch1986 type: book author: [{family: Schivelbusch, given: Wolfgang}] title: "The Railway Journey: The Industrialization of Time and Space in the 19th Century" publisher: University of California Press publisher-place: Berkeley issued: 1986 - id: thompson1967 type: article-journal author: [{family: Thompson, given: E. P.}] title: "Time, Work-Discipline, and Industrial Capitalism" container-title: Past & Present issue: 38 page: 56-97 issued: 1967 - id: howse1980 type: book author: [{family: Howse, given: Derek}] title: Greenwich Time and the Discovery of the Longitude publisher: Oxford University Press publisher-place: Oxford issued: 1980 - id: simmons1991 type: book author: [{family: Simmons, given: Jack}] title: The Victorian Railway publisher: Thames and Hudson publisher-place: London issued: 1991 - id: standage1998 type: book author: [{family: Standage, given: Tom}] title: The Victorian Internet publisher: Walker publisher-place: New York issued: 1998 - id: bartky2000 type: book author: [{family: Bartky, given: Ian R.}] title: "Selling the True Time: Nineteenth-Century Timekeeping in America" publisher: Stanford University Press publisher-place: Stanford, CA issued: 2000 - id: ogle2015 type: book author: [{family: Ogle, given: Vanessa}] title: The Global Transformation of Time, 1870–1950 publisher: Harvard University Press publisher-place: Cambridge, MA issued: 2015 :::`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── // Every face the design uses (gotcha: fonts-first). const FONTS = { 'Crimson Pro': ['300', '400', '400i', '600', '600i'], 'IBM Plex Sans Condensed': ['400', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── await loadFonts(FONTS, markdown); const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown); const title = t({ en: 'A history essay with Chicago notes', es: 'Un ensayo de historia con notas de Chicago' }); showPages(doc, { title }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);Kit · core, fonts, viewer, pdf: igual a totes les receptes · 275 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 · 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 ───────────────────────────────────────────────────────────────────────
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
#Porta les notes al final
Algunes revistes reuneixen les notes després de l'assaig; les citacions conserven les formes completa i breu.
- placement: 'column', numbering: 'chapter',
+ placement: 'chapterEnd', numbering: 'chapter',#Torna a escriure ibid.
La 17a edició de l'estil de Chicago imprimeix ibid. a la nota que repeteix l'obra anterior. Enganxa el fitxer .csl d'aquest estil, del repositori d'estils de Zotero, en una cadena i fes-lo servir com a estil propi.
- style: 'chicago-notes-bibliography', // CMOS 18th edition, notes and bibliography
+ style: 'custom', customStyle: CHICAGO_17TH_CSL, // the XML of the .csl fileErrors 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
Una paleta canviada no arriba als elements de disseny ni al color de les remissions
postext 1.4.1 aplica colorPalette als estils de text (cos, títols, llistes, peus, taules, requadres), però no als elements de capçaleres, peus de pàgina, obertures i portadelles, ni a bodyText.referenceColor: conserven l'hex escrit al costat del seu paletteId. Si canvies la paleta, per a una edició de pantalla fosca o per recolorejar, reescriu cada color enllaçat a partir de colorPalette abans de compondre. Paleta de color semàntica →
Error freqüent
Només 8 llengües tenen partició de mots, amb el codi exacte
La partició de mots existeix per a en-us, es, fr, de, it, pt, ca i nl, amb el codi exacte: 'es-ES' o qualsevol altra llengua passa sense avís a l'anglès americà. Partició de mots i llengua del document →
Error freqüent
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 →
- No tanquis amb punt una nota que acaba en una citació: l'estil ja hi posa el seu, i Postext no fon els dos, així que la nota acabaria amb dos punts.
- En un estil de notes,
@howse1980sense claudàtors deixa a la frase només el número de la nota, Com ¹ diu, sense el nom de l'autor. Escriu tu el nom i cita entre claudàtors:Com diu Howse,[@howse1980].
Crèdits
- Recepta
- Ignacio Ferro
- Text
- Text original, CC BY 4.0
- Fonts
- Crimson Pro (SIL OFL 1.1) · IBM Plex Sans Condensed (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)


