En bref
Un chapitre de thèse de doctorat. L’auteur écrit un code court pour chaque source ; Postext imprime les citations et, à la fin, la liste des références dans le style APA.
Ce que vous allez composer
Le deuxième chapitre d’une thèse de doctorat en sciences de l’éducation, la revue de la littérature : ce que vingt ans d’études disent de la lecture sur papier et sur écran. Le chapitre est composé sur une page A4 reliée à gauche, en Literata avec un interlignage de 16 pt, sous un bandeau pâle dans le bleu de l’université. L’auteur garde ses sources dans Zotero et colle l’export en BibTeX ; dans le texte, chaque source est une clé. Postext met chaque citation en forme en APA 7, avec l’auteur, l’année et la page quand elle est donnée, écrit « Delgado et al. (2018) » quand le nom fait partie de la phrase, et construit la liste des références dans l’ordre et sous la forme que demande le style. Quand le directeur de thèse demande Chicago, le style est la seule ligne qui change.
Cette recette répond à
- Comment citer des ouvrages et construire la bibliographie en APA, en IEEE ou dans un autre style de citation ?
La réponse courte
// Citations are written [@key, p. 33] and formatted by citeproc-js in the chosen CSL
// style; the references come from the BibTeX block at the end of the chapter. Register
// the engine once, before the first build, then the style is one setting.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
style: 'apa', // 'chicago-author-date', 'ieee', 'vancouver'… change nothing else
link: true, // each citation jumps to its entry in the PDF and on screen
bibliography: {
// APA asks for a half-inch hanging indent and keeps the list in the text size.
fontSize: em(1), hangingIndent: mm(12.7), entrySpacing: pt(4),
doi: 'link', // printed whole, as APA wants, and clickable
},
};
Ingrédients
- Fonctionnalités
- Citations dans un style bibliographiqueBibliographie tirée des référencesRenvoisTitres numérotésStyles de titreChapitres non numérotésOuvertures composéesTitres courants et foliosPalette de couleurs sémantiqueExport PDF
- Utilise aussi
- Des citations qui placent les figuresAttributs de titreTitres courants selon le rôle de la pagePolices intégrées au PDF
- Typographie
- Literata, Public Sans (SIL OFL 1.1)
- Ressources
- Aucune : chaque image est dessinée en code
Préparation
#1 · Un moteur, un style
Le code est dans la réponse courte plus haut. Postext lit les citations et laisse leur formulation à un moteur de citation, enregistré une fois avant la première composition ; postext-citeproc enveloppe citeproc-js, le moteur qu’utilisent Zotero et Mendeley, avec seize styles CSL et leurs fichiers de langue. [@mangen2013] donne « (Mangen et al., 2013) », @delgado2018 sans crochets place les auteurs dans la phrase, et [@ibanez2023, pp. 12–15] reporte l’étendue de pages dans les parenthèses. Avec un document en espagnol, les mêmes clés prennent leurs mots de liaison et leurs abréviations dans le fichier de langue espagnol, car le moteur suit la langue du document.
#2 · Les références viennent d’un bloc BibTeX
Le dernier bloc du chapitre est :::references{format=bibtex}, avec l’export de Zotero collé à l’intérieur et les accents écrits sous la forme {\'a}. :::bibliography, placé au-dessus, met la liste sous le titre « References » ; sans lui, Postext ajoute la liste à la fin. Seuls les travaux cités apparaissent, triés comme le veut l’APA, et chaque citation du texte renvoie à son entrée.
#3 · Un titre sans numéro, des renvois en toutes lettres
headingStyles: [{ id: 'references', numbered: false }],
crossRefs: { section: t({ en: 'Section {n}', es: 'Sección {n}' }) },
Le titre des références prend le style references, qui le garde dans la table des matières mais hors de la numérotation : la liste ne devient pas la section 2.4. :ref{id="sec-speed"} imprime « Section 2.2 » à partir de l’étiquette de crossRefs et renvoie au titre ; les numéros viennent de startAt=2 sur le titre du chapitre, le même compteur que celui des titres.
#4 · L’ouverture se loge dans un bandeau
const BAND = 92; // mm from the trim's top
const opener = {
enabled: true,
minHeight: mm(BAND - 28 + 10), // the body starts 10 mm under the band
slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col('tint') },
placement: { anchor: { to: 'page', edge: 'top-left' },
size: { width: 'fill', height: mm(BAND) } } },
{ kind: 'text', id: 'label', content: t({ en: 'Chapter', es: 'Capítulo' }),
fontFamily: LABEL, fontSize: pt(9), fontWeight: 600, letterSpacing: pt(1.8),
textTransform: 'uppercase',
color: col('accent'), align: 'left',
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { x: mm(0), y: mm(2) } } },
{ kind: 'text', id: 'number', content: '{number}', fontFamily: TEXT, fontSize: pt(96),
fontWeight: 300, lineHeight: 0.9, color: col('accent'), align: 'right',
placement: { anchor: { to: 'container', edge: 'top-right' },
offset: { x: mm(0), y: mm(-6) },
size: { width: mm(40), height: 'auto' } } },
{ kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontSize: pt(24),
lineHeight: 1.12, fontWeight: 600, color: col('ink'), align: 'left', overflow: 'wrap',
placement: { anchor: { to: 'container', edge: 'top-left' },
offset: { x: mm(0), y: mm(14) },
size: { width: mm(108), height: 'auto' } } },
] },
};
Le bandeau est une boîte ancrée au bord de coupe, et le texte commence 10 mm sous lui. Le numéro est composé en Literata Light de 96 pt, à droite, où il se lit comme une forme et laisse au titre toute sa justification.
const palette = {
ink: '#1b1b1f', accent: '#22406a', tint: '#e6ecf4', rule: '#b9bec8', muted: '#5d6370',
};
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' } }));
La recette complète
// ═══ Postext Cookbook · Nº 087 · A thesis chapter cited in APA 7 ═══════════════════ // https://postext.dev/en/cookbook/apa-thesis-with-bibtex // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Literata, Public Sans (SIL OFL 1.1) · Needs postext ≥ 1.25.0 import { buildDocumentWithFonts, renderPageToCanvas, 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 = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'apa-thesis-with-bibtex'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: a thesis prints in black; the one colour is the university's const palette = { ink: '#1b1b1f', accent: '#22406a', tint: '#e6ecf4', rule: '#b9bec8', muted: '#5d6370', }; 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 = 'Literata', LABEL = 'Public Sans'; const LEAD = 16; // pt: double spacing is a typewriter habit; 1.45 reads as well and fits more // #region answer: the citation engine, APA 7 and how its references look // Citations are written [@key, p. 33] and formatted by citeproc-js in the chosen CSL // style; the references come from the BibTeX block at the end of the chapter. Register // the engine once, before the first build, then the style is one setting. registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const citations = { style: 'apa', // 'chicago-author-date', 'ieee', 'vancouver'… change nothing else link: true, // each citation jumps to its entry in the PDF and on screen bibliography: { // APA asks for a half-inch hanging indent and keeps the list in the text size. fontSize: em(1), hangingIndent: mm(12.7), entrySpacing: pt(4), doi: 'link', // printed whole, as APA wants, and clickable }, }; // #endregion // #region opener: a pale band at the head of the page, the number large and light in it const BAND = 92; // mm from the trim's top const opener = { enabled: true, minHeight: mm(BAND - 28 + 10), // the body starts 10 mm under the band slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col('tint') }, placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(BAND) } } }, { kind: 'text', id: 'label', content: t({ en: 'Chapter', es: 'Capítulo' }), fontFamily: LABEL, fontSize: pt(9), fontWeight: 600, letterSpacing: pt(1.8), textTransform: 'uppercase', color: col('accent'), align: 'left', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: mm(0), y: mm(2) } } }, { kind: 'text', id: 'number', content: '{number}', fontFamily: TEXT, fontSize: pt(96), fontWeight: 300, lineHeight: 0.9, color: col('accent'), align: 'right', placement: { anchor: { to: 'container', edge: 'top-right' }, offset: { x: mm(0), y: mm(-6) }, size: { width: mm(40), height: 'auto' } } }, { kind: 'text', id: 'title', content: '{titleText}', fontFamily: TEXT, fontSize: pt(24), lineHeight: 1.12, fontWeight: 600, color: col('ink'), align: 'left', overflow: 'wrap', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: mm(0), y: mm(14) }, size: { width: mm(108), height: 'auto' } } }, ] }, }; // #endregion const head = (id, content, edge, x) => ({ kind: 'text', id, content, pages: 'body', fontFamily: LABEL, fontSize: pt(7.5), letterSpacing: pt(0.8), color: col('muted'), placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(14) } }, }); const config = () => ({ locale: t({ en: 'en-us', es: 'es' }), colorPalette, citations, page: { sizePreset: 'custom', width: mm(210), height: mm(297), dpi: 150, // one-sided, bound at the left margins: { top: mm(28), bottom: mm(28), left: mm(35), right: mm(25) }, }, layout: { layoutType: 'single' }, bodyText: { fontFamily: TEXT, fontSize: pt(11), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('accent'), textAlign: 'justify', firstLineIndent: mm(6), indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true, minWordSpacing: 0.8, // no word space closes to less than four fifths avoidWidows: true, avoidOrphans: true, avoidRunts: true, }, headings: { fontFamily: TEXT, color: col('ink'), fontWeight: 600, levels: [ // A chapter opens on the next page, recto or verso. { level: 1, numberingTemplate: '{1}', fontSize: pt(22), breakBefore: { enabled: true, parity: 'any' }, advancedDesign: opener }, { level: 2, numberingTemplate: '{1}.{2}', numberSeparator: ' ', fontSize: pt(13), lineHeight: pt(LEAD), marginTop: pt(LEAD), marginBottom: pt(LEAD / 2) }, ], }, // #region refs: the references heading goes unnumbered; section references read in words headingStyles: [{ id: 'references', numbered: false }], crossRefs: { section: t({ en: 'Section {n}', es: 'Sección {n}' }) }, // #endregion header: { elements: [ head('title', '{title}', 'top-left', 35), head('folio', '{pageNumber}', 'top-right', -25), ] }, footer: { elements: [] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Exemple Markdown · 69 lignes · content.en.md
title: "Reading on Paper and on Screens" author: "Marta Ibáñez Roca" --- # Reading on Paper and on Screens: The State of the Evidence {#ch-review startAt=2} The question this thesis takes up is older than the screens it is usually asked about. Long before the first e-reader, typographers measured how the size of a letter, the length of a line and the space between lines changed the speed and the comfort of reading [@tinker1963]. What changed with screens was the scale of the experiment: by the 2010s a large share of what students read for their courses reached them through a display, and the question of whether that mattered left the laboratory for the lecture hall [@baron2015]. This chapter reviews what is known. :ref{id="sec-comprehension"} gathers the studies of comprehension, :ref{id="sec-speed"} those of speed and line length, and :ref{id="sec-attention"} the arguments about attention that frame the rest of the thesis. The method of the study itself follows in Chapter 3. ## Comprehension {#sec-comprehension} The most cited early comparison asked Norwegian upper-secondary students to read two texts, one narrative and one expository, either on paper or as PDF files on a computer screen [@mangen2013]. Those who read on paper scored better on the comprehension test that followed. The difference was modest, but it pointed in the same direction as a growing number of studies, and it raised a question the authors could not settle: whether the advantage came from the medium, from the way readers navigated it, or from what they expected to do with it. Two meta-analyses later pooled the evidence. @delgado2018 combined 54 studies with more than 170,000 participants and found an advantage for paper in the comprehension of informational texts, larger when reading was done under time pressure and, unexpectedly, larger in the more recent studies than in the older ones. @clinton2019 reached a similar conclusion from a narrower set of experiments: reading from paper led to better comprehension, while the time spent reading did not differ between the media. Neither review found a reliable difference for narrative texts. A pilot study run for this thesis with forty first-year students found the same pattern [@ibanez2023, pp. 12–15], although its sample was too small to settle it. ## Speed and line length {#sec-speed} Speed is the measure readers notice first and researchers trust least. A reader can go faster by understanding less, and a gain in words per minute says nothing of what was retained [@rayner2016]. Studies that control for comprehension tell a more modest story than the claims of speed-reading courses: the eyes move in short jumps, take in a few letters on either side of the fixation, and cannot be trained to take in a whole line at once [@rayner2016]. Line length is the variable typographers have argued about longest. On screen, @dyson2001 measured speed and comprehension at several line lengths and found that the two do not always improve together, which warns against judging a layout by speed alone. Older work on print had already shown that the best length depends on the size of the type and the leading, not on a fixed count of characters [@tinker1963; see also @ibanez2023, sec. 2]. ## Attention and the reading brain {#sec-attention} The empirical studies leave room for a wider argument. @wolf2018 holds that the habits formed by reading on screens, quick and skimming, carry over to the reading of long texts and weaken the slower processes that comprehension of a demanding argument needs. @baron2015 reaches a similar view from surveys of students, many of whom reported that they concentrated better on paper even when they preferred screens for convenience. These are arguments rather than measurements, and this thesis treats them as hypotheses: Chapter 3 tests whether the advantage for paper reported in :ref{id="sec-comprehension"} holds when the screen layout is set with the care a printed page receives. ## References {style="references"} :::bibliography{title=""} :::references{format=bibtex} @book{tinker1963, author = {Tinker, Miles A.}, title = {Legibility of print}, publisher = {Iowa State University Press}, address = {Ames, IA}, year = 1963} @book{baron2015, author = {Baron, Naomi S.}, title = {Words onscreen: The fate of reading in a digital world}, publisher = {Oxford University Press}, address = {New York}, year = 2015} @article{mangen2013, author = {Mangen, Anne and Walgermo, Bente R. and Br{\o}nnick, Kolbj{\o}rn}, title = {Reading linear texts on paper versus computer screen: Effects on reading comprehension}, journal = {International Journal of Educational Research}, volume = 58, pages = {61--68}, year = 2013, doi = {10.1016/j.ijer.2012.12.002}} @article{delgado2018, author = {Delgado, Pablo and Vargas, Crist{\'o}bal and Ackerman, Rakefet and Salmer{\'o}n, Ladislao}, title = {Don't throw away your printed books: A meta-analysis on the effects of reading media on reading comprehension}, journal = {Educational Research Review}, volume = 25, pages = {23--38}, year = 2018, doi = {10.1016/j.edurev.2018.09.003}} @article{clinton2019, author = {Clinton, Virginia}, title = {Reading from paper compared to screens: A systematic review and meta-analysis}, journal = {Journal of Research in Reading}, volume = 42, number = 2, pages = {288--325}, year = 2019, doi = {10.1111/1467-9817.12269}} @article{dyson2001, author = {Dyson, Mary C. and Haselgrove, Mark}, title = {The influence of reading speed and line length on the effectiveness of reading from screen}, journal = {International Journal of Human-Computer Studies}, volume = 54, number = 4, pages = {585--612}, year = 2001, doi = {10.1006/ijhc.2001.0458}} @article{rayner2016, author = {Rayner, Keith and Schotter, Elizabeth R. and Masson, Michael E. J. and Potter, Mary C. and Treiman, Rebecca}, title = {So much to read, so little time: How do we read, and can speed reading help?}, journal = {Psychological Science in the Public Interest}, volume = 17, number = 1, pages = {4--34}, year = 2016, doi = {10.1177/1529100615623267}} @techreport{ibanez2023, author = {Ib{\'a}{\~n}ez Roca, Marta}, title = {Screen layouts and comprehension: A pilot study}, institution = {Reading Lab}, type = {Working paper}, number = 3, year = 2023} @book{wolf2018, author = {Wolf, Maryanne}, title = {Reader, come home: The reading brain in a digital world}, publisher = {Harper}, address = {New York}, year = 2018} :::`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { Literata: ['300', '400', '400i', '600', '600i'], 'Public Sans': ['400', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── const doc = await buildDocumentWithFonts({ markdown }, config(), kitFonts(FONTS)); const title = t({ en: 'A thesis chapter in APA 7', es: 'Un capítulo de tesis en APA 7' }); showPages(doc, { title }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);Kit · core, fonts, viewer, pdf : identique dans toutes les recettes · 269 lignes
// ─── 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 v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { 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', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}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().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes 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; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), 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 v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { 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, { ...render, 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 v3 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType, with their range and * place: the nearest weight the family ships, upright with no italic; * latin, then what its letters need (kitSubsetsFor). */ async function fontsourceProvider(family, weight, style, request) { 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 text = String.fromCodePoint(...(request?.codePoints ?? [])); const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => { const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())), { unicodeRange: kitRanges()[subset], order }); })); return files.length === 1 ? files[0] : files; } /** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame * shows no 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 ───────────────────────────────────────────────────────────────────────
Le script.js composé s’exécute tel quel : collez-le dans le script de module de n’importe quelle page, ou ouvrez la recette sur CodePen. Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)
Variantes
#Changez de style
Le même chapitre en Chicago auteur-date supprime la virgule après le nom et place l’année après les auteurs.
- style: 'apa', // 'chicago-author-date', 'ieee', 'vancouver'… change nothing else
+ style: 'chicago-author-date',#Composez la liste plus petit que le texte
Beaucoup d’universités autorisent la liste des références un corps en dessous, en interligne simple.
- fontSize: em(1), hangingIndent: mm(12.7), entrySpacing: pt(4),
+ fontSize: em(0.9), lineHeight: pt(13), hangingIndent: mm(12.7), entrySpacing: pt(3),Erreurs fréquentes
- L’APA écrit les titres d’articles et de livres avec une majuscule initiale seulement, et citeproc-js imprime les titres tels que les donnent les données. Zotero enregistre la plupart des titres avec une majuscule à chaque mot : corrigez-les dans Zotero ou dans le BibTeX avant qu’ils n’arrivent sur la page.
Crédits
- Recette
- Ignacio Ferro
- Texte
- Texte original, CC BY 4.0
- Polices
- Literata (SIL OFL 1.1) · Public Sans (SIL OFL 1.1)
Modifier ce texte ↗ (s’ouvre dans un nouvel onglet)Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)


