# Evidenziazioni a colori in un libro di letture

> Un libro di letture di storia le cui parole chiave hanno tre colori (verde per i termini, ambra per le date, blu per i nomi) presi da tre markStyles.

- Versione HTML: https://postext.dev/it/cookbook/colour-coded-study-highlights
- Ricetta Nº 180 · Caratteri e testo · Livello 2 (Intermedio) · Output: Canvas
- Generi: Libri di testo
- Richiede postext ≥ 1.65.0 · provata con 1.65.0 il 2026-10-31
- Pagine: [1](https://postext.dev/cookbook/colour-coded-study-highlights/en/p01.webp?v=d1c18f3b), [2](https://postext.dev/cookbook/colour-coded-study-highlights/en/p02.webp?v=d1c18f3b), [3](https://postext.dev/cookbook/colour-coded-study-highlights/en/p03.webp?v=d1c18f3b), [4](https://postext.dev/cookbook/colour-coded-study-highlights/en/p04.webp?v=d1c18f3b)
- Apri nella Sandbox: https://postext.dev/it/sandbox#recipe=colour-coded-study-highlights&lang=en (.postext: https://postext.dev/cookbook/colour-coded-study-highlights/en/colour-coded-study-highlights.postext)
- Ultimo aggiornamento: 2026-10-31
- Altre lingue: [en](https://postext.dev/en/cookbook/colour-coded-study-highlights.md), [es](https://postext.dev/es/cookbook/colour-coded-study-highlights.md), [ca](https://postext.dev/ca/cookbook/colour-coded-study-highlights.md), [de](https://postext.dev/de/cookbook/colour-coded-study-highlights.md), [fr](https://postext.dev/fr/cookbook/colour-coded-study-highlights.md), [pt](https://postext.dev/pt/cookbook/colour-coded-study-highlights.md), [zh](https://postext.dev/zh/cookbook/colour-coded-study-highlights.md), [ja](https://postext.dev/ja/cookbook/colour-coded-study-highlights.md), [ar](https://postext.dev/ar/cookbook/colour-coded-study-highlights.md)

## In breve

Una lettura scolastica sulla storia della stampa. I termini chiave sono evidenziati in verde, le date in ambra, le persone e i luoghi in blu, e una legenda nella prima pagina dice che cosa significa ogni colore.

## Che cosa comporrai

La lettura 2 di un’unità di un libro scolastico di storia, *Readings in Early Modern History*: quattro pagine su come la stampa si diffuse in Europa da Magonza tra il 1450 e il 1550, su una pagina di 170 × 240 mm. I curatori hanno segnato il testo come farebbe uno studente attento, con un codice che resta lo stesso in tutto il libro: i termini chiave su una fascia verde, le date sull’ambra, le persone e i luoghi sull’azzurro. Una legenda sotto il titolo spiega il codice con i tre colori stessi, e il glossario dell’ultima pagina apre ogni voce con il suo termine verde. I segni sono fasce sotto testo normale, quindi le righe vanno a capo, si sillabano e si giustificano come se le fasce non ci fossero.

**Questa ricetta risponde a:**

- Come evidenzio un testo di studio con colori che abbiano ciascuno un significato: termini, date e nomi?

## La risposta breve

```js
// script.js, righe 37–52
// :mark[Mainz]{style="name"} in the text picks a style by id. The band is painted under
// words that stay ordinary text, so the lines break, hyphenate and justify as if the
// mark were not there, and a mark that runs over a line end gets a band on each line.
const band = (id, name) => ({
  id, name,
  background: col(id), // pale enough for ink at full contrast on top
  color: col('ink'), // the words keep the colour of the text
  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
  paddingX: em(0.12), // the band reaches a little past the first and the last letter
  paddingTop: em(0.04), paddingBottom: em(0.1), // and down past the descenders
});
const markStyles = [
  band('term', t({ en: 'Key term', es: 'Término clave' })),
  band('date', t({ en: 'Date', es: 'Fecha' })),
  band('name', t({ en: 'Name', es: 'Nombre' })),
];
```

## Ingredienti

**Insegna**

- [Evidenziazioni](https://postext.dev/it/docs/document-format-inline.md#evidenziazioni): Una fascia colorata dietro una frase, come un libro di scuola evidenzia le parole chiave: le parole restano testo normale e vanno a capo, la fascia segue ogni pezzo.

**Usa anche**

- [Riquadri](https://postext.dev/it/docs/configuration-callouts.md#stili-dei-riquadri)
- [Tavolozza di colori semantica](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori)
- [Aperture progettate](https://postext.dev/it/docs/configuration-headings.md#estensione-e-grafica-avanzata)
- [Immagini nei progetti di pagina](https://postext.dev/it/docs/configuration-headers-footers.md#elementi-immagine)
- [Attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)
- [Testatine e folio](https://postext.dev/it/docs/configuration-headers-footers.md#intestazioni-e-piè-di-pagina)
- [Testatine secondo il ruolo della pagina](https://postext.dev/it/docs/configuration-headers-footers.md#elementi-di-testo)
- [Margini speculari](https://postext.dev/it/docs/configuration-page-layout.md#margini-speculari)
- [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Vedove, orfane e righe corte](https://postext.dev/it/docs/configuration-page-flow.md#orfane-vedove-righe-finali-corte-e-regole-di-tenuta)
- [Bibliografie e glossari](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)
- [Figura e Tabella nella tua lingua](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)
- [Tipi di risorsa personalizzati](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)
- [Figure e tabelle come risorse](https://postext.dev/it/docs/document-format-resources.md#risorse)

**La configurazione in breve**

- [`bodyText`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), [`calloutStyles`](https://postext.dev/it/docs/configuration-callouts.md#stili-dei-riquadri), [`captionStyle`](https://postext.dev/it/docs/configuration-resources.md#stile-delle-didascalie), [`colorPalette`](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori), [`footer`](https://postext.dev/it/docs/configuration-headers-footers.md#intestazioni-e-piè-di-pagina), [`header`](https://postext.dev/it/docs/configuration-headers-footers.md#intestazioni-e-piè-di-pagina), [`headings`](https://postext.dev/it/docs/configuration-headings.md#titoli), [`layout`](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione), [`locale`](https://postext.dev/it/docs/configuration-language.md#sillabazione), [`markStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-di-evidenziazione), [`orderedLists`](https://postext.dev/it/docs/configuration-lists-math.md#elenchi-numerati), [`page`](https://postext.dev/it/docs/configuration-page-layout.md#pagina), [`paragraphStyles`](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo), [`resourceTypes`](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa), [`unorderedLists`](https://postext.dev/it/docs/configuration-lists-math.md#elenchi-puntati)

**API**

- [`buildDocumentWithFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione), [`defaultResourceTypes`](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa), [`registerResourceImage`](https://postext.dev/it/docs/architecture.md#superficie-dellapi), [`renderPageToCanvas`](https://postext.dev/it/docs/configuration-programmatic-usage.md#rasterizzare-una-pagina)

**Caratteri**

- Source Serif 4 (OFL-1.1), Source Sans 3 (OFL-1.1)

## Preparazione

### 1 · Uno stile per ogni tipo di parola

Il codice è nella [risposta breve](https://postext.dev/it/cookbook/colour-coded-study-highlights.md#la-risposta-breve) qui sopra. `markStyles` dichiara tre stili con nome, e il testo ne sceglie uno per ogni segno: `:mark[Johann Fust]{style="name"}`, `:mark[1455]{style="date"}`, `:mark[colophon]{style="term"}` ([evidenziazioni](https://postext.dev/it/docs/document-format-inline.md#evidenziazioni)). Ogni stile dice che cosa significa (`name` è ciò che vede chi cura il testo), così il Markdown registra perché una parola è segnata e non di che colore è. Se cambi il colore di uno stile, cambiano con lui tutte le date del libro. Le estremità sono dritte, come di default da postext 1.52 ([stili di evidenziazione](https://postext.dev/it/docs/configuration-styles.md#stili-di-evidenziazione)): un evidenziatore lascia estremità dritte, e una fascia arrotondata si legge come un chip.

### 2 · Tre tinte dalla tavolozza

```js
// script.js, righe 14–29
const palette = {
  ink: '#1f1c19', // text: a warm near-black
  accent: '#8a3220', // kickers, rules, numbers: an iron-gall red
  tint: '#f5f0e7', // the key's background
  rule: '#d3cabc', // hairlines
  muted: '#6b645b', // running heads, colophon
  paper: '#ffffff',
  term: '#cce8c8', // key terms: a pale green
  date: '#fbe0a1', // dates: amber
  name: '#cfe0f4', // people and places: a pale blue
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
```

Le tre fasce sono voci della tavolozza come l’inchiostro e l’accento, quindi chi apre il libro nel Sandbox può ricolorare il codice da un solo campione. Sono chiare, vicine alla luminosità del fondo del riquadro, e il testo sopra di esse mantiene un contrasto di 12,6:1 o più; le tre restano distinte quando si incontrano su una riga, come in «In 1517 Martin Luther» a pagina 3. Un segno non occupa spazio: `paddingX` entra di 0,12 em nello spazio accanto alla frase e `paddingBottom` porta la fascia sotto i discendenti, entrambi dipinti in uno spazio che c’era già.

### 3 · L’apertura: un dipinto sopra il titolo

```js
// script.js, righe 56–81
const opener = {
  enabled: true,
  minHeight: mm(122), // the floor of the opener: the key starts on the same line
  marginBottom: pt(0),
  slot: {
    elements: [
      { kind: 'image', id: 'press', resourceId: 'press', fit: 'cover',
        placement: { anchor: { to: 'page', edge: 'top-left' },
          size: { width: mm(170), height: mm(96) } } },
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: SANS,
        fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
        textTransform: 'uppercase', color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(82) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF,
        fontSize: pt(26), lineHeight: 1.08, fontWeight: 700, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(2) },
          size: { width: mm(126), height: 'auto' } } },
      { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: SERIF,
        italic: true, fontSize: pt(10.5), lineHeight: LEAD / 10.5, color: col('ink'),
        align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(118), height: 'auto' } } },
    ],
  },
};
```

Il titolo di primo livello stampa un design. Il dipinto è ancorato alla pagina, quindi va al vivo in alto e sui due lati, 170 per 96 mm, con `fit: 'cover'`. L’occhiello e il sommario vengono da attributi della riga del titolo ([attributi dei titoli](https://postext.dev/it/docs/document-format.md#attributi-dei-titoli)), e la legenda segue come un normale `:::callout` le cui voci di elenco cominciano con i tre segni. `minHeight` fissa la profondità: la legenda comincia sulla stessa riga per quanto lungo sia il titolo.

### 4 · Testatine secondo la parità

```js
// script.js, righe 85–106
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: SANS, fontSize: pt(7.5), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } },
  ...extra,
});
const folio = { fontWeight: 700, color: col('ink') };
const header = {
  elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ],
};
const footer = {
  elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
    ...folio, pages: 'opener',
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: mm(0), y: mm(9) } },
  })],
};
```

La pagina pari porta il titolo della collana e la dispari quello della lettura, filtrati con `parity` e tenuti fuori dall’apertura da `pages: 'body'` ([elementi di testo](https://postext.dev/it/docs/configuration-headers-footers.md#elementi-di-testo)). L’apertura porta il numero di pagina al piede.

## La ricetta completa

Un unico file, composto a partire dalla cartella della ricetta con il testo di esempio e il kit comune del Ricettario già inclusi; costruisce la propria pagina. Per eseguirlo, mettilo in uno `<script type="module">` di una pagina vuota, oppure incollalo nel pannello JS di un nuovo pen di CodePen (come modulo). Importa postext da esm.sh: non c’è niente da installare né da compilare.

- Cartella della ricetta: https://github.com/drnachio/postext/tree/main/cookbook/colour-coded-study-highlights

### script.js

```js
// ═══ Postext Cookbook · Nº 180 · Colour-coded highlights in a study reader ════════
// https://postext.dev/en/cookbook/colour-coded-study-highlights
// Code: MIT · Text: original (CC BY 4.0) · Picture: painting made for the recipe
// Fonts: Source Serif 4, Source Sans 3 (SIL OFL 1.1) · Needs postext ≥ 1.65.0
import {
  buildDocumentWithFonts, renderPageToCanvas, registerResourceImage, defaultResourceTypes,
} from 'https://esm.sh/postext';

