Saltar al contingut principal
Recepta número 31

Receptari · Capítol 5 · Estructura del llibre

Final de tesi: apèndix, glossari i índex

El final d'una tesi en blanc i negre: apèndix amb lletra, glossari a dues columnes, bibliografia APA i índex tret de marques en el text.

En aquesta pàgina

pp. 174–175 · 4–5 de 7

  • Mostra en anglès: encara no hi ha edició en català
  • Format 176 × 250 mm
  • 1 columna
  • Libertinus Serif 11/14,6
  • Libertinus Sans
  • Libertinus Serif Display
  • 7 pàgines
  • Nivell
  • Postext 1.7.0
  • Compost en 63 ms
  • 181 línies de codi

En poques paraules

Les últimes pàgines d'una tesi doctoral, del capítol final a l'índex. Postext crea l'índex a partir de paraules marcades en el text i hi posa els números de pàgina.

Què compondràs

Les set últimes pàgines d'una tesi doctoral sobre la lectura en pantalla i en paper, en format B5 i en blanc i negre. El capítol 6, l'apèndix A, el glossari, la bibliografia i l'índex alfabètic obren sota la mateixa banda negra de 62 mm, amb el títol calat en blanc. El capítol porta el numeral a la banda; l'apèndix, la lletra; i les seccions finals, l'avanttítol BACK MATTER i una nota breu en cursiva. El capítol va a una columna justificada. El glossari i l'índex passen a dues columnes en bandera i la bibliografia torna a una, amb les línies de continuació de cada entrada sagnades. L'índex surt de marques en el text: el motor ordena els termes sota la seva lletra, cerca la pàgina de cada marca i agrupa les seguides en trams com 171–73. Els números en negreta remeten a les definicions del glossari.

Aquesta recepta respon a

  • Com componc una bibliografia o un glossari (sagnat francès, cos més petit)?
  • Com faig un índex analític els números de pàgina del qual canviïn sols quan el text es mou?
  • Com evito que els títols, les negretes i les vinyetes surtin en blau?
  • Com numero els títols (1, 1.1, 1.1.1) i dono a cada nivell un estil diferent?
  • Com poso capçaleres: el títol del llibre a la pàgina esquerra, el del capítol a la dreta i el foli per fora?
  • Com forço un salt de pàgina o de columna, i faig que cada capítol comenci en pàgina senar?

La resposta curta

script.js · línies 27–50al codi complet
// '# Glossary {style="glossary"}' in the Markdown picks a style. Each style starts a page
// of either parity, the appendix a recto (a style that sets no break inherits its level's
// 'odd': gotcha style-inherits-break), stays out of the chapter count (numbered: false, so
// its band has no numeral) and brings its own running heads; the glossary and the index
// set their pages in two columns until the next '#'. config() takes both lists below.
const twoColumns = { layoutType: 'double', gutterWidth: mm(6) };
const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
  parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
const headingStyles = () => [
  backMatter('appendix', { breakBefore: { enabled: true, parity: 'odd' }, // {letter="A"}
    header: appendixHeads, advancedDesign: opener('Appendix', '{attr.letter}') }),
  backMatter('glossary', { layout: twoColumns }),
  backMatter('references'),
  backMatter('index', { layout: twoColumns }),
];
// One paragraph per entry, in :::paragraphs{style="…"}: the turnover lines hang, so the
// first word of every entry stands clear at the left. Ragged, as APA asks of references,
// and so never hyphenated (gotcha: ragged-no-hyphenation). The index has its own settings.
const entries = (id, size, lead, hang, extra) => ({ id, fontSize: pt(size),
  lineHeight: pt(lead), textAlign: 'left', hangingIndent: em(hang), ...extra });
const paragraphStyles = () => [
  entries('term', 9.3, 12.4, 1), // the glossary: a bold term, then its definition
  entries('reference', 9.3, 12.4, 1.5, { spaceBetween: pt(2.4) }),
];

Ingredients

Tipografia
Libertinus Serif, Libertinus Serif Display, Libertinus Sans (SIL OFL 1.1)
Recursos
Cap: totes les imatges es dibuixen en codi

Elaboració

#1 · Un estil de títol per a cada part del final

El codi és la resposta curta de més amunt. # Glossary {style="glossary" note="…"} obre una secció que arriba fins al títol de nivell 1 següent, i les seves pàgines prenen la disposició, les capçaleres i l'obertura de l'estil (estils d'encapçalament), de manera que el glossari i l'índex passen a dues columnes i la bibliografia, l'estil de la qual no fixa disposició, torna a la columna única del document. numbered: false deixa aquests títols fora del recompte de capítols (sense aquesta opció, a la banda del glossari sortiria un 8), i cada estil declara el seu salt de pàgina, perquè el que no el declara hereta el 'odd' del capítol i deixa pàgines parelles en blanc. Les entrades del glossari i de la bibliografia són paràgrafs d'un bloc :::paragraphs{style="…"} en cos 9,3 sobre 12,4 pt, enfront de l'11 sobre 14,6 del text, amb les línies de continuació sagnades 1 em al glossari i 1,5 em a la bibliografia (estils de paràgraf).

#2 · Una sola banda per a totes les obertures

