Vai al contenuto principale
Ricetta numero 31

Ricettario · Capitolo 5 · Struttura del libro

Apparati finali di una tesi: appendice, glossario e indice

Le ultime pagine di una tesi in bianco e nero: appendice con lettera, glossario su due colonne, bibliografia APA e un indice costruito dai segni nel testo.

pp. 174–175 · 4–5 di 7

  • Esempio in inglese: non c’è ancora un’edizione italiana
  • Formato 176 × 250 mm
  • 1 colonna
  • Libertinus Serif 11/14,6
  • Libertinus Sans
  • Libertinus Serif Display
  • 7 pagine
  • Livello
  • Postext 1.31.0
  • Impaginato in 115 ms
  • 183 righe di codice

In breve

Le ultime pagine di una tesi di dottorato, dall’ultimo capitolo all’indice. Postext costruisce l’indice dalle parole segnate nel testo e ne inserisce i numeri di pagina.

Che cosa comporrai

Le ultime sette pagine di una tesi di dottorato sulla lettura da schermo e su carta, composta su una pagina B5 in bianco e nero. Il capitolo 6, l’appendice A, il glossario, la bibliografia e l’indice si aprono ciascuno sotto la stessa fascia nera, alta 62 mm, con il titolo in negativo. Il capitolo mostra nella fascia il suo numero e l’appendice la sua lettera; le sezioni degli apparati finali portano l’occhiello BACK MATTER e una breve nota in corsivo. Il capitolo scorre su una colonna giustificata. Il glossario e l’indice passano a due colonne in bandiera e la bibliografia torna a una, con le righe successive di ogni voce rientrate. L’indice viene dai segni nel testo: il motore ordina i termini sotto le loro lettere, trova la pagina di ogni segno e unisce le pagine consecutive in intervalli come 171–73. I numeri in grassetto rimandano alle definizioni del glossario.

Questa ricetta risponde a

  • Come compongo una bibliografia o un glossario (rientro sporgente, corpo minore)?
  • Come costruisco un indice analitico i cui numeri di pagina cambiano da soli quando il testo si sposta?
  • Come evito che titoli, parole in grassetto e punti elenco escano in blu?
  • Come numero i titoli (1, 1.1, 1.1.1) e do a ogni livello uno stile diverso?
  • Come imposto le testatine: titolo del libro sulla pagina sinistra, titolo del capitolo sulla destra, numero di pagina all’esterno?
  • Come forzo un’interruzione di pagina o di colonna, e faccio cominciare ogni capitolo su una pagina destra?

La risposta breve

script.js · righe 27–51nel codice completo
// '# 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 '#'. The appendix counts, in letters: its
// template prints the level's counter as A, B… 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', { numbered: true, numberingTemplate: '{1:A}', header: appendixHeads,
    breakBefore: { enabled: true, parity: 'odd' }, advancedDesign: opener('Appendix') }),
  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,
// with every word whole (hyphenation.ragged is off). 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) }),
];

Ingredienti

Tipo
Libertinus Serif, Libertinus Serif Display, Libertinus Sans (SIL OFL 1.1)
Risorse
Nessuna: ogni immagine è disegnata nel codice

Preparazione

#1 · Dai a ogni parte degli apparati finali uno stile di titolo

Il codice è nella risposta breve qui sopra. # Glossary {style="glossary" note="…"} apre una sezione che arriva fino al titolo di livello 1 successivo, e le sue pagine prendono l’impaginazione, le testatine e l’apertura dello stile (stili dei titoli), così il glossario e l’indice passano a due colonne mentre la bibliografia, il cui stile non imposta un’impaginazione, torna all’unica colonna del documento. numbered: false tiene questi titoli fuori dal conteggio dei capitoli (toglilo e la fascia del glossario stampa 2), e ogni stile nomina la propria interruzione di pagina, perché uno che non ne nomina nessuna eredita il 'odd' del capitolo e lascia versi bianchi. Le voci del glossario e della bibliografia sono paragrafi di un blocco :::paragraphs{style="…"} a 9,3 pt su 12,4 pt, contro gli 11 su 14,6 del testo, con righe successive che sporgono di 1 em nel glossario e di 1,5 em nella bibliografia (stili di paragrafo).