const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es')
const RECIPE = 'colour-coded-study-highlights';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: semantic colours, three of them for the highlighter
const palette = {
  ink: '#1f1c19', // text: a warm near-black
  accent: '#8a3220', // kickers, rules, numbers: an iron-gall red
  tint: '#f5f0e7', // the key's background
  rule: '#d3cabc', // hairlines
  muted: '#6b645b', // running heads, colophon
  paper: '#ffffff',
  term: '#cce8c8', // key terms: a pale green
  date: '#fbe0a1', // dates: amber
  name: '#cfe0f4', // people and places: a pale blue
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.accent, model: 'hex' } },
];
// #endregion
const OUTER = 20; // outer margin in mm: the running heads align to it
const LEAD = 14.5; // body leading in pt
const SANS = 'Source Sans 3';
const SERIF = 'Source Serif 4';

// #region answer: three highlight styles, one per kind of word, linked to the palette
// :mark[Mainz]{style="name"} in the text picks a style by id. The band is painted under
// words that stay ordinary text, so the lines break, hyphenate and justify as if the
// mark were not there, and a mark that runs over a line end gets a band on each line.
const band = (id, name) => ({
  id, name,
  background: col(id), // pale enough for ink at full contrast on top
  color: col('ink'), // the words keep the colour of the text
  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
  paddingX: em(0.12), // the band reaches a little past the first and the last letter
  paddingTop: em(0.04), paddingBottom: em(0.1), // and down past the descenders
});
const markStyles = [
  band('term', t({ en: 'Key term', es: 'Término clave' })),
  band('date', t({ en: 'Date', es: 'Fecha' })),
  band('name', t({ en: 'Name', es: 'Nombre' })),
];
// #endregion