script.js · línies 54–78al codi complet
const SINK = 8; // lines reserved, 41.2 mm: 3.2 mm more than the band, and text on the grid
// Design text sets each baseline 0.8 of its line under the line's top, and a line is 1.2 × the
// size unless lineHeight says otherwise. In mm, a line's part above its baseline and below it:
const PT = 25.4 / 72;
const above = (size, lineHeight = 1.2) => 0.8 * size * lineHeight * PT;
const below = (size, lineHeight = 1.2) => 0.2 * size * lineHeight * PT;
const KICKER = 4.3, TITLE = BAND - TOP - 9.5; // mm under the text block's top: two baselines
// A bottom-aligned box that ends below() under a baseline sets its last line on it. The
// numeral's line is 0.72 of its size: a line taller than its box would hang from its top.
const text = (id, content, family, size, lineHeight, baseline, edge, w, extra) => ({
  kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight,
  color: col('paper'), overflow: 'wrap', align: edge.endsWith('right') ? 'right' : 'left',
  verticalAlign: 'bottom', ...extra, placement: { anchor: { to: 'container', edge },
    size: { width: mm(w), height: mm(baseline + below(size, lineHeight)) } } });
// The mark: '{number}', empty on an unnumbered heading, or the appendix's '{attr.letter}'.
const opener = (label, mark = '{number}') => ({ enabled: true, minHeight: pt(SINK * LEAD),
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('band') }, placement: {
      anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(BAND) } } },
    text('label', label, LABEL, 8, 1.2, KICKER, 'top-left', 80,
      { fontWeight: 700, letterSpacing: pt(1.6), textTransform: 'uppercase' }),
    text('title', '{titleText}', DISPLAY, 34, 1.04, TITLE, 'top-left', 84),
    text('mark', mark, DISPLAY, 118, 0.72, TITLE, 'top-right', 34),
    text('note', '{attr.note}', TEXT, 8.6, 1.3, TITLE, 'top-right', 44, { italic: true }),
  ] } });

La marca és {number}, que queda buit en un títol sense numerar, així que el mateix disseny serveix per al capítol, per a l'apèndix, que passa {attr.letter} com a marca, i per a les seccions finals, que imprimeixen el seu {attr.note} on aniria el numeral. minHeight reserva vuit línies de 14,6 pt, 41,2 mm des del capdamunt de la caixa de text, que deixen 3,2 mm lliures sota la banda i mantenen el text a la seva retícula. En els textos de disseny, la línia de base cau a 0,8 de l'alçada de línia, de manera que una caixa alineada a baix el peu de la qual queda a below() d'una línia de base hi assenta la seva última línia. Així, el títol, el numeral de 118 pt i l'última línia de la nota es recolzen a la línia de base del títol, a 52,5 mm del tall. La línia del numeral fa 0,72 vegades el seu cos perquè a la 1.4.1 una línia més alta que la seva caixa ignora verticalAlign: 'bottom'.

#3 · La lletra de l'apèndix es posa a mà

script.js · línies 108–112al codi complet
// In 1.4.1 a heading style cannot change the numbering: the appendix is unnumbered, and its
// letter feeds the band (see answer), the running head and a table type that counts A.1.
const appendixHeads = heads('Appendix {attr.letter}. {chapterTitle}');
const appendixTables = { ...defaultResourceTypes(LANG).find((type) => type.id === 'table'),
  id: 'table-a', numberingTemplate: 'A.{n}' }; // a copy of 'table'

A postext 1.4.1 un estil de títol no pot canviar la numeració del seu nivell, així que l'apèndix va sense numerar i la seva lletra s'escriu com a atribut: # Interview guide {style="appendix" letter="A"}. La banda i la capçalera de l'apèndix la prenen d'aquí, i un tipus de recurs propi numera les seves taules A.1, A.2, etc.; amb el tipus per defecte, la taula seria la 6.2, perquè un títol sense numerar deixa el comptador de capítols en 6. Els títols del capítol es numeren amb les plantilles de cada nivell: '{1}.{2}' imprimeix 6.1, i continuation.headings.h1: 5 fa d'aquest el sisè capítol.

#4 · Les capçaleres, cap al marge exterior

script.js · línies 82–104al codi complet
const HEAD = 17.5, GAP = 9; // mm: the heads' baseline under the trim; the folio to the words
// Each text is placed by its top, above() over HEAD: the folio and the capitals share a baseline.
const head = (id, content, parity, edge, x, size = 7.5, extra) => ({ kind: 'text', id, content,
  parity, pages: 'body', fontFamily: LABEL, fontSize: pt(size), fontWeight: 700,
  letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('ink'), ...extra, placement: {
    anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD - above(size)) } } });