#2 · Disegna una sola fascia per ogni apertura

script.js · righe 55–79nel codice completo
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, so the line fits inside its box.
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 is '{number}': the chapter's 6, the appendix's A, nothing on an unnumbered heading.
const opener = (label) => ({ 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', '{number}', DISPLAY, 118, 0.72, TITLE, 'top-right', 34),
    text('note', '{attr.note}', TEXT, 8.6, 1.3, TITLE, 'top-right', 44, { italic: true }),
  ] } });

Il segno è {number}: 6 sul capitolo, A sull’appendice e niente su un titolo non numerato. Un solo design serve tutti e tre, e le sezioni degli apparati finali stampano il loro {attr.note} dove starebbe il numero. minHeight riserva otto righe da 14,6 pt, 41,2 mm dalla testa della gabbia, che superano la fascia di 3,2 mm e tengono il testo sulla sua griglia. Il testo di design mette la sua linea di base a 0,8 della riga, quindi un riquadro allineato in basso il cui piede sta below() sotto una linea di base mette la sua ultima riga su quella linea di base. Il titolo, il numero da 118 pt e l’ultima riga della nota poggiano tutti sulla linea di base del titolo, 52,5 mm sotto il taglio. La riga del numero è 0,72 del suo corpo, così la riga sta dentro il suo riquadro.

#3 · Indica l’appendice con una lettera dal suo stile

script.js · righe 109–115nel codice completo
// The style's '{1:A}' (see answer) prints the level's counter as a letter, and startAt=1 on the
// first appendix restarts it after chapter 6. The band and the running head read that letter.
// A resource template's {h1} is a number unless it names a style: '{h1:A}.{n}' counts the
// tables A.1, A.2… and B.1 in a second appendix, with one type for all of them.
const appendixHeads = heads('Appendix {chapterNumber}. {chapterTitle}');
const appendixTables = { ...defaultResourceTypes(LANG).find((type) => type.id === 'table'),
  id: 'table-a', numberingTemplate: '{h1:A}.{n}', resetOn: 'h1' }; // a copy of 'table'

Lo stile dell’appendice imposta numberingTemplate: '{1:A}', che stampa il contatore del livello come lettera, e # Interview guide {style="appendix" startAt=1} fa ripartire quel contatore, così la prima appendice è A e non G (stili dei titoli). La fascia legge la lettera tramite {number} e la testatina tramite {chapterNumber}. Il {h1} di un tipo di risorsa stampa il contatore come numero, a meno che non nomini uno stile, quindi le tabelle dell’appendice prendono un tipo il cui modello è '{h1:A}.{n}': A.1, A.2 e così via, e B.1 in una seconda appendice; con il tipo predefinito la tabella sarebbe la tabella 1.1. I titoli interni del capitolo contano dai modelli dei livelli: '{1}.{2}' stampa 6.1, e continuation.headings.h1: 5 fa di questo il sesto capitolo.

#4 · Metti le testatine sul margine esterno

script.js · righe 83–105nel codice completo
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)) } } }] };

I quattro elementi di testo sono ancorati alla pagina e filtrati per parity, che tiene il folio sul margine esterno di entrambe le pagine; ognuno è collocato dalla sua sommità, above() sopra una linea di base 17,5 mm sotto il taglio, così il folio da 9,5 pt e le maiuscole da 7,5 pt stanno su una stessa riga. Il verso porta il titolo della tesi ({title}, dal frontmatter) e il recto quello della sezione ({chapterTitle}), come INDEX a pagina 177. pages: 'body' le tiene lontane dalle aperture, il cui unico folio è quello del piè di pagina, centrato sotto la gabbia. Il recto del capitolo direbbe Chapter 6. Conclusion e quello dell’appendice Appendix A. Interview guide, ma in questo esempio solo l’indice prosegue su un recto.