// #region opener: the painting bleeds off the top of the page, the title sits under it
const opener = {
  enabled: true,
  minHeight: mm(122), // the floor of the opener: the key starts on the same line
  marginBottom: pt(0),
  slot: {
    elements: [
      { kind: 'image', id: 'press', resourceId: 'press', fit: 'cover',
        placement: { anchor: { to: 'page', edge: 'top-left' },
          size: { width: mm(170), height: mm(96) } } },
      { kind: 'text', id: 'kicker', content: '{attr.kicker}', fontFamily: SANS,
        fontSize: pt(8.5), fontWeight: 700, letterSpacing: pt(1.6),
        textTransform: 'uppercase', color: col('accent'), align: 'left',
        placement: { anchor: { to: 'container', edge: 'top-left' },
          offset: { x: mm(0), y: mm(82) } } },
      { kind: 'text', id: 'title', content: '{titleText}', fontFamily: SERIF,
        fontSize: pt(26), lineHeight: 1.08, fontWeight: 700, color: col('ink'), align: 'left',
        placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { x: mm(0), y: mm(2) },
          size: { width: mm(126), height: 'auto' } } },
      { kind: 'text', id: 'standfirst', content: '{attr.standfirst}', fontFamily: SERIF,
        italic: true, fontSize: pt(10.5), lineHeight: LEAD / 10.5, color: col('ink'),
        align: 'left',
        placement: { anchor: { to: '#title', edge: 'below' }, offset: { x: mm(0), y: mm(3) },
          size: { width: mm(118), height: 'auto' } } },
    ],
  },
};
// #endregion