const folio = { fontFamily: TEXT, fontWeight: 400, letterSpacing: pt(0) };
const heads = (recto) => ({ elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, 9.5, folio),
  head('verso', '{title}', 'even', 'top-left', OUTER + GAP),
  head('recto', recto, 'odd', 'top-right', -(OUTER + GAP)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, 9.5, folio),
  // The header's container spans the text block, so one rule serves both pages.
  { kind: 'rule', id: 'hairline', pages: 'body', direction: 'horizontal', thickness: pt(0.5),
    color: col('rule'), placement: { anchor: { to: 'container', edge: 'top-left' },
      offset: { y: mm(HEAD + 2) }, size: { width: 'fill' } } },
] });
const chapterHeads = heads('Chapter {chapterNumber}. {chapterTitle}');
const sectionHeads = heads('{chapterTitle}'); // 'Glossary', 'References', 'Index'
// Openers drop the folio to the foot, centred under the text block, its baseline 12 mm below.
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
  pages: 'opener', ...folio, fontSize: pt(9.5), color: col('ink'), align: 'center',
  placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(12 - above(9.5)) } } }] };

Els quatre textos s'ancoren a la pàgina i es filtren per parity, cosa que manté el foli a la vora exterior de les dues pàgines; cadascun es col·loca per la vora superior, a above() per sobre d'una línia de base situada a 17,5 mm del tall, així que el foli de 9,5 pt i les majúscules de 7,5 pt queden a la mateixa línia. La parella porta el títol de la tesi ({title}, del frontmatter) i la senar el de la secció ({chapterTitle}), com INDEX a la pàgina 177. pages: 'body' les deixa fora de les obertures, on el foli passa al peu. La senar del capítol diria Chapter 6. Conclusion i la de l'apèndix Appendix A. Interview guide, però en aquesta mostra només l'índex continua en una pàgina senar després de la seva obertura.

#5 · Marca els termes on el text en parla

script.js · línies 116–122al codi complet
// Glossary definitions carry 'main' (bold numbers); runs of pages join as 171–72 (Chicago).
// The heads stand on the entries' 11.6 pt pitch, 7.5 pt of space above them: 19 pt from the
// last entry of a letter to the next letter's baseline, 11.6 pt from a letter to its first entry.
const index = { fontFamily: TEXT, fontSize: pt(9), lineHeight: pt(11.6), color: col('ink'),
  indent: em(1), turnoverIndent: em(2), rangeFormat: 'chicago',
  groups: { fontFamily: DISPLAY, fontSize: pt(13), fontWeight: 400, color: col('ink'),
    marginTop: pt(7.5) } };

:index[tablet] imprimeix la paraula i la registra tal com és; Rayner:index{term="Rayner, Keith"} no imprimeix res i registra la pàgina de Rayner sota el nom complet; term="interviews!timing of" crea una subentrada. Els termes del glossari porten main, així que la seva pàgina surt en negreta, com avisa la nota de la banda de l'índex. :::index, sota # Index {style="index"}, imprimeix les entrades a les dues columnes de l'estil, i buildDocument torna a compondre el document fins que els números de pàgina deixen de moure's. Les entrades van en cos 9 sobre 11,6 pt, amb les línies de continuació sagnades 2 em i les subentrades 1 em; les lletres, en 13 pt amb la font dels titulars. Les marques no funcionen a les cel·les de taula, així que les files de la taula 6.1 no hi afegeixen pàgines: look-backs remet al paràgraf que les comenta, no a la taula.

#6 · Tots els valors per defecte, en negre

script.js · línies 15–19al codi complet
const palette = { ink: '#000000', band: '#000000', rule: '#000000', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Bold, italic and list markers default to 'main-color': pointed at the ink, they print black.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.ink })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));

La negreta, la cursiva i les vinyetes i els números de les llistes segueixen per defecte l'entrada main-color de la paleta, així que, si li dones el color de la tinta, surten en negre i no en blau (paleta de colors). A la 1.4.1 la paleta no arriba a bodyText.referenceColor, i per això la configuració el repeteix: sense aquesta línia, Table 6.1 surt al text en blau #295AA3. referenceBold: false posa la referència en rodona, com les cites autor-any que l'envolten.

La recepta completa

Sandbox
// ═══ Postext Cookbook · Nº 031 · Thesis back matter: appendix, glossary and index ═══
// https://postext.dev/en/cookbook/thesis-back-matter
// Code: MIT · Text: original (CC BY 4.0) · Pictures: none
// Fonts: Libertinus Serif, Serif Display and Sans (SIL OFL 1.1) · Needs postext ≥ 1.7.0
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, defaultResourceTypes,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'en'; // @lang: the language of the sample document ('en')
const RECIPE = 'thesis-back-matter';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: one ink; every colour is black or white, each under its own name
const palette = { ink: '#000000', band: '#000000', rule: '#000000', paper: '#ffffff' };
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
// Bold, italic and list markers default to 'main-color': pointed at the ink, they print black.
const colorPalette = Object.entries({ ...palette, 'main-color': palette.ink })
  .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
// #endregion
const TEXT = 'Libertinus Serif', DISPLAY = 'Libertinus Serif Display', LABEL = 'Libertinus Sans';
const TOP = 24, INNER = 25, OUTER = 31; // mm: a 120 mm measure, about 70 characters at 11 pt
const LEAD = 14.6; // pt: the body's leading, the grid every page is set on
const BAND = 62; // mm from the trim's top: the black band at the head of every opener