#5 · Segna i termini dove il testo ne parla

script.js · righe 119–125nel codice completo
// 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] stampa la sua parola e la archivia sotto la stessa parola; Rayner:index{term="Rayner, Keith"} non stampa nulla e archivia la pagina di Rayner sotto il nome completo; term="interviews!timing of" crea una sottovoce. I termini del glossario portano main, quindi la loro pagina è in grassetto, come dice la nota nella fascia dell’indice. :::index sotto # Index {style="index"} stampa le voci nelle due colonne dello stile, e buildDocument impagina di nuovo il documento finché i numeri di pagina non smettono di spostarsi. Le voci sono a 9 su 11,6 pt, con le righe successive sporgenti di 2 em e le sottovoci rientrate di 1 em; le lettere sono a 13 pt nel carattere da titolazione. I segni non funzionano nelle celle delle tabelle, quindi le righe della tabella 6.1 non aggiungono pagine: look-backs elenca il paragrafo che ne riferisce, non la tabella.

#6 · Tieni ogni valore predefinito nel colore dell’inchiostro

script.js · righe 15–19nel codice completo
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' } }));

I valori predefiniti del motore per grassetto, corsivo, marcatori di elenco e rimandi seguono la voce di palette main-color, quindi farla puntare all’inchiostro li stampa in nero invece che in blu (palette dei colori). referenceBold: false compone Table 6.1 in tondo, come le citazioni autore-anno che la circondano.

La ricetta 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.28.0
import {
  buildDocumentWithFonts, renderPageToCanvas, 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 '#'. The appendix counts, in letters: its
// template prints the level's counter as A, B… 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', { numbered: true, numberingTemplate: '{1:A}', header: appendixHeads,
    breakBefore: { enabled: true, parity: 'odd' }, advancedDesign: opener('Appendix') }),
  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,