// #region running-heads: series title on the verso, reading title on the recto
const head = (id, content, parity, edge, x, extra = {}) => ({
  kind: 'text', id, content, parity, pages: 'body',
  fontFamily: SANS, fontSize: pt(7.5), fontWeight: 600, letterSpacing: pt(1.2),
  textTransform: 'uppercase', color: col('muted'),
  placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } },
  ...extra,
});
const folio = { fontWeight: 700, color: col('ink') };
const header = {
  elements: [
    head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio),
    head('verso-title', '{title}', 'even', 'top-left', OUTER + 9),
    head('recto-title', '{chapterTitle}', 'odd', 'top-right', -(OUTER + 9)),
    head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio),
  ],
};
const footer = {
  elements: [head('drop-folio', '{pageNumber}', 'all', 'top', 0, {
    ...folio, pages: 'opener',
    placement: { anchor: { to: 'container', edge: 'top' }, offset: { x: mm(0), y: mm(9) } },
  })],
};
// #endregion

const config = {
  locale: t({ en: 'en-us', es: 'es' }),
  resourceTypes: defaultResourceTypes(LANG),
  colorPalette,
  markStyles,
  page: {
    sizePreset: 'custom', width: mm(170), height: mm(240), dpi: 150,
    margins: { top: mm(22), bottom: mm(22), left: mm(22), right: mm(OUTER), mirror: true },
  },
  layout: { layoutType: 'single' },
  bodyText: {
    fontFamily: SERIF, fontSize: pt(10.5), lineHeight: pt(LEAD), color: col('ink'),
    boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('ink'),
    textAlign: 'justify', firstLineIndent: mm(4.5), indentAfterHeading: false,
    hyphenation: { enabled: true }, optimalLineBreaking: true,
    avoidWidows: true, avoidOrphans: true, avoidRunts: true,
  },
  headings: {
    fontFamily: SERIF, color: col('ink'), fontWeight: 700,
    levels: [
      { level: 1, fontSize: pt(26), breakBefore: { parity: 'odd' }, advancedDesign: opener },
      { level: 2, fontFamily: SANS, fontSize: pt(12), lineHeight: pt(LEAD), fontWeight: 700,
        color: col('accent'), marginTop: pt(LEAD), marginBottom: pt(0) },
    ],
  },
  unorderedLists: { bulletChar: '–', color: col('accent'), fontWeight: 400,
    marginTop: pt(0), marginBottom: pt(0) },
  orderedLists: { numberFormat: 'arabic', color: col('accent'), fontWeight: 700,
    marginTop: pt(0), marginBottom: pt(0) },
  paragraphStyles: [
    // The glossary: each green term opens its entry, the turnovers hang 1.5 em.
    { id: 'glossary', fontSize: pt(9.5), lineHeight: pt(13), textAlign: 'left',
      hangingIndent: em(1.5) },
    { id: 'colophon', fontFamily: SANS, fontSize: pt(7.5), lineHeight: pt(10),
      color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) },
  ],
  // The key: a tint with a top stripe, its title in the label face.
  calloutStyles: [
    { id: 'key', title: t({ en: 'Key', es: 'Clave' }), background: col('tint'),
      stripe: { enabled: true, side: 'top', width: pt(2.5), color: col('accent') },
      padding: { top: mm(3), right: mm(4), bottom: mm(3), left: mm(4) },
      titleStyle: { fontFamily: SANS, fontSize: pt(8), fontWeight: 700,
        textTransform: 'uppercase', letterSpacing: pt(1.2), color: col('accent') },
      body: { fontFamily: SANS, fontSize: pt(9.5), lineHeight: pt(13), color: col('ink'),
        firstLineIndent: pt(0), textAlign: 'left', boldColor: col('ink'),
        italicColor: col('ink') } },
  ],
  captionStyle: { fontFamily: SANS, fontSize: pt(8), color: col('ink'), labelBold: true,
    labelColor: col('accent'), note: { color: col('muted') } },
  header,
  footer,
};

