# Um capítulo de tese citado em APA 7

> Um capítulo de tese de doutorado cujas citações [@key, p. 33] saem em APA 7 pelo citeproc-js, com a lista de referências montada a partir de um bloco BibTeX.

- Versão HTML: https://postext.dev/pt/cookbook/apa-thesis-with-bibtex
- Receita Nº 087 · Estrutura do livro · Nível 2 (Intermediário) · Saídas: Canvas, PDF
- Gêneros: Artigos e trabalhos acadêmicos, Relatórios
- Requer postext ≥ 1.12.0, postext-pdf ≥ 1.12.0 · testada com 1.19.1, postext-pdf 1.19.1 em 2026-10-06
- Páginas: [1](https://postext.dev/cookbook/apa-thesis-with-bibtex/en/p01.webp?v=a8b3536b), [2](https://postext.dev/cookbook/apa-thesis-with-bibtex/en/p02.webp?v=a8b3536b), [3](https://postext.dev/cookbook/apa-thesis-with-bibtex/en/p03.webp?v=a8b3536b)
- PDF: https://postext.dev/cookbook/apa-thesis-with-bibtex/en/apa-thesis-with-bibtex.pdf?v=a8b3536b
- Abrir no Sandbox: https://postext.dev/pt/sandbox#recipe=apa-thesis-with-bibtex&lang=en (.postext: https://postext.dev/cookbook/apa-thesis-with-bibtex/en/apa-thesis-with-bibtex.postext)
- Última atualização: 2026-10-01
- Outros idiomas: [en](https://postext.dev/en/cookbook/apa-thesis-with-bibtex.md), [es](https://postext.dev/es/cookbook/apa-thesis-with-bibtex.md), [ca](https://postext.dev/ca/cookbook/apa-thesis-with-bibtex.md), [zh](https://postext.dev/zh/cookbook/apa-thesis-with-bibtex.md), [ja](https://postext.dev/ja/cookbook/apa-thesis-with-bibtex.md), [ar](https://postext.dev/ar/cookbook/apa-thesis-with-bibtex.md)

## Em poucas palavras

Um capítulo de uma tese de doutorado. A autora escreve um código curto para cada fonte; o Postext imprime as citações e a lista de referências do final no estilo APA.

## O que você vai compor

O segundo capítulo de uma tese de doutorado em educação, a revisão de literatura: o que vinte anos de estudos dizem sobre ler no papel e na tela. O capítulo é composto numa página A4 encadernada à esquerda, em Literata com entrelinha de 16 pt, sob uma faixa clara no azul da universidade. A autora guarda as fontes no Zotero e cola a exportação em BibTeX; no texto, cada fonte é uma chave. O Postext formata cada citação em APA 7, com o autor, o ano e a página quando há, escreve “Delgado et al. (2018)” quando o nome faz parte da frase e monta a lista de referências na ordem e na forma que o estilo pede. Quando o orientador pedir Chicago, o estilo é a única linha que muda.

**Esta receita responde a:**

- Como cito fontes em APA 7 e monto a lista de referências a partir do meu arquivo BibTeX?

## A resposta curta

```js
// script.js, linhas 27–39
// 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
  },
};
```

## Ingredientes

**Ensina**

- [Citações em um estilo de citação](https://postext.dev/pt/docs/document-format.md#citações-e-bibliografia): Obras citadas como [@chave, p. 33] e formatadas em um estilo CSL (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) escolhido nas configurações, com link para as respectivas entradas.
- [Bibliografia a partir das referências](https://postext.dev/pt/docs/document-format.md#citações-e-bibliografia): A lista de obras citadas, montada a partir das referências do documento (front matter ou um bloco BibTeX) no lugar onde estiver :::bibliography ou depois do último capítulo.

**Também usa**

- [Referências cruzadas](https://postext.dev/pt/docs/document-format.md#referências-cruzadas-e-âncoras)
- [Títulos numerados](https://postext.dev/pt/docs/configuration.md#sobrescritas-por-nível)
- [Estilos de título](https://postext.dev/pt/docs/configuration.md#estilos-de-título)
- [Capítulos sem número](https://postext.dev/pt/docs/configuration.md#estilos-de-título)
- [Aberturas desenhadas](https://postext.dev/pt/docs/configuration.md#largura-e-design-avançado)
- [Cabeços e fólios](https://postext.dev/pt/docs/configuration.md#cabeços-e-rodapés)
- [Paleta de cores semântica](https://postext.dev/pt/docs/configuration.md#paleta-de-cores)
- [Exportação para PDF](https://postext.dev/pt/docs/configuration.md#geração-de-pdf)
- [Citações que posicionam as figuras](https://postext.dev/pt/docs/document-format.md#referência-em-linha-a-forma-principal)
- [Atributos de título](https://postext.dev/pt/docs/document-format.md#atributos-de-título)
- [Cabeços por tipo de página](https://postext.dev/pt/docs/configuration.md#elementos-de-texto)
- [Fontes incorporadas ao PDF](https://postext.dev/pt/docs/configuration.md#por-que-um-provedor-de-fontes)

**A configuração em resumo**

- [`bodyText`](https://postext.dev/pt/docs/configuration.md#texto-do-corpo), [`citations`](https://postext.dev/pt/docs/configuration.md#citações), [`colorPalette`](https://postext.dev/pt/docs/configuration.md#paleta-de-cores), [`crossRefs`](https://postext.dev/pt/docs/configuration.md#referências-cruzadas), [`footer`](https://postext.dev/pt/docs/configuration.md#cabeços-e-rodapés), [`header`](https://postext.dev/pt/docs/configuration.md#cabeços-e-rodapés), [`headingStyles`](https://postext.dev/pt/docs/configuration.md#estilos-de-título), [`headings`](https://postext.dev/pt/docs/configuration.md#títulos), [`layout`](https://postext.dev/pt/docs/configuration.md#diagramação), [`locale`](https://postext.dev/pt/docs/configuration.md#hifenização), [`page`](https://postext.dev/pt/docs/configuration.md#página)

**API**

- [`LOCALES`](https://postext.dev/pt/docs/document-format.md#citações-e-bibliografia), [`STYLES`](https://postext.dev/pt/docs/document-format.md#citações-e-bibliografia), [`buildDocument`](https://postext.dev/pt/docs/configuration.md#compilar-um-documento), [`clearMeasurementCache`](https://postext.dev/pt/docs/configuration.md#cache-de-medidas), [`createCiteprocEngine`](https://postext.dev/pt/docs/document-format.md#citações-e-bibliografia), [`decompressWoff2`](https://postext.dev/pt/docs/configuration.md#provedor-de-fontes-no-navegador-fontsource--woff2), [`registerCitationEngine`](https://postext.dev/pt/docs/document-format.md#citações-e-bibliografia), [`renderPageToCanvas`](https://postext.dev/pt/docs/configuration.md#renderizar-uma-página-como-bitmap), [`renderToPdf`](https://postext.dev/pt/docs/configuration.md#geração-de-pdf)

**Tipos**

- Literata (OFL-1.1), Public Sans (OFL-1.1)

## Preparo

### 1 · Um motor, um estilo

O código é [a resposta curta](https://postext.dev/pt/cookbook/apa-thesis-with-bibtex.md#a-resposta-curta) logo acima. O Postext lê as citações e deixa a redação delas a um motor de citações, registrado uma vez antes da primeira composição; `postext-citeproc` empacota o citeproc-js, o motor que o Zotero e o Mendeley usam, com dezesseis estilos CSL e os seus arquivos de idioma. `[@mangen2013]` dá “(Mangen et al., 2013)”, `@delgado2018` sem colchetes põe os autores dentro da frase e `[@ibanez2023, pp. 12–15]` leva o intervalo de páginas para dentro dos parênteses. Com o documento em espanhol, as mesmas chaves tiram os conectivos e as abreviaturas do arquivo de idioma espanhol, porque o motor segue o idioma do documento.

### 2 · As referências vêm de um bloco BibTeX

O último bloco do capítulo é `:::references{format=bibtex}`, com a exportação do Zotero colada dentro e os acentos escritos como `{\'a}`. `:::bibliography`, acima dele, coloca a lista sob o título References; sem ele, o Postext acrescenta a lista no fim. Só aparecem as obras citadas, ordenadas como a APA ordena, e cada citação no texto leva à sua entrada.

### 3 · Um título sem número, referências por extenso

```js
// script.js, linhas 105–106
  headingStyles: [{ id: 'references', numbered: false }],
  crossRefs: { section: t({ en: 'Section {n}', es: 'Sección {n}' }) },
```

O título das referências usa o estilo `references`, que o mantém no sumário mas fora da numeração, então a lista não vira a seção 2.4. `:ref{id="sec-speed"}` imprime “Section 2.2” com o rótulo de `crossRefs` e leva ao título; os números vêm de `startAt=2` no título do capítulo, o mesmo contador que os títulos usam.

### 4 · A abertura fica numa faixa

```js
// script.js, linhas 43–68
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' } } },
  ] },
};
```

A faixa é uma caixa ancorada ao refile, e o texto começa 10 mm abaixo dela. O número é composto em Literata Light de 96 pt, à direita, onde é lido como uma forma e deixa ao título a medida inteira.

```js
// script.js, linhas 16–21
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' } }));
```

## A receita completa

Um único arquivo, composto a partir da pasta da receita com o texto de exemplo e o kit comum das Receitas já incluídos; ele monta a própria página. Para executá-lo, coloque-o em um `<script type="module">` de uma página vazia ou cole-o no painel JS de um pen novo do CodePen (como módulo). Ele importa o postext do esm.sh, então não há nada para instalar nem compilar.

- Pasta da receita: https://github.com/drnachio/postext/tree/main/cookbook/apa-thesis-with-bibtex

### script.js

```js
// ═══ 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.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 = '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 = () => ({ // a factory: the engine caches resolved configs per object
  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,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: {
    fontFamily: TEXT, color: col('ink'), fontWeight: 600,
    levels: [
      // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
      { 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`---
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 ───────────────────────────────────────────────────────
await loadFonts(FONTS, markdown);
const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown);
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 ── 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 v2 ── the same in every recipe · postext.dev/cookbook
// Postext measures with the loaded faces and caches the widths: load every face
// before the first build, from Fontsource, the files the PDF embeds too.

/** 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.
 *  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',
    greek: 'U+0370-03FF',
  };
  const jobs = [];
  let added = 0;
  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 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 = `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` and loads any face the pages use that FONTS missed (a regular
 *  one with a warning), then clears the measurement cache and builds 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. */
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' };
}

/** 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, '-'); }

/** 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 v1 ── 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. */
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 v2 ── the same in every recipe that exports a PDF
/** The Fontsource files the screen used, as TrueType: the nearest weight the
 *  family ships, upright if it has no italic; latin, then what the face's
 *  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 more = kitSubsetsFor(text, meta);
  const files = await Promise.all(['latin', ...more].map(async (subset) => {
    const res = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-${subset}-${w}-${s}.woff2`);
    if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`);
    return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
  }));
  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 ───────────────────────────────────────────────────────────────────────
```

## Variações

### Mude o estilo

O mesmo capítulo em Chicago autor-data tira a vírgula depois do nome e põe o ano depois dos autores na lista.

```diff
-  style: 'apa', // 'chicago-author-date', 'ieee', 'vancouver'… change nothing else
+  style: 'chicago-author-date',
```

### Deixe a lista menor que o texto

Muitas universidades aceitam a lista de referências um corpo abaixo e em espaço simples.

```diff
-    fontSize: em(1), hangingIndent: mm(12.7), entrySpacing: pt(4),
+    fontSize: em(0.9), lineHeight: pt(13), hangingIndent: mm(12.7), entrySpacing: pt(3),
```

## Erros comuns

- **Qualquer objeto headings desativa a quebra de página do H1.** Por padrão, um H1 salta para uma página ímpar (always-odd), mas passar qualquer objeto headings redefine esse padrão, então os capítulos ficam emendados e span: 'page' não faz nada. Declare de novo headings.levels[0].breakBefore: { enabled: true, parity } em toda configuração.
- **Carregue todas as fontes antes do layout.** O motor de layout mede o texto com as fontes que o navegador carregou e guarda as larguras em cache, então uma fonte que chega depois da primeira composição deixa quebras de linha erradas e um PDF que não corresponde mais à tela. Carregue antes todos os pesos e estilos e chame clearMeasurementCache() antes de recompor quando alguma chegar atrasada.
- **Coloque entre aspas cada valor do frontmatter.** O YAML lê title: 1984 como número e uma data como objeto Date, e valores que não são strings saem vazios nos placeholders e deixam o PDF sem título. Coloque cada valor entre aspas: title: "1984".
- **A configuração fica em cache pela identidade: crie um objeto novo.** O motor guarda em cache as configurações resolvidas pela identidade do objeto, então alterar uma configuração no próprio objeto e compor de novo reaproveita o resultado antigo. Crie um objeto novo a cada composição; por isso a configuração de uma receita é uma função, config().

- A APA escreve os títulos de artigos e livros só com a inicial maiúscula, e o citeproc-js imprime os títulos como os dados os trazem. O Zotero guarda a maioria dos títulos com maiúscula em cada palavra, então corrija-os no Zotero ou no BibTeX antes que cheguem à página.

## Créditos

- Receita: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Tipos: Literata (OFL-1.1), Public Sans (OFL-1.1)
- Código: MIT · Conteúdo de exemplo: CC-BY-4.0

## Relacionadas

- [Nº 096 · Um ensaio de humanidades com obras citadas em MLA](https://postext.dev/pt/cookbook/mla-humanities-essay.md): Ensaio literário cujas citações [@key, 48] viram referências MLA 9 de autor e página, com citação em bloco e lista de obras citadas feita de dados CSL-YAML. · Nível 2 (Intermediário) · Artigos e trabalhos acadêmicos
- [Nº 090 · Um artigo chinês citado pela GB/T 7714](https://postext.dev/pt/cookbook/gbt7714-chinese-paper.md): Um artigo de revista chinesa em duas colunas: as citações [@key] saem como [1] e [2–4] sobrescritos, e as referências levam [M], [J], [D] e [EB/OL]. · Nível 2 (Intermediário) · Artigos e trabalhos acadêmicos
- [Nº 095 · Um artigo médico no estilo Vancouver](https://postext.dev/pt/cookbook/medical-article-vancouver.md): Relato de ensaio clínico em A4 e duas colunas: números de citação sobrescritos, referências numeradas no estilo Vancouver e DOIs que abrem como links no PDF. · Nível 2 (Intermediário) · Artigos e trabalhos acadêmicos