// with every word whole (hyphenation.ragged is off). 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, so the line fits inside its box.
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 is '{number}': the chapter's 6, the appendix's A, nothing on an unnumbered heading.
const opener = (label) => ({ 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', '{number}', 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: lettered by its style, '# Interview guide {style="appendix" startAt=1}'
// The style's '{1:A}' (see answer) prints the level's counter as a letter, and startAt=1 on the
// first appendix restarts it after chapter 6. The band and the running head read that letter.
// A resource template's {h1} is a number unless it names a style: '{h1:A}.{n}' counts the
// tables A.1, A.2… and B.1 in a second appendix, with one type for all of them.
const appendixHeads = heads('Appendix {chapterNumber}. {chapterTitle}');
const appendixTables = { ...defaultResourceTypes(LANG).find((type) => type.id === 'table'),
  id: 'table-a', numberingTemplate: '{h1:A}.{n}', resetOn: 'h1' }; // 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 = () => ({
  locale: 'en-us', // the thesis is written in English
  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'),
    referenceBold: false, // 'Table 6.1' in roman, like the citations around it
    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: [
      // Chapters open on a recto. span: 'page' (the styles inherit it) sets the band and the
      // title across both columns of the glossary and the index.
      { 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 on white: an APA table has no fills and no vertical rules.
  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`---
Esempio Markdown · 126 righe · 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" startAt=1} 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 // ─── 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 } }; // The index is laid out again until its page numbers settle, inside this one call. const doc = await buildDocumentWithFonts({ markdown, resources, continuation }, config(), kitFonts(FONTS)); showPages(doc, { title: 'Reading on Screens and Paper: the back matter' }); offerPdf(() => renderToPdf(doc, { fontProvider: fontsourceProvider }), `${RECIPE}.pdf`);
Kit · core, fonts, viewer, pdf: uguale in ogni ricetta · 269 righe// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── Kit · pdf v3 ── the same in every recipe that exports a PDF /** The Fontsource files the screen used, as TrueType, with their range and * place: the nearest weight the family ships, upright with no italic; * latin, then what its letters need (kitSubsetsFor). */ async function fontsourceProvider(family, weight, style, request) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); const weights = meta?.weights?.length ? meta.weights : [400, 700]; const w = weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)); const s = style === 'italic' && meta && !meta.styles.includes('italic') ? 'normal' : style; const text = String.fromCodePoint(...(request?.codePoints ?? [])); const files = await Promise.all(['latin', ...kitSubsetsFor(text, meta)].map(async (subset, order) => { const res = await fetch(`${await fontsourceAt(id)}files/${id}-${subset}-${w}-${s}.woff2`); if (!res.ok) throw new Error(`Fontsource has no ${family} ${w} ${s} ${subset}`); return Object.assign(await decompressWoff2(new Uint8Array(await res.arrayBuffer())), { unicodeRange: kitRanges()[subset], order }); })); return files.length === 1 ? files[0] : files; } /** A "Build the PDF" button; then "Open the PDF" (a new tab: CodePen's frame * shows no PDFs) and a download link. */ function offerPdf(makePdf, filename) { viewer(); const button = Object.assign(document.createElement('button'), { type: 'button', textContent: 'Build the PDF' }); button.dataset.postextPdf = filename; button.addEventListener('click', async () => { button.disabled = true; button.textContent = 'Building the PDF…'; try { const bytes = await makePdf(); const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' })); const size = `${Math.max(1, Math.round(bytes.length / 1024))} KB`; button.replaceWith( Object.assign(document.createElement('a'), { href: url, target: '_blank', rel: 'noopener', textContent: 'Open the PDF ↗' }), Object.assign(document.createElement('a'), { href: url, download: filename, textContent: `Download ${filename} · ${size}` })); } catch (error) { button.disabled = false; button.textContent = 'Build the PDF'; kitFail(error); } }); document.getElementById('pt-actions').append(button); } // ─── /Kit ───────────────────────────────────────────────────────────────────────

Lo script.js composto funziona così com’è: incollalo nello script modulo di qualsiasi pagina, oppure apri la ricetta su CodePen. Cartella della ricetta su GitHub ↗ (si apre in una nuova scheda)

Varianti

#Comincia ogni sezione su un recto

Le tesi da rilegare per una biblioteca aprono spesso ogni sezione su una pagina di destra. Il glossario, la bibliografia e l’indice passano allora alle pagine 175, 177 e 179, ciascuno dopo un verso bianco, e i numeri in grassetto dell’indice rimandano a pagina 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 });

#Componi anche l’inizio della tesi

Le pagine preliminari, numerate in cifre romane prima di pagina 1, sono in Pagine preliminari con folii romani, poi pagina 1.

Errori frequenti

Errore frequente

Uno stile di titolo eredita l’interruzione di pagina del suo livello

Una voce di headingStyles prende dal suo livello di titolo ogni campo che omette, breakBefore compreso. Un indice o un colophon con lo stile di un H1 dopo un :::pagebreak eredita parity 'odd' e finisce dietro una pagina bianca. Dai a uno stile del genere breakBefore: { enabled: false }. Stili di titolo →

Errore frequente

La pagina 1 è un recto: pianifica le pagine con i numeri fisici

La pagina 1 è una pagina destra e la pagina 2 il primo verso, quindi pianifica le doppie pagine con i numeri di pagina fisici: un’apertura su una pagina pari sta di fronte alla pagina dispari che la segue. Interruzioni di pagina e di colonna →

Crediti

Testo
Testo originale, CC BY 4.0
Font
Libertinus Serif (SIL OFL 1.1) · Libertinus Serif Display (SIL OFL 1.1) · Libertinus Sans (SIL OFL 1.1)
SandboxPDF