// ─── 2 · Content ────────────────────────────────────────────────────────────
const markdown = String.raw`---
title: "Readings in Early Modern History"
author: "Study edition"
---

# How Printing Spread Across Europe {kicker="Unit 4 · Reading 2" standfirst="In a hundred years a craft from one German town filled Europe with books. This reading follows the press from Mainz to more than two hundred towns and asks what changed when books became cheap."}

:::callout{type="key" title="Reading the highlights"}
The editors have marked three kinds of words in this reading. Use the same colours in your own notes.

- :mark[Key term]{style="term"}: a word the glossary at the end of the unit defines.
- :mark[Date]{style="date"}: a year or a period to place on your timeline.
- :mark[Name]{style="name"}: a person or a place in the story.
:::

## A workshop in Mainz

Around :mark[1450]{style="date"} a goldsmith named :mark[Johannes Gutenberg]{style="name"} was running a printing workshop in :mark[Mainz]{style="name"}, a city on the Rhine. Printing itself was not new. Printers in China had cut whole pages into wooden blocks for centuries, and Korean printers had cast metal type by the :mark[thirteenth century]{style="date"}. What Gutenberg put together was a system that suited the twenty-odd letters of the Latin alphabet.

The system had four parts. The first was :mark[movable type]{style="term"}: single letters cast in metal, set in lines, printed and then put back in the case for the next page. A hand mould cast thousands of identical letters from an alloy of lead, tin and antimony. An ink made with linseed oil clung to the metal where a watery ink would have run off. And a :mark[screw press]{style="term"}, of the kind used to press grapes and to flatten wet paper, pushed the paper evenly onto the inked type.

The first large book from the workshop was a Latin Bible in two volumes, set in two columns of 42 lines, finished by :mark[1455]{style="date"}. About 180 copies were printed, most on paper and some on vellum. A single scribe could spend years copying one Bible; the workshop produced its whole edition in about the same time.

Gutenberg had borrowed heavily to build all this. His lender, the Mainz merchant :mark[Johann Fust]{style="name"}, took him to court in :mark[1455]{style="date"} and kept much of the equipment. Fust carried on the business with :mark[Peter Schöffer]{style="name"}, who had trained in the workshop. Their Psalter of :mark[1457]{style="date"} is the first printed book whose :mark[colophon]{style="term"}, the closing note of a book, names its printers and the day it was finished.

## The craft travels

For about ten years printing stayed close to Mainz. In :mark[1462]{style="date"} the city was sacked in a war between two rival archbishops, and many of its craftsmen left. Printers trained on the Rhine set up wherever they found readers, paper and money: at :mark[Subiaco]{style="name"}, near Rome, in :mark[1465]{style="date"}; in :mark[Venice]{style="name"} in :mark[1469]{style="date"}; in :mark[Paris]{style="name"}, in rooms at the Sorbonne, in :mark[1470]{style="date"}. A press was at work in :mark[Segovia]{style="name"} around :mark[1472]{style="date"}, and :mark[Valencia]{style="name"} had one by :mark[1474]{style="date"}.

:mark[William Caxton]{style="name"}, an English merchant who had learned the craft in :mark[Cologne]{style="name"}, set up his press at :mark[Westminster]{style="name"} in :mark[1476]{style="date"}. Most printers of the time worked in Latin, the language of the Church and the universities, and sold across borders. Caxton printed mostly in the :mark[vernacular]{style="term"}, the language people spoke at home: romances, histories and Chaucer's *Canterbury Tales*, for readers who had no Latin.

Books printed before :mark[1501]{style="date"} are called :mark[incunabula]{style="term"}, from the Latin word for a cradle: the books of printing's infancy. Catalogues today list about 30,000 editions from those fifty years. By :mark[1500]{style="date"} presses had worked in more than 250 towns, and estimates of the copies they printed run from eight to twenty million.

## Venice and the pocket book

By the end of the century :mark[Venice]{style="name"} had more printing houses than any other city in Europe. It had paper mills on the mainland, ships to carry books to every port of the Mediterranean, and buyers with money. :mark[Aldus Manutius]{style="name"} opened his press there in the :mark[mid-1490s]{style="date"} and printed the Greek classics in editions prepared by scholars. His books carried a printer's mark, a dolphin wrapped round an anchor, so that buyers could tell them from the copies other printers soon made.

In :mark[1501]{style="date"} Aldus issued a Virgil in :mark[octavo]{style="term"}, a sheet folded three times to make sixteen pages, so that the book fitted in a hand. It was set in a slanting letter cut by :mark[Francesco Griffo]{style="name"}, which we now call :mark[italic]{style="term"}. The narrow letters fitted more words on each line, so the small books needed less paper and cost less. Readers could carry a poet in a pocket instead of reading him at a desk.

## Print and the Reformation

In :mark[1517]{style="date"} :mark[Martin Luther]{style="name"}, who taught theology at :mark[Wittenberg]{style="name"}, wrote ninety-five theses against the sale of indulgences. Printers in several German cities reprinted them within weeks. Luther went on to write short :mark[pamphlets]{style="term"} in German: a few pages each, quick to set, cheap to buy and easy to pass from hand to hand. Historians estimate that between :mark[1518]{style="date"} and :mark[1525]{style="date"} his writings made up about a third of all the books printed in German.

His opponents answered in print too, and the quarrel over the Church was argued in thousands of pamphlets read aloud in taverns and market squares. Printing did not cause the Reformation, but it carried each side's arguments further and faster than any preacher could.

## Check your reading

1. Name the four parts of Gutenberg's system, and say what each one did.
2. Why did the sack of Mainz in 1462 help printing to spread?
3. What is a colophon, and why would a historian be glad to find one?
4. Caxton chose the vernacular. Who were his readers, and why did that choice sell books?
5. Put the amber dates of this reading on a timeline from 1450 to 1525.

## Glossary

:::paragraphs{style="glossary"}
**:mark[colophon]{style="term"}** A note at the end of a book that gives the printer, the place and the date of printing.

**:mark[incunabula]{style="term"}** Books printed in Europe before 1501. One such book is an incunable.

**:mark[italic]{style="term"}** A slanting style of type, first cut by Francesco Griffo for Aldus Manutius.

**:mark[movable type]{style="term"}** Single letters cast one by one, which can be set in lines, printed and used again.

**:mark[octavo]{style="term"}** A book whose sheets are folded three times, giving sixteen pages to the sheet.

**:mark[pamphlet]{style="term"}** A short booklet of a few unbound pages, quick and cheap to print.

**:mark[screw press]{style="term"}** A press in which a turning screw drives a flat plate down onto the paper and the type.

**:mark[vernacular]{style="term"}** The language people speak at home, as opposed to Latin.
:::

:::paragraphs{style="colophon"}
Set in Source Serif 4 and Source Sans 3 (SIL Open Font License) · Text: original, CC BY 4.0 · Opening picture: a painting made for this edition.
:::
`; // content.<lang>.md: the reading, its key and questions