// #region answer: back matter as unnumbered heading styles, entries in hanging indents
// '# Glossary {style="glossary"}' in the Markdown picks a style. Each style starts a page
// of either parity, the appendix a recto (a style that sets no break inherits its level's
// 'odd': gotcha style-inherits-break), stays out of the chapter count (numbered: false, so
// its band has no numeral) and brings its own running heads; the glossary and the index
// set their pages in two columns until the next '#'. config() takes both lists below.
const twoColumns = { layoutType: 'double', gutterWidth: mm(6) };
const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
  parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
const headingStyles = () => [
  backMatter('appendix', { breakBefore: { enabled: true, parity: 'odd' }, // {letter="A"}
    header: appendixHeads, advancedDesign: opener('Appendix', '{attr.letter}') }),
  backMatter('glossary', { layout: twoColumns }),
  backMatter('references'),
  backMatter('index', { layout: twoColumns }),
];
// One paragraph per entry, in :::paragraphs{style="…"}: the turnover lines hang, so the
// first word of every entry stands clear at the left. Ragged, as APA asks of references,
// and so never hyphenated (gotcha: ragged-no-hyphenation). The index has its own settings.
const entries = (id, size, lead, hang, extra) => ({ id, fontSize: pt(size),
  lineHeight: pt(lead), textAlign: 'left', hangingIndent: em(hang), ...extra });
const paragraphStyles = () => [
  entries('term', 9.3, 12.4, 1), // the glossary: a bold term, then its definition
  entries('reference', 9.3, 12.4, 1.5, { spaceBetween: pt(2.4) }),
];
// #endregion

// #region opener: a black band across the head of the page, the title reversed out of it
const SINK = 8; // lines reserved, 41.2 mm: 3.2 mm more than the band, and text on the grid
// Design text sets each baseline 0.8 of its line under the line's top, and a line is 1.2 × the
// size unless lineHeight says otherwise. In mm, a line's part above its baseline and below it:
const PT = 25.4 / 72;
const above = (size, lineHeight = 1.2) => 0.8 * size * lineHeight * PT;
const below = (size, lineHeight = 1.2) => 0.2 * size * lineHeight * PT;
const KICKER = 4.3, TITLE = BAND - TOP - 9.5; // mm under the text block's top: two baselines
// A bottom-aligned box that ends below() under a baseline sets its last line on it. The
// numeral's line is 0.72 of its size: a line taller than its box would hang from its top.
const text = (id, content, family, size, lineHeight, baseline, edge, w, extra) => ({
  kind: 'text', id, content, fontFamily: family, fontSize: pt(size), lineHeight,
  color: col('paper'), overflow: 'wrap', align: edge.endsWith('right') ? 'right' : 'left',
  verticalAlign: 'bottom', ...extra, placement: { anchor: { to: 'container', edge },
    size: { width: mm(w), height: mm(baseline + below(size, lineHeight)) } } });
// The mark: '{number}', empty on an unnumbered heading, or the appendix's '{attr.letter}'.
const opener = (label, mark = '{number}') => ({ enabled: true, minHeight: pt(SINK * LEAD),
  slot: { elements: [
    { kind: 'box', id: 'band', style: { backgroundColor: col('band') }, placement: {
      anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(BAND) } } },
    text('label', label, LABEL, 8, 1.2, KICKER, 'top-left', 80,
      { fontWeight: 700, letterSpacing: pt(1.6), textTransform: 'uppercase' }),
    text('title', '{titleText}', DISPLAY, 34, 1.04, TITLE, 'top-left', 84),
    text('mark', mark, DISPLAY, 118, 0.72, TITLE, 'top-right', 34),
    text('note', '{attr.note}', TEXT, 8.6, 1.3, TITLE, 'top-right', 44, { italic: true }),
  ] } });
// #endregion

// #region running-heads: the thesis on the verso, the section on the recto, a hairline under
const HEAD = 17.5, GAP = 9; // mm: the heads' baseline under the trim; the folio to the words
// Each text is placed by its top, above() over HEAD: the folio and the capitals share a baseline.
const head = (id, content, parity, edge, x, size = 7.5, extra) => ({ kind: 'text', id, content,
  parity, pages: 'body', fontFamily: LABEL, fontSize: pt(size), fontWeight: 700,
  letterSpacing: pt(1.3), textTransform: 'uppercase', color: col('ink'), ...extra, placement: {
    anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(HEAD - above(size)) } } });
const folio = { fontFamily: TEXT, fontWeight: 400, letterSpacing: pt(0) };
const heads = (recto) => ({ elements: [
  head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, 9.5, folio),
  head('verso', '{title}', 'even', 'top-left', OUTER + GAP),
  head('recto', recto, 'odd', 'top-right', -(OUTER + GAP)),
  head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, 9.5, folio),
  // The header's container spans the text block, so one rule serves both pages.
  { kind: 'rule', id: 'hairline', pages: 'body', direction: 'horizontal', thickness: pt(0.5),
    color: col('rule'), placement: { anchor: { to: 'container', edge: 'top-left' },
      offset: { y: mm(HEAD + 2) }, size: { width: 'fill' } } },
] });
const chapterHeads = heads('Chapter {chapterNumber}. {chapterTitle}');
const sectionHeads = heads('{chapterTitle}'); // 'Glossary', 'References', 'Index'
// Openers drop the folio to the foot, centred under the text block, its baseline 12 mm below.
const footer = { elements: [{ kind: 'text', id: 'drop-folio', content: '{pageNumber}',
  pages: 'opener', ...folio, fontSize: pt(9.5), color: col('ink'), align: 'center',
  placement: { anchor: { to: 'container', edge: 'top' }, offset: { y: mm(12 - above(9.5)) } } }] };