// #region art: a printing workshop around 1460, a painting in assets/ (no lettering)
const resources = [{ id: 'press', typeId: 'figure', kind: 'bitmap', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'press-1400.jpg', format: 'jpeg', width: 1400, height: 790 },
  altText: t({
    en: 'A printing workshop: a compositor picks type from a case by the window while a '
      + 'pressman pulls the bar of a wooden screw press; printed sheets hang drying overhead.',
    es: 'Un taller de imprenta: un cajista toma tipos de la caja junto a la ventana mientras '
      + 'un prensista tira de la barra de una prensa de husillo; arriba se secan los pliegos.',
  }) }];
await loadImage(resources[0].bitmap.fileId, asset(resources[0].bitmap.fileId));
// #endregion

// ─── 3 · Fonts ──────────────────────────────────────────────────────────────
const FONTS = {
  'Source Serif 4': ['400', '400i', '700'],
  'Source Sans 3': ['400', '600', '700'],
};

// ─── 4 · Build & show ───────────────────────────────────────────────────────
const doc = await buildDocumentWithFonts({ markdown, resources }, config, kitFonts(FONTS));
showPages(doc, { title: t({ en: 'A study reader in three highlight colours',
  es: 'Un libro de lecturas resaltado en tres colores' }) });

// ─── 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 and asks the kit
// for Fontsource's files, the ones the PDF embeds too.

/** buildDocumentWithFonts' options: the resolver, and FONTS' faces loaded
 *  with the config's. 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 files with their ranges: latin, then latin-ext and greek when
 *  `text` needs them (kitSubsetsFor). Null for a face Fontsource lacks. */
async function fontsourceResolver(family, weight, style, { text = '' } = {}) {
  const id = fontsourceId(family);
  const meta = await fontsourceMeta(family);
  if (meta && !(await fontsourceWeights(family, style)).includes(weight)) 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],
  }));
}

/** Each file's unicode-range (a function: the kit runs after the pen's code). */
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: loads `faces` (as FONTS) for the
 *  sample `text`; `optional` skips a face Fontsource lacks. Files are added in
 *  the order asked, the last winning a shared character. Resolves to the count. */
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 && (await fontsourceWeights(family, style)).includes(weight))) 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 face (fonts.check() says yes to any family). */
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, id = fontsourceId(family)) {
  return (fontsourceMeta.c ??= {})[id] ??= fetch(`https://api.fontsource.org/v1/fonts/${id}`)
    .then((res) => (res.ok ? res.json() : null), () => null);
}

/** The weights the package ships in `style`: the API pairs none (no Libre
 *  Caslon 700 italic file), so italics are read from latin-italic.css. */
function fontsourceWeights(family, style, id = fontsourceId(family)) {
  return (fontsourceWeights.c ??= {})[id + style] ??= fontsourceMeta(family).then(async (m) => {
    if (!m?.styles.includes(style)) return m ? [] : [400, 700];
    const css = style === 'italic' ? await fetch(`${await fontsourceAt(id)}latin-italic.css`).then((r) => (r.ok ? r.text() : ''), () => '') : '';
    const w = [...css.matchAll(/weight: (\d+)/g)].map((x) => +x[1]);
    return w.length ? w : m.weights;
  });
}

// ─── 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 · images v3 ── recipes with pictures · postext.dev/cookbook
/** Registers a photo or PNG for the canvas and keeps its bytes for the PDF.
 *  fetch → ImageBitmap never taints the canvas (a plain cross-origin <img> would). */
async function loadImage(fileId, url) {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`Image not found (${res.status}): ${url}`);
  const bytes = new Uint8Array(await res.arrayBuffer());
  registerResourceImage(fileId, await createImageBitmap(new Blob([bytes])));
  (loadImage.bytes ??= new Map()).set(fileId, bytes);
}

/** Registers SVG markup as a vector image, its fonts inside (inlineSvgFonts, ≥ 1.25).
 *  `options` go to registerResourceImage: { singleInk: true } tints it on the canvas. */
async function loadSvg(fileId, svg, options) {
  if (typeof inlineSvgFonts === 'function') svg = await inlineSvgFonts(svg, svgFaces);
  const img = new Image();
  img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
  await img.decode();
  registerResourceImage(fileId, img, options);
  (loadImage.bytes ??= new Map()).set(fileId, new TextEncoder().encode(svg));
}