// #endregion

// #region appendix: the letter comes from the heading, '# Interview guide {letter="A"}'
// In 1.4.1 a heading style cannot change the numbering: the appendix is unnumbered, and its
// letter feeds the band (see answer), the running head and a table type that counts A.1.
const appendixHeads = heads('Appendix {attr.letter}. {chapterTitle}');
const appendixTables = { ...defaultResourceTypes(LANG).find((type) => type.id === 'table'),
  id: 'table-a', numberingTemplate: 'A.{n}' }; // a copy of 'table'
// #endregion

// #region index: the pages of the :index marks, sorted under letters in the display face
// Glossary definitions carry 'main' (bold numbers); runs of pages join as 171–72 (Chicago).
// The heads stand on the entries' 11.6 pt pitch, 7.5 pt of space above them: 19 pt from the
// last entry of a letter to the next letter's baseline, 11.6 pt from a letter to its first entry.
const index = { fontFamily: TEXT, fontSize: pt(9), lineHeight: pt(11.6), color: col('ink'),
  indent: em(1), turnoverIndent: em(2), rangeFormat: 'chicago',
  groups: { fontFamily: DISPLAY, fontSize: pt(13), fontWeight: 400, color: col('ink'),
    marginTop: pt(7.5) } };
// #endregion

const config = () => ({ // a factory, never a shared object (gotcha: config-cache-identity)
  colorPalette, header: chapterHeads, footer, layout: { layoutType: 'single' },
  page: { sizePreset: 'custom', width: mm(176), height: mm(250), dpi: 150, // B5
    margins: { top: mm(TOP), bottom: mm(24), left: mm(INNER), right: mm(OUTER), mirror: true } },
  bodyText: { fontFamily: TEXT, fontSize: pt(11), lineHeight: pt(LEAD), color: col('ink'),
    // 'Table 6.1' in roman and in ink, outside the palette's reach (gotcha: palette-skips-designs)
    referenceColor: col('ink'), referenceBold: false,
    firstLineIndent: mm(4.5), indentAfterHeading: false, minWordSpacing: 0.8, maxWordSpacing: 1.8 },
  // Exact heading margins (snapToGrid: false), no lines added above them; the chapter's heads
  // measure whole grid lines.
  headings: { fontFamily: DISPLAY, fontWeight: 400, color: col('ink'), snapToGrid: false,
    balancing: { maxLinesPerHeading: 0 }, levels: [
      // The H1 break restated (gotcha: headings-drop-h1-break). span: 'page' (the styles inherit
      // it) sets the band above the columns: inside a column, its top would be clipped.
      { level: 1, numberingTemplate: '{1}', span: 'page', marginBottom: pt(0),
        advancedDesign: opener('Chapter'), breakBefore: { enabled: true, parity: 'odd' } },
      { level: 2, numberingTemplate: '{1}.{2}', fontSize: pt(14), lineHeight: pt(LEAD),
        marginTop: pt(LEAD * 1.5), marginBottom: pt(LEAD / 2) }, // three lines in all
    ] },
  headingStyles: headingStyles(), paragraphStyles: paragraphStyles(), index,
  orderedLists: { marginTop: pt(LEAD / 2), marginBottom: pt(LEAD / 2) },
  unorderedLists: { bulletChar: '–' },
  resourceTypes: [...defaultResourceTypes(LANG), appendixTables], // tables 6.1… and A.1…
  // Captions in the text face, as APA sets a table's number and title.
  captionStyle: { fontSize: pt(9), position: 'above', gap: pt(4), note: { fontSize: pt(8) } },
  // Rules only and a bold header: filled header cells show seams between the columns.
  tableStyle: { rules: 'horizontal', borderColor: col('rule'), borderWidth: pt(0.5),
    headerBackgroundEnabled: false, headerFontSize: pt(9.5),
    bodyFontSize: pt(9.5), cellPadding: mm(1) },
  calloutStyles: [{ id: 'colophon', span: 'page', marginTop: pt(LEAD), backgroundEnabled: false,
    stripe: { enabled: true, side: 'top', width: pt(0.5), color: col('rule') },
    padding: { top: mm(2.5), right: mm(0), bottom: mm(0), left: mm(0) },
    body: { fontSize: pt(8), lineHeight: pt(10.5), firstLineIndent: pt(0), textAlign: 'left' } }],
});

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
Mostra en Markdown · 126 línies · content.en.mdtitle: "Reading on Screens and Paper" subtitle: "A Mixed-Methods Study of Comprehension, Confidence and Navigation" author: "Ines Varley" --- # Conclusion This thesis set out to test whether it matters if a long text:index{term="texts, length of"} is read on paper or on a screen. Chapters 3 to 5 reported a within-subjects:index{term="within-subjects design"} experiment with forty-eight :index[undergraduates] and :index[interviews] with sixteen of them, combined in the :index[convergent design] described by Creswell:index{term="Creswell, John W."} and Plano Clark:index{term="Plano Clark, Vicki L."} (2018). This chapter brings the two strands together and sets out what they mean for :index[teaching] and for the :index[design of reading software]. ## What the study found :ref{id="findings" style="full"} summarises the results.:index{term="comprehension"} On :index[literal]{term="comprehension!literal"} questions, answerable from a single sentence, the medium made no difference. On :index[inferential]{term="comprehension!inferential"} questions, which required connecting ideas across paragraphs, paper readers scored higher. The difference points the same way as the :index[meta-analyses] of Delgado:index{term="Delgado, Pablo"} et al. (2018) and Clinton:index{term="Clinton, Virginia"} (2019), which found the :index[paper advantage] in :index[expository]{term="expository text"} rather than :index[narrative]{term="narrative text"} texts. Calibration:index{term="calibration"} showed the larger difference. Screen readers:index{term="calibration!on screen"} predicted:index{term="confidence!judgements of"} higher scores than paper readers and obtained lower ones, so the gap between :index[confidence] and :index[accuracy]:index{term="calibration!bias in"} was nearly three times as wide. The result repeats the :index[overconfidence] that Ackerman:index{term="Ackerman, Rakefet"} and Goldsmith:index{term="Goldsmith, Morris"} (2011) found in students who read on screen and set their own study time.:index{term="self-regulated study"} Such readers stop once they judge a text understood, so overconfidence cuts their study short. Paper readers also turned back:index{term="look-backs"} almost twice as often as screen readers scrolled back:index{term="scrolling"}, most often just before an inferential question.:index{term="comprehension!inferential"} In the interviews:index{term="interviews"}, eleven of the sixteen :index[participants] remembered:index{term="memory"} where on a page:index{term="spatial memory"} an idea had been (“top left, next to the diagram”), and three gave up looking for a passage on the :index[tablet] because “it could have been anywhere”. Liu:index{term="Liu, Ziming"} (2005) described a drift towards :index[browsing] and :index[keyword spotting] on screen; these readers went through the whole text but had fewer :index[landmarks] to return to. ## Implications for teaching and design For short texts and factual questions, screens serve as well as paper.:index{term="teaching"} For long expository:index{term="expository text"} texts that students must understand:index{term="comprehension"} rather than search, paper remains the safer choice.:index{term="paper advantage"} Where it is not available, students should test their understanding instead of trusting their sense of it: in the :index[pilot sessions], a short :index[self-test] after reading halved the overconfidence:index{term="overconfidence"} on screen.:index{term="calibration!on screen"} Readers also used the fixed position of text on a page as a map,:index{term="landmarks"} one of the uses of paper that Sellen:index{term="Sellen, Abigail J."} and Harper:index{term="Harper, Richard H. R."} (2002) observed in offices.:index{term="offices, paper in"} Reading applications:index{term="design of reading software"} that keep a stable page and show the reader’s place in the whole text may restore some of that map. ## Limitations and further work The participants:index{term="participants"} were students at one university:index{term="university, single"} who read English fluently,:index{term="limitations"} and the medium matters more for some readers, texts and tasks than it does for others (Singer:index{term="Singer, Lauren M."} & Alexander:index{term="Alexander, Patricia A."}, 2017). The texts were expository and about 1,800 words long,:index{term="texts, length of"} and the screen condition used a single tablet.:index{term="tablet"} A :index[replication] with a larger sample, several devices and the eye-movement:index{term="eye movements"} recording reviewed by Rayner:index{term="Rayner, Keith"} (1998) would show where on the page the two media part company. Huey:index{term="Huey, Edmund Burke"} (1908) thought that a complete analysis of what we do when we read would be almost the acme of a psychologist’s achievements. The experiments reported here add a small part to that analysis; the replication proposed above could measure how far readers rely on the position of a passage on the page when they look back. # Interview guide {style="appendix" letter="A"} The interviews:index{term="interviews"} took place within a week of each participant’s:index{term="participants"} second session. They were audio-recorded,:index{term="interviews!recording of"} transcribed:index{term="transcription"} in full and analysed thematically:index{term="thematic analysis"} following Braun:index{term="Braun, Virginia"} and Clarke:index{term="Clarke, Victoria"} (2006); :ref{id="session-plan" style="full"} gives their timing:index{term="interviews!timing of"}: a free recall:index{term="recall"} of the two study texts, the questions below and a short debriefing.:index{term="debriefing"} The questions were asked in this order,:index{term="interviews!questions asked"} and a prompt:index{term="interviews!prompts in"} only when the participant had not already covered its point. 1. Tell me about the last long text:index{term="texts, length of"} you read for a course.:index{term="courses, reading for"} - Where did you read it, and on paper or on a screen? 2. Which of the texts in this study do you remember:index{term="memory"} best, and why? 3. When you wanted to check an earlier passage, what did you do?:index{term="look-backs"} - How did you know where to look? 4. How sure were you of your answers?:index{term="confidence"} What made you more or less sure? 5. Did reading on the tablet:index{term="tablet"} feel different from reading on paper? 6. Some students say they read more carefully on paper. Do you? 7. What would the ideal way to read a long text for study be like?:index{term="design of reading software"} # Glossary {style="glossary" note="Words in italics are defined under entries of their own."} :::paragraphs{style="term"} **calibration**:index{term="calibration" main} The agreement between a reader’s confidence in having understood a text and the accuracy of that understanding, measured here as the difference between predicted and actual scores. **comprehension, inferential**:index{term="comprehension!inferential" main} Understanding that requires the reader to connect information from different parts of a text or to add knowledge the text does not state. **comprehension, literal**:index{term="comprehension!literal" main} Understanding of what a single sentence or passage states directly. **confidence judgement**:index{term="confidence!judgements of" main} A reader’s estimate, made after reading and before seeing the questions, of how many answers will be correct. **convergent design**:index{term="convergent design" main} A mixed-methods design in which quantitative and qualitative data are collected in the same period, analysed separately and then compared. **expository text**:index{term="expository text" main} A text written to explain or inform, such as a textbook chapter or a report, as opposed to a narrative text. **fixation**:index{term="fixation" main} A pause of the eyes, typically about a quarter of a second, during which the reader takes in text; fixations alternate with *saccades*. **look-back**:index{term="look-backs" main} Any return to an earlier part of a text during reading: turning back a page, scrolling up or following a link to a previous section. **metacomprehension**:index{term="metacomprehension" main} A reader’s knowledge and monitoring of their own understanding of a text; *calibration* is one of its measures. **navigation**:index{term="navigation" main} The movements a reader makes through a text as a whole, as distinct from the movements of the eyes along a line. **overconfidence**:index{term="overconfidence" main} Positive *calibration* bias: predicting a higher score than the one actually obtained. **saccade**:index{term="saccade" main} A rapid movement of the eyes from one *fixation* to the next, during which little or no text is taken in. **screen inferiority effect**:index{term="screen inferiority effect" main} The finding that comprehension of the same text is lower on screen than on paper, most consistently for *expository texts* read under time pressure. **self-regulated study**:index{term="self-regulated study" main} Reading in which the reader, not the experimenter, decides how long to spend on a text. **spatial memory for text**:index{term="spatial memory" main} Memory of where on a page or in a document a piece of information appeared, used as a cue for *look-backs*. **thematic analysis**:index{term="thematic analysis" main} A method for identifying, analysing and reporting patterns of meaning across qualitative data such as interview transcripts. **within-subjects design**:index{term="within-subjects design" main} An experimental design in which every participant takes part in every condition, here reading on both paper and screen. ::: # References {style="references" note="Every work cited in the thesis, set in APA style (7th edition)."} :::paragraphs{style="reference"} Ackerman, R., & Goldsmith, M. (2011). Metacognitive regulation of text learning: On screen versus on paper. *Journal of Experimental Psychology: Applied, 17*(1), 18–32. Baron, N. S. (2015). *Words onscreen: The fate of reading in a digital world.* Oxford University Press. Braun, V., & Clarke, V. (2006). Using thematic analysis in psychology. *Qualitative Research in Psychology, 3*(2), 77–101. Clinton, V. (2019). Reading from paper compared to screens: A systematic review and meta-analysis. *Journal of Research in Reading, 42*(2), 288–325. Creswell, J. W., & Plano Clark, V. L. (2018). *Designing and conducting mixed methods research* (3rd ed.). SAGE. Delgado, P., Vargas, C., Ackerman, R., & Salmerón, L. (2018). Don’t throw away your printed books: A meta-analysis on the effects of reading media on reading comprehension. *Educational Research Review, 25*, 23–38. Dillon, A. (1992). Reading from paper versus screens: A critical review of the empirical literature. *Ergonomics, 35*(10), 1297–1326. Huey, E. B. (1908). *The psychology and pedagogy of reading.* Macmillan. Liu, Z. (2005). Reading behavior in the digital environment: Changes in reading behavior over the past ten years. *Journal of Documentation, 61*(6), 700–712. Mangen, A., Walgermo, B. R., & Brønnick, K. (2013). Reading linear texts on paper versus computer screen: Effects on reading comprehension. *International Journal of Educational Research, 58*, 61–68. Noyes, J. M., & Garland, K. J. (2008). Computer- vs. paper-based tasks: Are they equivalent? *Ergonomics, 51*(9), 1352–1375. Paterson, D. G., & Tinker, M. A. (1940). *How to make type readable.* Harper & Brothers. Rayner, K. (1998). Eye movements in reading and information processing: 20 years of research. *Psychological Bulletin, 124*(3), 372–422. Sellen, A. J., & Harper, R. H. R. (2002). *The myth of the paperless office.* MIT Press. Singer, L. M., & Alexander, P. A. (2017). Reading on paper and digitally: What the past decades of empirical research reveal. *Review of Educational Research, 87*(6), 1007–1041. Tinker, M. A. (1963). *Legibility of print.* Iowa State University Press. Wolf, M. (2018). *Reader, come home: The reading brain in a digital world.* Harper. ::: # Index {style="index" note="Bold numbers refer to the definitions in the glossary."} :::index :::callout{type="colophon"} Set in Libertinus Serif, Libertinus Serif Display and Libertinus Sans (SIL Open Font License). Text: original, CC BY 4.0. The thesis, its author, its participants and its results are fictional; the works in the references are real. :::
`; // content.<lang>.md, inlined by the Cookbook // A table from rows of 'cell|cell|cell'; aligns has a letter a column, l or r. const table = (id, typeId, caption, note, widths, aligns, rows) => ({ id, typeId, kind: 'table', caption, note, createdAt: 0, updatedAt: 0, table: { model: { headerRowCount: 1, columnWidths: widths, rows: rows.map((row, r) => row.split('|').map((content, c) => ({ content, isHeader: r === 0, align: aligns[c] === 'r' ? 'right' : 'left' }))) } } }); const resources = [ table('findings', 'table', 'Main results by medium', 'Means for 48 participants. Bias is the predicted minus the actual score.', [5, 1.4, 1.4], 'lrr', ['Measure|Paper|Screen', 'Literal comprehension (of 10)|7.8|7.7', 'Inferential comprehension (of 10)|6.4|5.6', 'Predicted score (%)|75|78', 'Actual score (%)|71|66.5', 'Calibration bias (points)|+4.0|+11.5', 'Look-backs per text|5.8|3.1']), table('session-plan', 'table-a', 'Timing of an interview session', undefined, [1, 6, 1.4], 'llr', ['Part|Content|Minutes', '1|Welcome, consent and a check of the recorder|3', '2|Free recall of the two study texts|5', '3|Questions 1–4: reading habits, look-backs and confidence|12', '4|Questions 5–7: the two media and an ideal design|12', '5|Debrief|3']), ]; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { 'Libertinus Serif': ['400', '400i', '700'], 'Libertinus Serif Display': ['400'], 'Libertinus Sans': ['700'] }; // every face the pages use, loaded first (gotcha: fonts-first) // ─── 4 · Build & show ─────────────────────────────────────────────────────── // The thesis's sixth and last chapter opens on page 171, a recto. const continuation = { pageIndexOffset: 170, pageNumbering: { startAt: 171 }, headings: { h1: 5 } }; await loadFonts(FONTS, markdown); // The index is laid out again until its page numbers settle, inside this one call. const doc = await buildWithFonts( () => buildDocument({ markdown, resources, continuation }, config()), markdown); showPages(doc, { title: 'Reading on Screens and Paper: the back matter' }); 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

#Obre cada secció en pàgina senar

Les tesis que s'enquadernen per a una biblioteca solen obrir cada secció en pàgina senar. El glossari, la bibliografia i l'índex passen llavors a les pàgines 175, 177 i 179, cadascuna després d'una parella en blanc, i els números en negreta de l'índex apunten a la 175.

-const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
-  parity: 'any' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });
+const backMatter = (id, extra) => ({ id, numbered: false, breakBefore: { enabled: true,
+  parity: 'odd' }, advancedDesign: opener('Back matter'), header: sectionHeads, ...extra });

#Compon també el principi de la tesi

Els preliminars, numerats en romans abans de la pàgina 1, són a Preliminars amb folis romans i després la pàgina 1.

Errors freqüents

Error freqüent

Un estil de títol hereta el salt de pàgina del seu nivell

Una entrada de headingStyles pren del seu nivell de títol tot allò que no fixa, també breakBefore. Un índex o un colofó amb estil sobre un H1 després d'un :::pagebreak hereta la paritat 'odd' i cau darrere d'una pàgina en blanc. Dona a aquest estil breakBefore: { enabled: false }. Estils de títol →

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

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

En el text en bandera no hi ha partició de mots

La partició de mots només s'aplica al text justificat; el text en bandera talla entre paraules, així que una columna estreta en bandera queda molt desigual. Justifica el passatge o eixampla la mesura. Partició de mots i llengua del document →

Error freqüent

El PDF demana tots els gruixos i estils de cada família

renderToPdf demana al proveïdor de fonts la negreta, la cursiva i la negreta cursiva de cada família que un bloc podria fer servir, encara que no s'imprimeixi mai, i un sol rebuig atura l'exportació. El proveïdor s'ha d'ajustar al gruix més proper que tingui la família i tornar a la rodona quan no hi hagi cursiva. Fonts incrustades al PDF →

Error freqüent

La pàgina 1 és senar: planifica amb números físics

La pàgina 1 queda a la dreta i la 2 és la primera pàgina parella, així que planifica els plecs amb números de pàgina físics: una obertura en pàgina parella queda davant de la senar que la segueix. Salts de pàgina i de columna →

Error freqüent

Posa entre cometes cada valor del frontmatter

YAML llegeix title: 1984 com un nombre i una data com un objecte Date, i els valors que no són cadenes s'imprimeixen buits als marcadors i deixen el PDF sense títol. Posa entre cometes cada valor: title: "1984". Metadades 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 →

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 →

Crèdits

Text
Text original, CC BY 4.0
Fonts
Libertinus Serif (SIL OFL 1.1) · Libertinus Serif Display (SIL OFL 1.1) · Libertinus Sans (SIL OFL 1.1)
SandboxPDF