/** A face's Fontsource files: nearest weight; latin + what the SVG sets. */
async function svgFaces(family, weight, style, { codePoints = [] } = {}) {
  const [id, m] = [fontsourceId(family), await fontsourceMeta(family)];
  const w = m?.weights.reduce((a, b) => (Math.abs(b - weight) < Math.abs(a - weight) ? b : a)) ?? weight;
  const s = m?.styles.includes(style) === false ? 'normal' : style;
  return Promise.all(['latin', ...kitSubsetsFor(String.fromCodePoint(...codePoints), m)].map(async (x) => new Uint8Array(
    await (await fetch(`${await fontsourceAt(id)}files/${id}-${x}-${w}-${s}.woff2`)).arrayBuffer())));
}

/** renderToPdf({ resourceBytes: imageBytes }) */
function imageBytes(fileId) { return loadImage.bytes?.get(fileId); }

/** renderToHtml({ resourceImageUrl: imageUrl }) */
function imageUrl(fileId) {
  const bytes = imageBytes(fileId);
  if (!bytes) return undefined;
  imageUrl.urls ??= new Map();
  if (!imageUrl.urls.has(fileId)) {
    const type = /\.svg$/i.test(fileId) ? 'image/svg+xml' : /\.png$/i.test(fileId) ? 'image/png' : 'image/jpeg';
    imageUrl.urls.set(fileId, URL.createObjectURL(new Blob([bytes], { type })));
  }
  return imageUrl.urls.get(fileId);
}

// ─── /Kit ───────────────────────────────────────────────────────────────────────
```

## Varianti

### Arrotonda le estremità delle fasce

Un raggio di un quinto del corpo dà le estremità morbide di un pennarello.

```diff
-  // borderRadius: 0 is the default (square ends, as a highlighter pen leaves them)
+  borderRadius: em(0.2), // soft ends, as a felt-tip pen leaves them
```

### Dai a ogni riga la sua fascia

Con `decorationBreak: 'clone'`, un segno che va a capo riceve il margine alle due estremità di ogni pezzo, come a pagina 3, dove «pamphlets» si spezza dopo «pam-».

```diff
   paddingX: em(0.12), // the band reaches a little past the first and the last letter
+  decorationBreak: 'clone', // each line piece gets a whole band
```

## Errori frequenti

- Il colore da solo non sopravvive a una fotocopia in bianco e nero, che è il modo in cui molte letture arrivano in classe. Il glossario mette in più i termini in neretto; se i tuoi lettori fotocopieranno le pagine, dai a uno dei tre tipi un secondo segno tutto suo.
- Un id di stile che `markStyles` non dichiara prende il primo stile, senza errori sulla pagina (il Sandbox lo segnala). Usa id brevi e controlla come li scrivi nel testo.

## Crediti

- Ricetta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Testo: The reading, its key, the review questions and the colophon: Ignacio Ferro, CC-BY-4.0
- Immagini: The printing workshop on the opening page, a painting: Generated With Diffusion Models, originale
- Caratteri: Source Serif 4 (OFL-1.1), Source Sans 3 (OFL-1.1)
- Codice: MIT · Contenuto di esempio: CC-BY-4.0

## Ricette correlate

- [Nº 076 · Un sillabario in pinyin: la lettura sopra ogni carattere](https://postext.dev/it/cookbook/pinyin-primer.md): Ruby mono in un primo libro di lettura di Hong Kong: {人之初|rén zhī chū} centra una sillaba pinyin su ogni carattere, in Andika, nei 28 pt tra le righe. · Livello 2 (Intermedio) · Libri di testo, Quaderni ed eserciziari
- [Nº 163 · Un manuale di armonia con esempi musicali in ABC](https://postext.dev/it/cookbook/harmony-textbook-abc-examples.md): Un capitolo sulle cadenze i cui esempi sono blocchi ABC nel Markdown, incisi alla larghezza della colonna; uno di essi prosegue su due pagine. · Livello 2 (Intermedio) · Libri di testo
- [Nº 122 · Kanbun con i segni di lettura e la lettura giapponese](https://postext.dev/it/cookbook/kanbun-kundoku.md): Tre detti dei Dialoghi e una quartina Tang composti alla giapponese: segni di ritorno e okurigana in vermiglione, e la lettura in kana sotto ogni passo. · Livello 2 (Intermedio) · Libri di testo
