# Configurazione: uso programmatico

> Chiamare Postext dal codice: buildDocument, i Web Worker, il visualizzatore HTML, i file PDF, il libro 3D, l’EPUB e i pacchetti .postext

- Versione HTML: https://postext.dev/it/docs/configuration-programmatic-usage
- Ultimo aggiornamento: 2026-10-10
- Tempo di lettura: 8 min
- Altre lingue: [en](https://postext.dev/en/docs/configuration-programmatic-usage.md), [es](https://postext.dev/es/docs/configuration-programmatic-usage.md), [ca](https://postext.dev/ca/docs/configuration-programmatic-usage.md), [de](https://postext.dev/de/docs/configuration-programmatic-usage.md), [fr](https://postext.dev/fr/docs/configuration-programmatic-usage.md), [pt](https://postext.dev/pt/docs/configuration-programmatic-usage.md), [zh](https://postext.dev/zh/docs/configuration-programmatic-usage.md), [ja](https://postext.dev/ja/docs/configuration-programmatic-usage.md), [ar](https://postext.dev/ar/docs/configuration-programmatic-usage.md)

## In breve

Questa pagina è per chi scrive codice. Mostra come costruire un libro con una sola chiamata di funzione e come leggere gli avvisi che segnala. Spiega come eseguire il lavoro in background, così la pagina resta reattiva. Mostra come produrre una vista web, un file PDF, un libro 3D e un e-book EPUB. L’ultima sezione spiega il file che contiene un libro intero con i suoi font e le sue immagini.

## Uso programmatico

> **Percorso consigliato: usa il Web Worker.** In un browser, la stragrande maggioranza delle integrazioni dovrebbe guidare la pipeline di impaginazione tramite `createLayoutWorker()` da `postext/worker`, **non** chiamando `buildDocument` direttamente sul thread principale. Il worker mantiene reattiva l’interfaccia durante le generazioni, mette in cache le misure del testo tra una rigenerazione incrementale e l’altra e predispone l’annullamento in cui vince l’ultima richiesta, così un nuovo tasto premuto interrompe qualsiasi generazione superata ancora in corso. Vai direttamente a [Eseguire l’impaginazione in un Web Worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#eseguire-limpaginazione-in-un-web-worker) per la ricetta canonica. Tutto il resto di questa sezione (`buildDocument` diretto, resolver, funzioni di pulizia, cache) resta utile, perché il worker espone esattamente gli stessi input e output, ma per il codice dell’interfaccia il wrapper del worker è il punto di partenza giusto. Ripiega sulla chiamata di `buildDocument` nel thread principale solo per esportazioni una tantum, rendering lato server (Node) o test.

### Costruire un documento

La funzione `buildDocument` esegue l’intera pipeline di impaginazione e restituisce un albero virtuale del documento (VDT) con coordinate precise per ogni elemento. È il punto d’ingresso di livello più basso; il codice dell’interfaccia dovrebbe preferire il [wrapper Web Worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#eseguire-limpaginazione-in-un-web-worker), che chiama `buildDocument` dentro un thread worker dedicato con gli stessi argomenti.

```ts
import { buildDocument } from 'postext';

const content = {
  markdown: '# Chapter One\n\nThe story begins here...',
};

const config = {
  page: { sizePreset: '17x24' },
  layout: { layoutType: 'double' },
  bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt overrides the 8 pt default
};

// Build the layout — produces a VDT with one entry per page in `vdt.pages`
const vdt = buildDocument(content, config);
console.log(`Document has ${vdt.pages.length} pages`);
```

### Avvisi nel documento

`buildDocument` non si ferma davanti a un rimando sbagliato o a uno stile sconosciuto: imposta un ripiego e registra ciò che ha fatto in `doc.contentWarnings`. I riquadri che l’impaginazione ha dovuto forzare sono in `doc.warnings`, che mantiene la forma che aveva in postext 1.4: ogni voce lì è un `calloutOverflow` con i suoi `pageIndex`, `columnIndex` e `overflowPx`. Ogni campo è assente quando non c’è nulla da segnalare. Ogni voce ha un `kind`. I tipi di contenuto portano l’intervallo di sorgente del costrutto (`sourceStart` / `sourceEnd`, offset nel Markdown che hai passato, frontmatter compreso) e, quando il costrutto è finito su una pagina, il suo `pageIndex`.

| Tipo | Segnalato quando | Che cosa fa l’output |
| --- | --- | --- |
| `calloutOverflow` | Un riquadro `:::callout` non entra in nessuna colonna e nessun taglio può dividerlo. Lo è anche un riquadro `span: 'side'` più alto di una colonna laterale vuota (da postext 1.25). | Viene collocato comunque, sbordando dalla sua colonna di `overflowPx` (su `pageIndex` / `columnIndex`). È l’unico tipo elencato in `doc.warnings`; i tipi seguenti sono in `doc.contentWarnings`. |
| `invalidFrontmatter` | Il frontmatter non è YAML valido (una virgoletta non chiusa, del testo dopo un valore tra virgolette). `message` è il motivo indicato dal parser, con riga e colonna. | Il documento viene composto senza i suoi metadati; il corpo dopo il `---` di chiusura viene impaginato come al solito. |
| `unknownResourceId` | Un’incorporazione `::resource` (`usage: 'embed'`), un `:ref` in linea (`'ref'`) o l’immagine di una cella di tabella (`'cellImage'`) indica un id che nessuna risorsa ha. | L’incorporazione viene omessa, il rimando stampa `?` (o la sua etichetta `text=`) senza numero né link, la cella resta solo testo. `inResource` indica la risorsa la cui didascalia, nota o cella contiene il rimando. |
| `unknownDirective` | Una riga `:::name` il cui nome non è né una direttiva né un contenitore. | La riga viene composta come testo. |
| `malformedEmbed` | Una riga `::name` che non è un’incorporazione ben formata e isolata: `::resource` con un id senza virgolette o tra virgolette semplici o con un altro attributo, oppure una riga attaccata sotto un paragrafo senza una riga vuota. | La riga viene composta come testo. |
| `fullwidthMarkup` | Una riga contiene marcatura digitata con un metodo di input cinese o giapponese: una recinzione `：：：`, un titolo `＃`, un richiamo di nota `［＾…］`, attributi `｛…｝` dopo una recinzione o un titolo, oppure un grassetto `＊＊…＊＊`. `typed` è la marcatura così come è scritta, `ascii` la forma da digitare. Uno per riga. | La riga viene composta come testo; non viene convertito nulla. |
| `attributeKeyInvalid` | La chiave di un attributo contiene lettere non ASCII (`作者=曹雪芹`); punta alla chiave. | L’attributo viene ignorato. |
| `unknownParagraphStyle` | `:::paragraphs{style}` non indica alcuno stile di paragrafo. | I paragrafi vengono composti come testo corrente. |
| `unknownCalloutType` | `:::callout{type}` non indica nessuno dei `calloutStyles`; viene segnalato solo quando ne è configurato almeno uno. | Il riquadro prende il primo stile di riquadro. |
| `columnsFlowUnknown` | `:::columns{flow}` non è né `snake` né `parallel`; `value` è il valore indicato. | Il gruppo prende il valore predefinito: `parallel` con `breaks`, `snake` senza. |
| `unknownChipStyle` | `:chip[…]{style}` non indica alcuno stile di chip. | Il chip prende il primo stile di chip. |
| `undefinedFootnote` | Un richiamo di nota `[^id]` che nessun paragrafo `[^id]:` definisce (`id` è quello della nota). | Il numero viene stampato; la nota è vuota. |
| `unusedFootnote` | Una definizione di nota `[^id]:` che nessun richiamo cita. | La nota non viene composta. |
| `indexMarkInvalid` | Un segno d’indice senza termine: `:index{}`, oppure attributi senza `term` su un segno senza testo tra parentesi quadre. | Il segno non indicizza nulla. |
| `indexSeeUnknown` | Una destinazione `see` o `seealso` (`target`) che non è una voce del suo indice (`index`, `''` per quello principale). Punta alla riga `:::index`. | Il rimando viene stampato comunque. |
| `indexRangeUnclosed` | Un segno `range="start"` senza il corrispondente `range="end"`, o viceversa (`missing` dice quale estremo manca; `term` indica la voce). Punta alla riga `:::index`. | L’intervallo stampa la sua unica pagina. |
| `unknownHeadingStyle` | Il `{style="…"}` di un titolo non indica alcuno stile di titolo (`level` è quello del titolo). | Il titolo e la sua sezione mantengono le impostazioni proprie del livello. |
| `unknownTableStyle` | Il `table.styleId` di una risorsa tabella non indica alcuna voce di `tableStyles`. | La tabella viene composta con `tableStyle`. |
| `raggedTableGrid` | La griglia di una tabella non è rettangolare una volta contate le unioni (vedi [Costruire i modelli di tabella](https://postext.dev/it/docs/configuration-resources.md#costruire-i-modelli-di-tabella)). | Le celle slittano su un’unione o lasciano un buco. `reason` (`'spanOverlap'` / `'missingCells'`), `row` e `col` individuano il primo problema; `count` dice quanti sono. |
| `lineNumberOverlap` | Con `lineNumbers.position: 'side'`, un numero di riga si sovrappone a un riquadro laterale, a una didascalia laterale o a una figura nella colonna laterale. Punta alla riga numerata; `number` è il numero come viene stampato. | Il numero viene disegnato comunque, e nessuno dei due si sposta. |
| `dropCap` | Un paragrafo aperto da un [capolettera](https://postext.dev/it/docs/configuration-text.md#capilettera) che non può accoglierlo come configurato. `reason`: `'shortParagraph'` (meno righe di quelle su cui scende l’iniziale; `handling` è ciò che ha fatto `shortParagraph`, `lines` le righe occupate da un’iniziale ridotta), `'split'` (si interrompe prima dell’ultima riga dell’iniziale, da solo in una colonna troppo corta), `'joiningScript'` (la sua prima lettera si lega alla successiva), `'verticalText'` o `'noLetter'` (si apre con un rimando, una formula o un richiamo di nota). `text` è la sua prima riga. | Lo spazio viene mantenuto, l’iniziale si riduce o viene omessa, come dice l’avviso. |
| `codeOverflow` | Un [listato di codice](https://postext.dev/it/docs/configuration-styles.md#listati-di-codice) ha righe più larghe del suo riquadro. `mode` è ciò che ha fatto `codeStyle.overflow` (`'wrap'`, `'shrink'`, `'clip'`), `lines` quante righe di sorgente erano troppo larghe, `scale` il corpo con cui è stato composto un listato ridotto (una frazione di `fontSize`), `lang` il linguaggio della recinzione. Punta al listato. | Le righe vanno a capo, vengono rimpicciolite o tagliate, come dice `mode`. |
| `floatShrunk` | Un’immagine flottante è stata composta più piccola della sua dimensione per entrare nello spazio disponibile (`placement.shrink`). `resourceId` la indica, `scale` è la frazione della sua larghezza che mantiene, e `overflowPx`, quando presente, quanto sborda ancora oltre il piede della gabbia del testo alla scala minima (`placement.minScale`), su una pagina nuova dove non aveva altro posto. Punta al paragrafo che la cita per primo. | L’immagine viene stampata a quella scala; oltre la gabbia del testo solo quando lo dice `overflowPx`. |
| `textWrap` | Una risorsa o un riquadro impostato per far scorrere il testo attorno (`placement.wrap`, il `wrap` di un riquadro) che non lo fa come richiesto. `reason`: `'tooNarrow'` (il testo accanto sarebbe più stretto di `layout.wrap.minTextWidth`), `'fewLines'` (è più corto di `layout.wrap.minLinesBeside` righe), `'moved'` (uno nel testo troppo alto per lo spazio rimasto nella sua colonna è passato alla successiva, insieme alla sua ancora) o `'verticalText'`. `resourceId` indica una risorsa, `box` lo stile di un riquadro. Punta all’incorporazione o al riquadro. | L’elemento occupa la sua fascia per intero, o si colloca nella colonna successiva, come dice il motivo. |
| `columnsTooNarrow` | Le sottocolonne di un gruppo `:::columns` sono più strette di sei em del loro testo: `columns` sottocolonne, `widthPx` ciascuna. Punta alla recinzione del gruppo. | Il gruppo viene composto come richiesto, con poche parole per riga. |
| `afterText` | Un riquadro `span: 'side'`, o una figura o tabella della colonna laterale (`resourceId`), composto su una pagina che non contiene testo: il testo del suo capitolo, o del documento, è finito mentre aspettava ancora spazio nella colonna laterale. Uno per riquadro o elemento flottante; punta al riquadro (al blocco che cita l’elemento flottante), con la sua pagina. Da postext 1.25. | Si colloca nella colonna laterale di una pagina aperta dopo il testo, con i riquadri nell’ordine delle loro recinzioni. |
| `unplaced` | Un riquadro o una risorsa flottante (`resourceId`) ancora in attesa di uno spazio quando l’impaginazione è finita: le pagine aperte per lui non potevano accoglierlo (per esempio un riquadro laterale dove quelle pagine non hanno una colonna laterale). Punta al riquadro o al blocco che cita la risorsa; nessuna pagina. Da postext 1.25. | Non si trova su nessuna pagina. Fino a postext 1.24 spariva senza avviso. |
| `shortListSplit` | Un elenco di `items` voci, breve secondo `bodyText.shortListMaxItems`, tagliato tra due colonne o pagine. Con `bodyText.shortListSplit: 'warn'` viene segnalato ogni elenco del genere; con `'avoid'`, solo quello che nessuna colonna poteva contenere intero. Uno per elenco; punta all’elenco, con la pagina della sua prima voce. Da postext 1.28. | L’elenco viene tagliato dove finisce la colonna. |
| `openerFloat` | Un elemento flottante composto su una pagina d’apertura con `layout.openerFloats: 'warn'`: una figura, una tabella o una striscia (`resourceId`), o un riquadro flottante. Uno per elemento flottante; punta al blocco che lo cita (al riquadro), con la sua pagina. Da postext 1.28. | L’elemento flottante resta sulla pagina d’apertura; `'avoid'` lo sposta nel primo spazio successivo. |
| `fontFallback` | Un carattere con cui è stato composto il testo (`family`, `weight`, `style`) che l’insieme dei font non poteva fornire al momento della generazione: `reason: 'missing'`, nessun carattere della famiglia era caricato né installato, oppure quello che risponde per quel peso e quell’inclinazione non era ancora caricato; `'synthesized'`, la famiglia non ha un carattere di quel peso o di quell’inclinazione e il browser lo prende da un altro, così com’è o disegnato più nero o inclinato (un 600 ricavato dal 700, un 700 corsivo chiesto a una famiglia che ha solo un 400 tondo). Viene controllato dove c’è un insieme di font (`document.fonts`, il `self.fonts` di un worker, o `BuildDocumentOptions.fontSet`), dietro `debug.warnings.missingFont`. Nessuna pagina e nessun intervallo di sorgente. | Il testo viene misurato e disegnato con il carattere di ripiego, o con un altro carattere della famiglia, così com’è o reso più nero o inclinato; i suoi a capo cambiano quando arriva il carattere. Vedi [Caricare i font prima dell’impaginazione](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione). |

Gli avvisi su una risorsa (il suo stile di tabella, la sua griglia, un rimando dentro la sua didascalia, nota o celle) puntano alla prima incorporazione o al primo rimando della risorsa nel testo, e ciascuno viene elencato una volta per risorsa. Si controllano solo le risorse che il documento usa: un capitolo di un libro segnala le tabelle che cita, non tutte le tabelle del libro.

```ts
import { buildDocument, formatWarning } from 'postext';

const doc = buildDocument({ markdown: 'See :ref{id="fig-map"}.\n\n:::sidebar\nNotes.' }, config);
for (const w of [...(doc.warnings ?? []), ...(doc.contentWarnings ?? []), ...(doc.configWarnings ?? [])]) console.warn(formatWarning(w));
// Unknown resource id "fig-map" in :ref — it prints "?" (or its text= label), with no number or link (page 1, offset 4)
// Unknown directive ":::sidebar" — the line is set as text (page 1, offset 25)

// Narrow on `kind` to read the fields of a kind.
const missing = (doc.contentWarnings ?? []).flatMap((w) => (w.kind === 'unknownResourceId' ? [w.resourceId] : []));
```

`formatWarning(w)` restituisce una descrizione di una riga in inglese. Un host che localizza i propri messaggi fa invece uno switch su `kind`, e tiene un ramo predefinito, perché le versioni minori possono aggiungere tipi. `collectContentWarnings(markdown, config, resources)` restituisce gli avvisi di contenuto senza impaginare nulla (l’elenco che aggiunge la generazione, senza `pageIndex`), per un editor che controlla il testo mentre viene digitato. `collectHeadingDesignCuts(doc)` controlla un’impaginazione finita alla ricerca di progetti grafici di titoli il cui testo sborda oltre il piede della loro pagina o colonna (`kind: 'headingDesignCut'`; vedi [Altezza riservata](https://postext.dev/it/docs/configuration-text.md#altezza-riservata)), che l’impaginazione stessa non segnala, e `formatWarning` descrive anche i suoi risultati. Il pannello **Verifiche** della Sandbox li elenca tutti.

I renderer segnalano ciò che non riescono a disegnare come richiesto tramite un’opzione `onWarning`: `renderPageToCanvas`, `renderPage` e `renderToCanvas` (`RenderPageOptions`), `renderToHtml` e `renderToHtmlIndexed` (`RenderHtmlOptions`), e `renderToPdf` (`RenderToPdfOptions`, anche tramite il worker PDF). Il tipo principale di avviso di rendering è `missingImage`: un’immagine (una figura, l’immagine di una cella di tabella, l’icona di un riquadro, un’immagine di design) senza nulla da disegnare viene resa come segnaposto neutro e segnalata, una volta per `fileId` e chiamata di rendering, con il suo `pageIndex`, il `resourceId` quando il renderer lo conosce (figure e immagini delle celle) e, nel PDF, il `documentIndex` di un rendering di più documenti. Nulla da disegnare significa nessun `registerResourceImage` per il `fileId` sul canvas, nessun URL da `resourceImageUrl` in HTML, e nessun byte da `resourceBytes` (o byte che non si decodificano) nel PDF. Una risorsa bitmap o SVG che non indica alcun `fileId` non ha nulla da chiedere: viene disegnata come segnaposto senza segnalazione. Altri due tipi vengono dagli host che incorporano i font nelle immagini SVG (`registerSvgImage`, `registerBundleImages`, `bundleImageUrl`, `renderToHtml` con `inlineSvgFonts`, postext-epub; vedi [Font nel testo SVG](https://postext.dev/it/docs/configuration-resources.md#font-nel-testo-svg)), con il `fileId` e il `resourceId` dell’immagine: `svgFontUnavailable` (`family`, `weight`, `style`), una famiglia nominata dal suo testo senza un carattere da incorporare, per cui l’immagine compone quel testo con un carattere di ripiego; e `svgFontsTooLarge` (`bytes`, `maxBytes`), caratteri oltre il limite di dimensione, nessuno incorporato. Gli avvisi di rendering non vengono memorizzati nel VDT: ciò che un host può fornire cambia dopo l’impaginazione.

```ts
import { buildDocument, renderPage, type RenderWarning, type Resource } from 'postext';

const map: Resource = {
  id: 'fig-map', typeId: 'figure', kind: 'bitmap', caption: 'The route.', createdAt: 0, updatedAt: 0,
  bitmap: { fileId: 'map-file', format: 'png', width: 1200, height: 800 },
};
const doc = buildDocument({ markdown: 'See :ref{id="fig-map"}.', resources: [map] }, config);

const warnings: RenderWarning[] = [];
const canvas = renderPage(doc.pages[0], doc, { onWarning: (w) => warnings.push(w) });
// Until 'map-file' is registered with registerResourceImage:
// [{ kind: 'missingImage', fileId: 'map-file', resourceId: 'fig-map', pageIndex: 0 }]
```

### Rendere una pagina come bitmap

Ogni pagina può essere rasterizzata in modo indipendente. Usa `renderPage(page, doc)` per ottenere un `HTMLCanvasElement` per un dato numero di pagina: il canvas è una bitmap dimensionata esattamente sulle misure della pagina in pixel (ai DPI configurati), quindi puoi mostrarla, esportarla o passarla a qualsiasi pipeline di immagini:

```ts
import { buildDocument, renderPage } from 'postext';

const vdt = buildDocument(content, config);

// Render page 3 (zero-indexed) to a bitmap canvas
const pageNumber = 2;
const page = vdt.pages[pageNumber];
if (!page) throw new Error(`Page ${pageNumber} does not exist`);

const canvas = renderPage(page, vdt);
// canvas.width / canvas.height are the page bitmap size in pixels

// Show it in the DOM
document.body.appendChild(canvas);

// …or export it as a PNG data URL
const pngDataUrl = canvas.toDataURL('image/png');

// …or get a Blob for download / upload
canvas.toBlob((blob) => {
  if (blob) saveAs(blob, `page-${pageNumber + 1}.png`);
}, 'image/png');

// …or grab raw RGBA pixels
const ctx = canvas.getContext('2d')!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
```

Se preferisci disegnare in un canvas che hai già (per esempio uno inserito nel DOM con una disposizione precisa), usa `renderPageToCanvas(page, doc, canvas)`: ridimensiona e disegna nel canvas che passi, invece di crearne uno nuovo.

Per rendere tutte le pagine, itera su `vdt.pages`:

```ts
const bitmaps = vdt.pages.map((page) => renderPage(page, vdt));
```

#### Esempio dal vivo: una pagina come immagine

Tutto quanto sopra, in esecuzione nel browser. Il pen importa l’ultima versione di `postext` da una CDN, aspetta i web font, impagina un breve documento a due colonne, disegna la sua prima pagina in un canvas e offre quella bitmap come PNG. Premi *Esegui su CodePen* per caricare l’editor e modificare il Markdown o la configurazione; la pagina si ridisegna a ogni modifica.

> **Esempio eseguibile: Postext · rendere una pagina come immagine** — Impagina un documento Markdown con postext e rasterizza la sua prima pagina in un canvas / PNG. ([codice](https://github.com/drnachio/postext/tree/main/docs/examples/render-page))

### React

`postext/react` esporta `createLayout(content, config?)`: un componente che impagina il documento una volta, al montaggio, e mostra ogni pagina come un `<canvas>` dentro un `<div>`.

```tsx
import { createLayout } from 'postext/react';

const Article = createLayout(
  { markdown: '# Hello\n\nThe first paragraph of the article.' },
  { page: { sizePreset: '17x24' } },
);

export function ArticlePage() {
  return <Article className="pages" style={{ maxWidth: 480 }} />;
}
```

- **Thread principale, una volta sola.** Le pagine vengono disegnate alla risoluzione del documento e scalate alla larghezza del contenitore. `content` e `config` sono fissati quando chiami `createLayout`; crea un altro componente per mostrare qualcos’altro. Per un’anteprima dal vivo, genera nel [Web Worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#eseguire-limpaginazione-in-un-web-worker) e disegna con `renderPageToCanvas`, come nell’esempio React di quella sezione.
- **Prima i font e le immagini.** Carica i web font del documento prima che il componente venga montato, e registra le sue immagini con `registerResourceImage`. Quando il Markdown contiene un `$`, il componente avvia da solo il [motore matematico](https://postext.dev/it/docs/configuration-text.md#avviare-il-motore-matematico).
- **React resta fuori dal punto d’ingresso principale.** `postext` non importa mai React; lo fa solo `postext/react`. `createLayout` è ancora esportato da `postext` perché il codice esistente continui a funzionare, ma è deprecato: carica `postext/react` quando lo chiami, e il componente resta sospeso finché quel modulo non è arrivato (React lo ridisegna da solo). Importalo da `postext/react`.
- **Il componente deprecato si sospende.** Finché `postext/react` non è arrivato, `createLayout` da `postext` ha bisogno di una root concorrente (`createRoot`) o di un confine `<Suspense>` sopra di sé. In una root legacy `ReactDOM.render`, o in `renderToString`, senza confine, React segnala invece un errore. `react` resta una peer dependency obbligatoria, così i bundler possono risolvere quell’import pigro.

### Risolvere i valori predefiniti

Le funzioni di risoluzione completano con i valori predefiniti gli oggetti di configurazione parziali. Sono utili quando ti serve una configurazione completa da ispezionare o confrontare:

```ts
import { resolvePageConfig, resolveBodyTextConfig } from 'postext';

const fullPage = resolvePageConfig({ sizePreset: '21x28' });
// => { sizePreset: '21x28', width: { value: 21, unit: 'cm' }, height: { value: 28, unit: 'cm' },
//      margins: { top: { value: 2, unit: 'cm' }, ... }, dpi: 300, cutLines: { enabled: false, ... }, ... }

const fullBody = resolveBodyTextConfig({ fontFamily: 'Inter' });
// => { fontFamily: 'Inter', fontSize: { value: 8, unit: 'pt' }, lineHeight: { value: 1.5, unit: 'em' }, ... }
```

Funzioni di risoluzione disponibili, una per sezione di primo livello: `resolvePageConfig`, `resolveLayoutConfig`, `resolveBodyTextConfig`, `resolveHeadingsConfig`, `resolveHeadingStylesConfig`, `resolveTocConfig`, `resolvePartsConfig`, `resolveUnorderedListsConfig`, `resolveOrderedListsConfig`, `resolveMathConfig`, `resolveTableStyleConfig`, `resolveCaptionStyleConfig`, `resolveDiagramStyleConfig`, `resolveParagraphStylesConfig`, `resolveCalloutStylesConfig`, `resolveHeaderFooterConfig`, `resolveDebugConfig`, `resolveHtmlViewerConfig`, `resolvePdfGenerationConfig`, più `resolveDesignSlot` per un singolo slot di design. Le tavolozze di colori si applicano a parte tramite `applyPaletteToConfig(config)`, `applyPaletteToResolvedConfig(resolved, palette)` e `resolveColorValue(value, palette, fallback)`: vedi [Tavolozza dei colori](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#tavolozza-dei-colori).

Le funzioni di risoluzione i cui valori predefiniti derivano a cascata da un’altra sezione ricevono quella sezione, già risolta, come argomento aggiuntivo. `resolveUnorderedListsConfig` e `resolveOrderedListsConfig` ricevono il testo corrente risolto, perché i valori predefiniti degli elenchi per `fontFamily` e `color` derivano da esso; `resolveCalloutStylesConfig` riceve il testo corrente, i titoli e gli elenchi puntati risolti (vedi l’esempio in [Stili dei riquadri](https://postext.dev/it/docs/configuration-styles.md#stili-dei-riquadri)), e `resolveHeadingStylesConfig` la pagina, il testo corrente ed entrambe le sezioni degli elenchi risolti. Controlla le dichiarazioni di tipo del pacchetto per la firma esatta di ciascuna:

```ts
import { resolveBodyTextConfig, resolveUnorderedListsConfig } from 'postext';

const body = resolveBodyTextConfig({ fontFamily: 'Inter' });
const lists = resolveUnorderedListsConfig({ bulletChar: '—' }, body);
// => lists.fontFamily === 'Inter' (inherited)
```

Vengono esportati anche gli insiemi statici di valori predefiniti, cioè i valori usati quando non entra in gioco alcuna cascata: `DEFAULT_PAGE_CONFIG`, `DEFAULT_CUT_LINES`, `DEFAULT_PAGE_NUMBERING`, `PAGE_SIZE_PRESETS`, `DEFAULT_LAYOUT_CONFIG`, `DEFAULT_COLUMN_RULE`, `DEFAULT_COLUMN_BALANCING`, `DEFAULT_BODY_TEXT_CONFIG`, `DEFAULT_HYPHENATION_CONFIG`, `DEFAULT_HEADINGS_CONFIG`, `DEFAULT_UNORDERED_LISTS_STATIC`, `DEFAULT_ORDERED_LISTS_STATIC`, `DEFAULT_PARAGRAPH_STYLES`, `DEFAULT_CALLOUT_STYLES`, `DEFAULT_CALLOUT_STYLE_STATIC`, `DEFAULT_PARTS_CONFIG`, `DEFAULT_HEADING_STYLES`, `DEFAULT_TOC_CONFIG`, `DEFAULT_MATH_CONFIG`, `DEFAULT_DIAGRAM_STYLE_CONFIG`, `DEFAULT_DEBUG_CONFIG`, `DEFAULT_HTML_VIEWER_CONFIG`, `DEFAULT_PDF_GENERATION_CONFIG`, `DEFAULT_COLOR_PALETTE`, `DEFAULT_MAIN_COLOR`, `DEFAULT_MAIN_COLOR_ID`, `DEFAULT_MAIN_COLOR_NAME`, `DEFAULT_MAIN_COLOR_HEX`, più i valori predefiniti degli elementi di intestazione e piè di pagina (`DEFAULT_HEADER_FOOTER_SLOT`, `DEFAULT_HEADER_SLOT`, `DEFAULT_FOOTER_SLOT`, `DEFAULT_TEXT_ELEMENT`, `DEFAULT_RULE_ELEMENT`, `DEFAULT_BOX_ELEMENT`) e `defaultResourceTypes(locale)`, che tiene conto della lingua (vedi [Tipi di risorsa](https://postext.dev/it/docs/configuration-resources.md#tipi-di-risorsa)).

### Eliminare i valori predefiniti

Quando salvi una configurazione (ad esempio in localStorage o in un file), usa `stripConfigDefaults` per eliminare i valori che coincidono con quelli predefiniti. Così le configurazioni salvate restano minime: si salvano solo le sostituzioni volute.

```ts
import { stripConfigDefaults } from 'postext';

const minimal = stripConfigDefaults(fullConfig);
// Only properties that differ from defaults remain
```

Sono disponibili anche le singole funzioni di pulizia, una per funzione di risoluzione: `stripPageDefaults`, `stripLayoutDefaults`, `stripBodyTextDefaults`, `stripHeadingsDefaults`, `stripHeadingStylesDefaults`, `stripTocDefaults`, `stripPartsDefaults`, `stripUnorderedListsDefaults`, `stripOrderedListsDefaults`, `stripMathDefaults`, `stripTableStyleDefaults`, `stripCaptionStyleDefaults`, `stripDiagramStyleDefaults`, `stripParagraphStylesDefaults`, `stripCalloutStylesDefaults`, `stripHeaderFooterDefaults`, `stripDesignSlotDefaults`, `stripDebugDefaults`, `stripHtmlViewerDefaults`, `stripPdfGenerationDefaults`.

Alcuni valori predefiniti dipendono dal resto della configurazione: il bilanciamento delle colonne è disattivato su una griglia di caratteri e nel testo verticale, `bodyText.avoidWidows` è disattivato su una griglia di caratteri (e anche `bodyText.avoidOrphans` nella regione `japan`), e le note a piè di pagina, le didascalie e l’indice analitico seguono la lingua del documento. `stripConfigDefaults` confronta ogni valore con il predefinito della configurazione che riceve, così `headings.balancing.enabled: true` resta dove il bilanciamento è disattivato per impostazione predefinita, e lì `false` viene eliminato. Allo stesso modo `bodyText.avoidWidows: true` resta su una griglia di caratteri. Una funzione di pulizia chiamata da sola riceve quel contesto come argomenti: `stripHeadingsDefaults(headings, balancingOnByDefault(config))`, `stripIndexDefaults(index, locale)`, `stripCaptionStyleDefaults(captionStyle, locale)`, `stripFootnotesDefaults(footnotes, locale, writingMode)`, `stripBodyTextDefaults(bodyText, locale, avoidWidowsByDefault(config), avoidOrphansByDefault(config))`.

Un valore che dice «niente» viene mantenuto ovunque il niente non sia il predefinito. Uno stile di titolo prende l’intestazione e il piè di pagina del documento quando non ne imposta, così uno stile che li svuota (`footer: { elements: [] }` su una copertina) mantiene lo slot vuoto, e i suoi `margins`, `layout` e `bodyStyle` vengono mantenuti anche quando sono vuoti. Un livello di titolo riportato al suo valore predefinito sotto valori comuni a tutti i titoli che sono diversi mantiene il valore. `calloutStyles: []` e `chipStyles: []` restano elenchi vuoti: se venissero omessi, tornerebbe lo stile integrato. In ogni caso `resolveAllConfig(stripConfigDefaults(config))` si risolve come `resolveAllConfig(config)`.

### Parsing

Il motore espone il proprio tokenizzatore Markdown e il lettore del frontmatter. Usali per ispezionare un documento prima di generarlo, o per alimentare altri strumenti con la stessa struttura a blocchi che vede Postext:

```ts
import { parseMarkdown, extractFrontmatter } from 'postext';

const source = '---\ntitle: Chapter One\n---\n\n# Opening\n\nThe story begins here.';

const { metadata, content } = extractFrontmatter(source);
// metadata.title === 'Chapter One'

const blocks = parseMarkdown(content);
// => [ { type: 'heading', level: 1, text: 'Opening', … },
//      { type: 'paragraph', text: 'The story begins here.', … } ]
```

Vedi la pagina [Formato del documento](https://postext.dev/it/docs/document-format.md) per l’elenco completo dei costrutti Markdown che Postext riconosce.

### Caricare i font prima dell’impaginazione

L’impaginazione misura il testo con i caratteri che l’insieme dei font ha nel momento in cui viene eseguita. `prepareFonts` carica prima della prima generazione ogni carattere richiesto da una configurazione e dal suo testo: testo corrente, titoli, elenchi, titoli e corpi dei riquadri, tabelle, testi di design, testatine, indice, codice e lettering dei fumetti, in ogni peso e inclinazione che la configurazione imposta (i quattro della famiglia del testo corrente, perché `**` e `*` vi compongono grassetto e corsivo). Ogni carattere viene caricato per i caratteri che il documento compone, così una famiglia servita a fette `unicode-range` (Latin Extended, greco, arabo, le fette CJK di Google Fonts e Fontsource) porta i file di cui il testo ha bisogno.

```ts
import { prepareFonts, buildDocument, buildDocumentWithFonts } from 'postext';

// The host's files: the PDF font provider's contract, so one function serves both.
async function resolve(family: string, weight: number, style: 'normal' | 'italic') {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  return `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}-${style}.woff2`;
}

const report = await prepareFonts(content, config, { resolve });
// report.loaded, report.missing, report.synthesized: { family, weight, style }[]
const doc = buildDocument(content, config);

// Or in one call: prepare, build, load any face the pages used that was not there, build again.
const same = await buildDocumentWithFonts(content, config, { resolve });
```

- **I caratteri che la pagina dichiara** (una regola `@font-face`, un `FontFace` già aggiunto) vengono caricati tramite l’insieme dei font (`document.fonts.load`, o `self.fonts` in un worker). **I caratteri che non dichiara** vengono chiesti a `resolve(family, weight, style, { text, codePoints })`, che risponde con un file (byte o un URL), più file (le fette di un carattere), `{ source, unicodeRange, weight, style }` per una fetta o un intervallo variabile, oppure `null`. Il motore li aggiunge come `FontFace` una volta caricati tutti, nell’ordine della configurazione e di ogni risposta (latin, poi latin-ext, poi greek quando il resolver risponde così), così l’insieme dei font è lo stesso in qualunque ordine arrivino i file. Li registra anche nel registro dei font che leggono le [immagini SVG](https://postext.dev/it/docs/configuration-resources.md#font-nel-testo-svg) e i worker di impaginazione. Un resolver può dichiarare il carattere da sé (aggiungendo un foglio di stile) e rispondere `null`.
- **Il resoconto** elenca i caratteri a cui risponde un carattere caricato (o una famiglia installata), quelli ancora `missing`, e quelli che il browser ricaverebbe (`synthesize`) da un altro peso o inclinazione. `timeoutMs` (10.000 per impostazione predefinita) limita l’attesa; un carattere ancora in caricamento a quel punto conta come mancante. Dove non c’è un insieme di font (Node), `prepareFonts` non fa nulla e segnala tutti i caratteri come caricati.
- **`buildDocumentWithFonts(content, config, options)`** prepara, genera con `buildDocumentAsync`, legge i caratteri con cui le pagine compongono effettivamente il testo, carica quelli che l’insieme dei font non poteva fornire (un peso che solo le pagine rivelano viene chiesto a `resolve` anche quando un altro peso della famiglia risponderebbe per lui) e genera di nuovo (al massimo due generazioni in più). `withLoadedFonts(build, options)` fa lo stesso attorno a qualsiasi funzione di generazione, per un libro generato capitolo per capitolo o per un pacchetto (`buildBundle`) che restituisce più documenti. `options.onFonts` riceve il resoconto finale.
- **Dopo la generazione**, ogni carattere con cui è stato composto il testo e che l’insieme dei font non poteva fornire viene elencato in `doc.contentWarnings` come `fontFallback` (vedi [Avvisi nel documento](https://postext.dev/it/docs/configuration-programmatic-usage.md#avvisi-nel-documento)).

I caratteri che arrivano dopo vengono rilevati da soli. Il motore tiene traccia, per ogni insieme di font, di quali caratteri di ogni famiglia erano caricati l’ultima volta che ha guardato; una generazione guarda di nuovo all’inizio (solo quando l’insieme è cresciuto o si è ridotto, sta caricando o ha finito di caricare un carattere) e scarta ciò che era stato misurato nelle famiglie i cui caratteri sono cambiati. `watchFonts(fontSet)` fa lo stesso mentre i caratteri si caricano, al massimo una volta per frame di animazione per quante fette porti un’ondata, e `onFontsChanged(listener)` dice all’host quali famiglie sono cambiate, così può reimpaginare le pagine:

```ts
import { watchFonts, onFontsChanged } from 'postext';

const stop = watchFonts();                         // document.fonts by default
const off = onFontsChanged((families) => relayout());
```

`prepareFonts` avvia il controllo sull’insieme in cui carica (`watch: false` lo lascia spento).

### Cache delle misure

La misurazione del testo è il passaggio costoso dell’impaginazione. Due tipi di cache la mantengono economica:

- **Una cache dei blocchi che gestisci tu.** `createMeasurementCache()` restituisce una `MeasurementCache` che ricorda ogni paragrafo misurato, con una chiave basata su testo, font, larghezza, opzioni di divisione in righe e dizionario di sillabazione attivo. Passala come terzo argomento di `buildDocument` (o di `buildDocumentAsync`) per riusare le misure tra i passaggi di collocazione di una generazione e tra una generazione e l’altra: un editor che impagina il documento a ogni tasto premuto misura allora solo i paragrafi cambiati. Senza cache, ogni passaggio misura di nuovo ogni blocco. Un paragrafo letto dalla cache è identico a uno misurato da capo, quindi una generazione con cache compone ogni riga come una generazione senza; in postext 1.4.1 un paragrafo in cache perdeva il contrassegno di ultima riga troppo corta, e la correzione delle righe corte e il bilanciamento delle colonne potevano allora dividerlo in modo diverso. La cache porta con sé la generazione di misure in cui è stata riempita: quando i caratteri di una famiglia arrivano o se ne vanno, la consultazione successiva scarta i blocchi composti in quella famiglia, così una cache conservata attraverso i caricamenti dei font non fornisce mai righe misurate con il carattere di ripiego.
- **Cache globali delle larghezze.** Le larghezze delle parole sono in cache per stringa di font nello stato del modulo che tutte le generazioni della pagina condividono, e pretext tiene una cache propria. Il motore scarta le larghezze di una famiglia quando i suoi caratteri cambiano (all’inizio di una generazione, da `watchFonts`, da `prepareFonts` e da `loadBundleFonts`); la cache di pretext non ha un indice per famiglia e viene svuotata per intero.

```ts
import { buildDocument, createMeasurementCache, evictFontFamilies, clearMeasurementCache } from 'postext';

const cache = createMeasurementCache();
let doc = buildDocument(content, config, cache);

// A face of "EB Garamond" was added to document.fonts: the next build sees it,
// with the same cache, and measures that family again.
doc = buildDocument(content, config, cache);

// A host that changes faces the engine cannot see (a font set of its own) says so:
evictFontFamilies(['EB Garamond']);   // that family's widths and cached blocks
clearMeasurementCache();              // every family
```

Per le applicazioni che misurano il testo pezzo per pezzo, `cachedMeasureBlock(text, font, maxWidthPx, lineHeightPx, options, cache)` e `cachedMeasureRichBlock(spans, normalFont, boldFont, italicFont, boldItalicFont, maxWidthPx, lineHeightPx, options, cache)` prendono gli argomenti di `measureBlock` e `measureRichBlock` più la cache, in ultima posizione.

### Il motore di divisione in righe (WebAssembly)

I paragrafi vengono divisi in righe con Knuth–Plass. Da postext 1.33 il motore lo esegue in un modulo di divisione in righe scritto in Rust e compilato in WebAssembly (30 KB, dentro il pacchetto). Sceglie le stesse divisioni del modulo TypeScript, con la stessa aritmetica nello stesso ordine, e un libro intero si impagina circa il 15 % più in fretta. Non c’è nulla da configurare:

- In un worker, in Node e in Bun, il modulo viene compilato al primo paragrafo.
- Nel thread principale di una pagina, dove un browser compila un modulo di queste dimensioni solo in modo asincrono, viene compilato in background e fino ad allora risponde il modulo TypeScript. `await initLineBreaker()` prima della prima impaginazione lo mette al lavoro dal primo paragrafo; si risolve con `false` dove WebAssembly non è disponibile.
- Dove WebAssembly manca, o una Content Security Policy lo rifiuta (`script-src` senza `'wasm-unsafe-eval'`), risponde a ogni paragrafo il modulo TypeScript.

Entrambi scelgono le stesse divisioni, quindi quale dei due abbia risposto non cambia mai un’impaginazione. `useWasmLineBreaker(false)` mantiene il modulo TypeScript, per confrontare i due.

```ts
import { initLineBreaker, buildDocument } from 'postext';

await initLineBreaker(); // optional: the first layout already uses it
const doc = buildDocument({ markdown }, config);
```

### Stato globale condiviso in una pagina

Parte dello stato di Postext vive in variabili a livello di modulo. Tutto ciò che importa `postext` nello stesso realm JavaScript lo condivide: una pagina e i suoi script condividono una copia, mentre ogni iframe e ogni worker ha la propria. Un solo documento per pagina non se ne accorge mai. Più documenti in una pagina (due anteprime dal vivo, una galleria di esempi) sì:

- **Immagini delle risorse.** `registerResourceImage(fileId, image)` riempie un unico registro con chiave `fileId`, che `renderPage` e `renderPageToCanvas` leggono. Due documenti che registrano entrambi `figure.svg` condividono quella voce: vince l’ultima registrazione, per tutti e due. Dai agli id dei file un prefisso per documento, e chiama `unregisterResourceImage(fileId)` o `clearResourceImages()` quando un documento viene rimosso. I raster che il backend canvas mette in cache hanno la stessa chiave e vengono scartati con l’immagine.
- **Misure del testo.** Le larghezze misurate sono in cache per stringa di font e testo per l’intero realm. Quando i caratteri di una famiglia arrivano o se ne vanno, il motore scarta le larghezze di quella famiglia per tutti i documenti (vedi [Caricare i font prima dell’impaginazione](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione)); `clearMeasurementCache()` le scarta tutte.
- **Configurazioni risolte.** Ogni oggetto di configurazione viene risolto una volta e il risultato viene messo in cache associato a quell’oggetto. Ogni generazione confronta prima l’oggetto con il testo che aveva quando è stato risolto (un `JSON.stringify`, circa 0,1 ms per una configurazione di libro di 55 KB e 1,5 ms per una di 240 KB), così una configurazione modificata sul posto, a qualsiasi profondità (`config.bodyText.fontSize = …`, un colore della tavolozza), viene risolta di nuovo. `invalidateConfig(config)` scarta la risoluzione a mano. `stableStringify` e `hashString` danno una chiave di contenuto che non dipende dall’ordine delle chiavi, per gli host che mettono in cache le impaginazioni per configurazione.
- **Lingua di sillabazione.** Ogni generazione imposta la lingua di sillabazione dell’intero processo sul `bodyText.hyphenation.locale` del proprio documento. Le funzioni esportate `hyphenateText(text)` e `layoutDesignSlot` usano la lingua dell’ultima generazione a meno che tu non ne passi una: chiama `hyphenateText(text, 'es')`.
- **Motore matematico.** C’è un solo motore MathJax e una sola cache di formule rese per il realm; `initMathEngine()` lo avvia per tutti.
- **Motore di divisione in righe.** C’è un solo modulo WebAssembly di divisione in righe per il realm, compilato una volta; `useWasmLineBreaker(false)` lo disattiva per tutti.

L’isolamento più semplice è un realm per documento: un iframe per esempio dal vivo (un embed di CodePen lo è), oppure un [worker di impaginazione](https://postext.dev/it/docs/configuration-programmatic-usage.md#eseguire-limpaginazione-in-un-web-worker) per documento per le misure e la sillabazione (le immagini vengono comunque registrate sulla pagina).

## Eseguire l’impaginazione in un Web Worker

**È il modo consigliato di usare Postext nel browser.** Se stai costruendo qualcosa di interattivo (un’anteprima dal vivo, un editor, un visualizzatore che reagisce al ridimensionamento o un ambiente di prova come la Sandbox), guida la pipeline tramite `createLayoutWorker()` da `postext/worker`. Non chiamare `buildDocument` direttamente sul thread principale nel codice dell’interfaccia.

Chiamare `buildDocument` sul thread principale esegue l’intera pipeline (parsing, misurazione, collocazione e ogni nuova esecuzione del passaggio di collocazione di cui l’impaginazione ha bisogno) sul thread che l’ha invocata. Per un’esportazione una tantum va bene. Per un’interfaccia interattiva è il thread sbagliato: un’impaginazione di 150 ms blocca gli eventi di input, i tasti premuti si accodano e lo scorrimento scatta. Il worker sposta ognuno di quei millisecondi su un thread in background.

Postext fornisce un punto d’ingresso Web Worker dedicato, `postext/worker`, che toglie la pipeline dal thread principale. È il percorso che ci aspettiamo usi la maggior parte delle integrazioni: le viste Canvas, HTML e PDF della Sandbox condividono tutte lo stesso handle `createLayoutWorker()` tramite un unico hook `useLayoutWorker` (`packages/postext-sandbox/src/worker/useLayoutWorker.ts`) e lo guidano con l’annullamento in cui vince l’ultima richiesta: un nuovo tasto premuto interrompe la generazione in corso prima ancora che finisca.

A colpo d’occhio, l’integrazione canonica è:

1. **Crea** un worker una volta per vista con `createLayoutWorker()`.
2. **Registra i font** una volta per famiglia inviando `ArrayBuffer` trasferibili tramite `registerFonts(payloads)`.
3. **Genera** con `build(content, config, { signal })`, passando un `AbortSignal` nuovo a ogni chiamata così le generazioni superate possono essere annullate.
4. **Sostituisci** qualsiasi generazione precedente annullandone il segnale *prima* di avviare la successiva: è lo schema in cui vince l’ultima richiesta.
5. **Elimina** il worker quando il componente che lo possiede viene smontato.

Lo stesso `VDTDocument` restituito da `build(...)` alimenta tutti i renderer a valle: `renderPage`/`renderPageToCanvas` per il canvas, `renderToHtmlIndexed` per l’HTML e `renderToPdf` (da `postext-pdf`) per il PDF. Generi una volta nel worker e rasterizzi sul thread principale tutte le volte che serve all’interfaccia.

### Che cosa ti dà il worker

- **Il thread principale resta libero.** Parsing, misurazione e ogni passaggio di collocazione vengono eseguiti dentro il worker. Il thread principale viene toccato solo quando il `VDTDocument` finito viene rispedito indietro.
- **Annullamento in cui vince l’ultima richiesta.** `build(content, config, { signal })` porta un `AbortSignal` dentro il worker. Annullare prima del completamento solleva un `AbortError` lato principale; dentro il worker la pipeline lancia un `BuildCancelledError` al successivo punto di controllo dell’annullamento, che c’è a ogni blocco, e si ferma subito.
- **Cache delle misure per worker.** Il worker mantiene una sola `MeasurementCache` per tutta la sua vita. Le generazioni successive che condividono font, testo e larghezza riusano le misure delle righe in cache: digitare un solo carattere in un documento lungo rimisura solo i blocchi il cui input è davvero cambiato.
- **Metriche identiche al thread principale.** I font vengono spediti nel worker come `ArrayBuffer` trasferibili e registrati tramite `new FontFace(...)` nel `FontFaceSet` del worker stesso. Il worker misura con le stesse metriche dei font su canvas che userebbe il thread principale, quindi gli a capo e le altezze delle colonne sono identici byte per byte.
- **La cache dei raster matematici sopravvive alle generazioni nel worker.** Il renderer matematico affianca alla cache con chiave d’identità una cache dei raster con chiave di contenuto: altrimenti la clonazione strutturata di un `MathRender` attraverso il confine del worker mancherebbe la cache d’identità a ogni rigenerazione.
- **Le stesse righe della pagina.** Un canvas creato in un worker non ha una pagina da cui prendere una lingua, e un font risponde con glifi diversi a seconda della lingua. Il motore imposta la lingua del documento (`locale`) su ogni contesto con cui misura, nel worker come sulla pagina, così `build()` restituisce le righe che `buildDocument` dà su una pagina, in qualsiasi lingua ([`bodyText.shapingLanguage: 'document'`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), il valore predefinito da postext 1.30). Con `'host'`, che una configurazione salvata prima della 1.30 conserva, un worker misura senza lingua e una pagina con la propria, come fino a postext 1.28.

### API pubblica

Il client del worker si trova nel sottopercorso `postext/worker` ed è composto da pochi nomi:

- **`createLayoutWorker(opts?): LayoutWorkerHandle`**: avvia un worker dedicato (o avvolge quello che passi tramite `opts.worker`, o avvia il punto d’ingresso del worker in `opts.url`) e restituisce un handle tipizzato. Vedi [Caricare il worker da una CDN](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-il-worker-da-una-cdn).
- **`LayoutWorkerHandle.registerFonts(faces: FontPayload[]): Promise<void>`**: spedisce i byte dei font nel worker. I buffer vengono trasferiti, quindi tieni una copia nuova sul thread principale se devi rinviarli più tardi.
- **`LayoutWorkerHandle.build(content, config?, { signal? }): Promise<VDTDocument>`**: esegue la pipeline. Annullare il segnale interrompe la generazione in corso.
- **`LayoutWorkerHandle.dispose(): void`**: termina il worker e rifiuta tutte le generazioni in sospeso con `AbortError`.
- **`FontPayload`**: `{ family, weight, style, unicodeRange?, buffer: ArrayBuffer }`. `weight` è una stringa di peso CSS (`'700'`, `'bold'`); `registerFonts` lo accetta anche come numero (`700`). Il `buffer` viene trasferito al worker quando chiami `registerFonts`.
- **`BuildCancelledError`** (riesportato da `postext`): ciò che `buildDocument` lancia internamente quando `options.shouldCancel` restituisce `true`. Di solito non lo vedi sul thread principale: il protocollo del worker lo converte in un `AbortError` prima che raggiunga il tuo codice.

Il pacchetto pubblica anche un percorso `postext/worker/entry` che punta allo script compilato del worker. `createLayoutWorker()` risolve questo URL automaticamente; devi farvi riferimento esplicito solo quando il tuo bundler richiede una chiamata costruita a mano `new Worker(new URL(...), { type: 'module' })`, o quando servi tu stesso il punto d’ingresso (`opts.url`).

### Integrazione minima

```ts
import { createLayoutWorker } from 'postext/worker';
import type { FontPayload, LayoutWorkerHandle } from 'postext/worker';
import type { PostextConfig, VDTDocument } from 'postext';

// 1. Create the worker once and keep the handle for the lifetime of your viewport.
const layout: LayoutWorkerHandle = createLayoutWorker();

// 2. Register fonts once per family (transferable ArrayBuffers).
//    getConfigFontFamilies(config) is a helper that lists the families your config will render.
const payloads: FontPayload[] = await collectFontPayloadsForFamilies([
  'EB Garamond',
  'Open Sans',
]);
await layout.registerFonts(payloads);

// 3. Drive builds with last-wins cancellation: abort the previous signal
//    before starting a new build. A stale build is thrown away inside the worker.
let pending: AbortController | null = null;

async function rebuild(
  markdown: string,
  config: PostextConfig,
): Promise<VDTDocument | null> {
  pending?.abort();
  pending = new AbortController();
  try {
    return await layout.build({ markdown }, config, { signal: pending.signal });
  } catch (err) {
    if ((err as { name?: string } | null)?.name === 'AbortError') return null;
    throw err;
  }
}

// 4. Dispose when the component that owns the worker unmounts.
//    Pending builds reject with AbortError.
layout.dispose();
```

Avvolta in un componente React, la forma è questa:

```tsx
import { useEffect, useRef } from 'react';
import { createLayoutWorker } from 'postext/worker';
import type { LayoutWorkerHandle } from 'postext/worker';
import { renderPageToCanvas } from 'postext';
import type { PostextConfig } from 'postext';

export function CanvasPreview({
  markdown,
  config,
}: {
  markdown: string;
  config: PostextConfig;
}) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const workerRef = useRef<LayoutWorkerHandle | null>(null);
  const pendingRef = useRef<AbortController | null>(null);

  // Mount: spin up the worker and ship the fonts once.
  useEffect(() => {
    const handle = createLayoutWorker();
    workerRef.current = handle;
    (async () => {
      const payloads = await collectFontPayloadsForFamilies(
        getConfigFontFamilies(config),
      );
      await handle.registerFonts(payloads);
    })();
    return () => {
      pendingRef.current?.abort();
      handle.dispose();
    };
  }, []); // fonts registered once; re-register only when the family set changes

  // Every keystroke or config change: supersede the in-flight build and kick a new one.
  useEffect(() => {
    const handle = workerRef.current;
    if (!handle) return;
    pendingRef.current?.abort();
    const ac = new AbortController();
    pendingRef.current = ac;
    (async () => {
      try {
        const vdt = await handle.build({ markdown }, config, { signal: ac.signal });
        const canvas = canvasRef.current;
        if (!canvas || !vdt.pages[0]) return;
        renderPageToCanvas(vdt.pages[0], vdt, canvas); // rasterise on the main thread
      } catch (err) {
        if ((err as { name?: string } | null)?.name !== 'AbortError') throw err;
      }
    })();
  }, [markdown, config]);

  return <canvas ref={canvasRef} />;
}
```

Lo schema è sempre lo stesso: **crea una volta, registra i font una volta, genera con AbortSignal tutte le volte che serve, elimina allo smontaggio.**

### Caricare il worker da una CDN

Uno script worker deve provenire dall’origine della pagina stessa, quindi una copia di `postext/worker` servita da una CDN non può avviare il file `layout.worker.js` che ha accanto. `createLayoutWorker()` se ne occupa:

- **esm.sh, senza opzioni.** Quando `postext/worker` è stato caricato a sua volta da esm.sh (il suo URL di modulo ha la forma `https://esm.sh/postext@1.5.0/es2022/worker.mjs`), il client avvia il corrispondente `https://esm.sh/postext@1.5.0/worker/entry` tramite un modulo blob di una riga, della stessa origine, che lo importa. Lo stesso vale per gli import che aggiungono `?deps=`, `?external=` o `?alias=`, e per la forma `https://esm.sh/*postext@1.5.0/worker`. Il worker riceve sempre la build semplice di quella versione, perché un worker non ha una import map con cui risolvere le dipendenze esterne.
- **Qualsiasi altro server, con `url`.** Le altre CDN, come jsDelivr (`/+esm`) o unpkg, non vengono rilevate. `createLayoutWorker({ url })` avvia il modulo d’ingresso del worker in `url`: direttamente se l’URL è della stessa origine, tramite lo stesso involucro blob se è di un’altra origine. Quel server deve consentire le richieste cross-origin (CORS).
- **I bundler non cambiano nulla.** Con Vite, webpack o Next.js, continua a chiamare `createLayoutWorker()` senza opzioni: emettono il worker come un chunk della tua app.

```js
import { createLayoutWorker } from 'https://esm.sh/postext/worker';

const layout = createLayoutWorker();
const face = async (weight, style) => ({
  family: 'EB Garamond',
  weight,
  style,
  buffer: await (await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/eb-garamond@5/files/eb-garamond-latin-${weight}-${style}.woff2`)).arrayBuffer(),
});
await layout.registerFonts(await Promise.all([face(400, 'normal'), face(700, 'normal'), face(400, 'italic')]));
const doc = await layout.build({ markdown }, { bodyText: { fontFamily: 'EB Garamond' } });
```

**Un worker non vede i font della pagina.** Ha un proprio insieme di font, che contiene solo i caratteri inviati con `registerFonts` e i font installati nel sistema. Quando una generazione compone testo in una famiglia che il worker non trova, quel testo viene misurato con un font di ripiego, quindi i suoi a capo non corrisponderanno a quelli della pagina. Il client stampa allora un avviso nella console per famiglia (`"EB Garamond" is not available inside the layout worker…`) ed elenca le famiglie in `BuildStats.missingFonts`, che riceve la callback `onStats` di `build`. Il documento stesso riporta gli stessi caratteri come avvisi di contenuto `fontFallback`.

`handle.prepareFonts(content, config, options)` esegue [`prepareFonts`](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione) sulla pagina e poi invia al worker i file di ogni carattere trovato che il registro dei font contiene (i file del resolver, i caratteri di un pacchetto, le regole `@font-face` leggibili della pagina), solo le fette che contengono i caratteri del documento. Quando i caratteri raggiungono il worker, questo scarta solo le misure di quelle famiglie e la sua cache di documenti finiti.

I caratteri possono raggiungere il worker anche dopo una generazione: un peso di cui le pagine si rivelano avere bisogno, un’altra fetta `unicode-range` di un taglio, un file caricato di nuovo con lo stesso nome di famiglia. La generazione successiva misura con essi. Il canvas di un worker non lo farebbe da solo: in Chrome risolve un font la prima volta che viene impostata una scorciatoia font e non guarda più l’insieme dei font per quella scorciatoia, così il testo manterrebbe le larghezze del carattere che ha risposto per primo. Il worker quindi nomina una famiglia ancora una volta nelle scorciatoie che imposta quando un carattere di quella famiglia è arrivato in ritardo (`Geist, Geist`): un altro elenco di famiglie per il canvas, lo stesso font e le stesse larghezze. I documenti che restituisce mantengono le stringhe di font come le scrive il motore, e un worker che ha i suoi caratteri prima della prima generazione imposta ogni scorciatoia così come è scritta.

### Raccolta dei payload dei font (Fontsource / Google Fonts)

`registerFonts` accetta i byte grezzi dei font. Il thread principale è il posto giusto dove scaricarli, perché una cache centrale permette a più istanze di worker di condividere gli stessi byte.

Il `collectFontPayloadsForFamilies` della Sandbox (`packages/postext-sandbox/src/controls/fontLoader.ts`) è un’implementazione di riferimento. Questa funzione:

1. Interroga `https://api.fontsource.org/v1/fonts/{family-id}` per sapere quali pesi e stili ha la famiglia.
2. Legge i sottoinsiemi della famiglia e il loro `unicode-range` dal suo pacchetto su jsDelivr (`@fontsource/{family-id}@5/400.css`), una volta per famiglia. I file vengono poi chiesti alla versione esatta da cui è stato servito quel foglio di stile (l’`x-jsd-version` di jsDelivr), così una famiglia non arriva mai da due versioni del suo pacchetto.
3. Scarica tutti i file del tondo, del grassetto e dei rispettivi corsivi di ogni famiglia (`files/{family-id}-{subset}-{weight}-{style}.woff2`); `collectFontPayloadsForConfig(config, markdown)` prende invece i pesi e gli stili richiesti da una configurazione. Sono file statici, quelli che il PDF incorpora, così il worker misura con ciò che il PDF disegna. Un file variabile dà avanzamenti diversi tra due dei suoi master, e una riga a una frazione di pixel dalla sua giustezza si divide altrove.
4. Restituisce un `FontPayload[]` in cui `buffer` è un `ArrayBuffer` nuovo a ogni chiamata; è importante, perché `registerFonts` trasferisce il buffer e lascia staccata la copia del mittente.

I file di un taglio vanno nell’ordine in cui li dichiara un pen, prima `latin` e per ultimi i file propri della scrittura (`arabic`, le fette numerate di una famiglia cinese o giapponese), perché il file aggiunto per ultimo risponde per un carattere contenuto in due file. Una famiglia per cui Fontsource non ha file viene letta invece dal foglio di stile CSS2 di Google, che restituisce WOFF2 solo a stringhe User-Agent simili a quelle di un browser.

Abbinala a `getConfigFontFamilies(config)` per ottenere l’elenco delle famiglie che una data `PostextConfig` comporrà davvero (testo corrente, titoli, punti elenco, numeri degli elenchi numerati).

### Annullamento cooperativo dentro il motore

Se guidi tu stesso `buildDocument` (per esempio dentro un worker personalizzato), la pipeline espone un hook `shouldCancel` che puoi usare direttamente:

```ts
import { buildDocument, BuildCancelledError } from 'postext';

let superseded = false;
try {
  const vdt = buildDocument(content, config, cache, {
    shouldCancel: () => superseded,
  });
} catch (err) {
  if (err instanceof BuildCancelledError) return; // a newer build took over
  throw err;
}
```

`shouldCancel` viene chiamato una volta per ogni blocco di primo livello durante la collocazione. L’hook è volutamente cooperativo: non può fermare la chiamata di impaginazione di pretext a metà riga, ma mantiene la granularità dell’annullamento abbastanza fine (millisecondi) perché chi digita velocemente non aspetti mai una generazione superata.

### Guidare l’esportazione PDF dal worker

Il backend PDF prende un `VDTDocument` pronto e lo trasforma in byte PDF. **Non** riesegue l’impaginazione. Questo significa che il flusso PDF canonico nel browser si abbina senza attriti al worker: genera il VDT nel worker (fuori dal thread principale, annullabile, con riuso della cache), poi chiama `renderToPdf` sul thread principale sullo stesso VDT.

```ts
import type { LayoutWorkerHandle } from 'postext/worker';
import { renderToPdf } from 'postext-pdf';
import type { PostextConfig } from 'postext';
import { createPdfFontProvider } from './pdfFontProvider';

const fontProvider = createPdfFontProvider();

export async function exportPdf(
  layout: LayoutWorkerHandle,
  markdown: string,
  config: PostextConfig,
): Promise<Uint8Array> {
  // 1. Build the VDT in the worker — UI stays responsive during the layout passes.
  const vdt = await layout.build({ markdown }, config);

  // 2. Rasterise to PDF on the main thread. renderToPdf is fast once the VDT exists
  //    because it is walking precomputed coordinates, not remeasuring text.
  return renderToPdf(vdt, {
    fontProvider,
    // pdfGeneration config on `vdt.config` is honoured automatically.
  });
}
```

Se mantieni già un handle del worker per l’anteprima dal vivo, riusalo per l’esportazione invece di avviare un secondo worker: la cache delle misure dentro il worker rende praticamente gratuita un’esportazione PDF che segue un’anteprima a schermo.

Per un libro lungo, anche scrivere il PDF richiede secondi; `postext-pdf/worker` esegue quel passaggio in un worker tutto suo (vedi [Rendere il PDF in un worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-il-pdf-in-un-worker)).

### Quando usare il worker e quando no

Usa il worker per:

- **Anteprime dal vivo, editor e ambienti di prova.** Tutto ciò in cui il documento viene rigenerato in risposta a un input dell’utente.
- **Visualizzatori HTML che reagiscono al ridimensionamento** e rieseguono l’impaginazione a ogni notifica di `ResizeObserver`.
- **Esportazione PDF nel browser** avviata da un’interfaccia che ha già un’anteprima dal vivo: riusa l’handle del worker esistente così l’esportazione sfrutta la cache delle misure.
- **Più schede di output** che hanno tutte bisogno dello stesso VDT (le viste Canvas / HTML / PDF della Sandbox condividono un handle del worker per ogni montaggio della vista).

Evita il worker per:

- **La generazione lato server**: Node non ha un `FontFaceSet` del browser, e il thread lo controlli comunque tu.
- **Esportazioni una tantum isolate** (una CLI, uno script di esportazione headless, una Cloud Function) dove non esiste un’interfaccia interattiva da bloccare. Chiamare direttamente `buildDocument` è più semplice ed evita il costo del trasferimento iniziale dei font.

## Integrare il visualizzatore HTML

Il visualizzatore HTML è il renderer di Postext pensato prima di tutto per lo schermo. Invece di rasterizzare le pagine in una bitmap emette nodi DOM posizionati in modo assoluto, la cui geometria è determinata dalla stessa pipeline che produce l’output di stampa. Per questo è la scelta giusta quando vuoi in un browser una tipografia leggibile, selezionabile e sensibile al ridimensionamento (un’app di lettura, un’anteprima dentro un prodotto o una superficie di documentazione incorporata) senza dover includere un visualizzatore PDF.

I pezzi principali dell’API pubblica:

- **`buildDocument(content, config, cache?)`**: esegue l’intera pipeline di impaginazione e restituisce un `VDTDocument`.
- **`renderToHtmlIndexed(doc, options)`**: trasforma il VDT in un’unica stringa HTML più una scomposizione per pagina e per blocco. La scomposizione permette di aggiornare il DOM a basso costo quando tra due rendering cambiano solo pochi blocchi.
- **`resolveHtmlViewerConfig(partial)`**: completa i valori predefiniti del visualizzatore HTML (`maxCharsPerLine`, `columnGap`, `optimalLineBreaking`).
- **`buildFontString` + `measureGlyphWidth` + `dimensionToPx`**: primitive di misurazione usate per ricavare una larghezza di colonna reale in pixel da un numero di caratteri desiderato.
- **`createMeasurementCache` / `clearMeasurementCache`**: cache intercambiabili che ti permettono di riutilizzare le misure tra un’impaginazione e l’altra.
- **`prepareFonts` / `buildDocumentWithFonts` / `watchFonts` / `onFontsChanged`**: caricano i caratteri del documento prima dell’impaginazione e impaginano di nuovo quando i caratteri arrivano (vedi [Caricare i font prima dell’impaginazione](https://postext.dev/it/docs/configuration-programmatic-usage.md#caricare-i-font-prima-dellimpaginazione)).

### Esempio dal vivo: una stringa HTML

L’intero percorso in JavaScript semplice, prima dell’integrazione con React più sotto: costruisci il documento, passa il `VDTDocument` a `renderToHtml` e inserisci la stringa in un contenitore. `mode: 'single'` impila le pagine in verticale; `background` dà loro un colore, perché per impostazione predefinita le pagine sono trasparenti. Il pen stampa anche il markup generato, così puoi vedere le righe posizionate in modo assoluto che il renderer emette: il browser le dipinge ma non le ridistribuisce mai.

> **Esempio eseguibile: Postext · rendere un documento in HTML** — Impagina un documento Markdown con postext e rendilo in una stringa HTML. ([codice](https://github.com/drnachio/postext/tree/main/docs/examples/render-html))

### Integrazione minima

Il frammento qui sotto è l’integrazione utile più breve: costruisce il documento alla dimensione attuale della finestra, lo rende in un contenitore e lo rieseguisce a ogni ridimensionamento.

```tsx
import { useEffect, useRef } from 'react';
import {
  buildDocument,
  renderToHtmlIndexed,
  resolveHtmlViewerConfig,
  buildFontString,
  measureGlyphWidth,
  dimensionToPx,
  createMeasurementCache,
  watchFonts,
  onFontsChanged,
} from 'postext';
import type { PostextConfig, MeasurementCache } from 'postext';

// Screen-friendly DPI: at 144 DPI, an 8pt body size resolves to 16 px.
const HTML_DPI = 144;
const PADDING_PX = 24;

// Prose sample used to measure the target column width. Proportional fonts
// make "N × average width" unreliable, so we measure a representative string.
const SAMPLE =
  'The quick brown fox jumps over the lazy dog. Sphinx of black quartz, judge my vow.';

function sampleForChars(n: number): string {
  let s = SAMPLE;
  while (s.length < n) s += ' ' + SAMPLE;
  return s.slice(0, n);
}

export function PostextHtmlViewer({
  markdown,
  config,
  mode = 'multi',
}: {
  markdown: string;
  config: PostextConfig;
  mode?: 'single' | 'multi';
}) {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const cacheRef = useRef<MeasurementCache>(createMeasurementCache());

  useEffect(() => {
    const host = hostRef.current;
    if (!host) return;

    const relayout = () => {
      const rect = host.getBoundingClientRect();
      if (rect.width === 0 || rect.height === 0) return;

      const viewer = resolveHtmlViewerConfig(config.htmlViewer);
      const fontFamily = config.bodyText?.fontFamily ?? 'EB Garamond';
      const fontWeight = config.bodyText?.fontWeight ?? 400;
      const fontSize = config.bodyText?.fontSize ?? { value: 8, unit: 'pt' as const };
      const fontSizePx = dimensionToPx(fontSize, HTML_DPI);

      // Measure the *actual* column width for N characters of body prose.
      const targetColumnPx = measureGlyphWidth(
        sampleForChars(viewer.maxCharsPerLine),
        buildFontString(fontFamily, fontSizePx, String(fontWeight), 'normal'),
      );

      const inner = Math.max(rect.width - PADDING_PX * 2, 100);
      let columnWidthPx: number;
      if (mode === 'single') {
        columnWidthPx = Math.min(targetColumnPx, inner);
      } else {
        // Fit as many columns as we can at the target width.
        const count = Math.max(
          1,
          Math.floor((inner + viewer.columnGap) / (targetColumnPx + viewer.columnGap)),
        );
        columnWidthPx = (inner - viewer.columnGap * (count - 1)) / count;
      }
      columnWidthPx = Math.max(Math.floor(columnWidthPx), 80);

      // Single mode uses one very tall page; multi mode uses the viewport
      // height so each VDT "page" becomes one column.
      const pageHeightPx =
        mode === 'single' ? Math.max(rect.height * 20, 200_000) : Math.max(rect.height - PADDING_PX * 2, 400);

      const override: PostextConfig = {
        ...config,
        page: {
          ...config.page,
          dpi: HTML_DPI,
          width: { value: columnWidthPx, unit: 'px' },
          height: { value: pageHeightPx, unit: 'px' },
          margins: {
            top: { value: 0, unit: 'px' },
            bottom: { value: 0, unit: 'px' },
            left: { value: 0, unit: 'px' },
            right: { value: 0, unit: 'px' },
          },
        },
        layout: { ...config.layout, layoutType: 'single' },
        bodyText: {
          ...config.bodyText,
          optimalLineBreaking: viewer.optimalLineBreaking,
        },
      };

      const doc = buildDocument({ markdown }, override, cacheRef.current);
      const { html } = renderToHtmlIndexed(doc, {
        mode,
        columnGap: viewer.columnGap,
        padding: PADDING_PX,
        background: 'transparent',
      });

      host.innerHTML = html;
    };

    relayout();

    const ro = new ResizeObserver(() => relayout());
    ro.observe(host);

    // Re-measure when web fonts land so glyph widths aren't taken from fallbacks.
    const stopWatching = watchFonts();
    const off = onFontsChanged(() => relayout());

    return () => {
      ro.disconnect();
      off();
      stopWatching();
    };
  }, [markdown, config, mode]);

  return <div ref={hostRef} style={{ width: '100%', height: '100%', overflow: 'auto' }} />;
}
```

Qualche nota su che cosa fa questo esempio:

- **Misurare la colonna, non stimarla.** Poiché `maxCharsPerLine` è un *obiettivo* espresso in caratteri, la larghezza reale in pixel dipende dal font del testo corrente. `measureGlyphWidth` fornisce una misura reale con il font scelto, il che mantiene coerente la giustezza quando cambi font.
- **Riscrivere la pagina.** Il visualizzatore HTML tratta ogni «pagina» del VDT come una colonna sullo schermo. L’esempio sostituisce `page.width` con la larghezza di colonna misurata, azzera i margini (il padding vive fuori dalla pagina, nel div `.pt-doc` che la avvolge) e usa `HTML_DPI = 144` perché un testo corrente di `8pt` diventi `16px`.
- **Attenzione al caricamento dei font.** `watchFonts` ascolta `document.fonts` e, una volta per frame, scarta ciò che era stato misurato nelle famiglie i cui caratteri sono arrivati; `onFontsChanged` impagina poi di nuovo la colonna. Senza la nuova impaginazione, il primo rendering usa le metriche di un font di ripiego e salta quando arriva il font vero.
- **Riutilizzare la cache delle misure.** Creare la cache una volta per componente significa che i ridimensionamenti e i cambi di scala del font riutilizzano le misure del rendering precedente invece di rimisurare ogni paragrafo.

### Andare oltre

L’esempio qui sopra è volutamente essenziale. Le integrazioni in produzione di solito aggiungono:

- **Isolamento con Shadow DOM**: rendi dentro `host.attachShadow({ mode: 'open' })` così che niente della pagina esterna possa far trapelare CSS nel visualizzatore.
- **Aggiornamento incrementale**: `renderToHtmlIndexed` restituisce `pages[i].blocks`, ognuno con un `id` stabile e l’HTML esterno del blocco. Quando tra due rendering differiscono solo pochi blocchi puoi sostituire sul posto quei contenitori di blocco invece di ricostruire `innerHTML`.
- **Sovrapposizioni**: un SVG posizionato in modo assoluto sopra ogni `.pt-page` per cursori, selezioni o griglie della linea di base.
- **Link**: le parole di un link Markdown sono avvolte in `<a href="…" rel="noopener noreferrer">`, che prende il colore del testo e non ha sottolineatura; vedi [Formato del documento › Link](https://postext.dev/it/docs/document-format.md#link). In un visualizzatore simile a un editor, intercetta i clic sugli `a[href]` che non cominciano con `#` e aprili in una nuova scheda (le ancore di `:ref` puntano dentro il documento).
- **Immagini a un solo inchiostro**: con `diagramStyle.singleInk` attivo, gli `<img>` SVG ricevono un filtro CSS, a meno che tu non passi `singleInk: false` per URL già ricolorati; vedi [Un solo inchiostro su canvas e in HTML](https://postext.dev/it/docs/configuration-resources.md#inchiostro-unico-sul-canvas-e-in-html).

Il componente `HtmlPreview` della Sandbox (`packages/postext-sandbox/src/viewport/HtmlPreview/index.tsx`) implementa tutto questo sopra la stessa API mostrata qui e può servire da riferimento. Inoltre fa passare ogni build da un worker di impaginazione condiviso (vedi [Eseguire l’impaginazione in un Web Worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#eseguire-limpaginazione-in-un-web-worker)), così le modifiche dal vivo e i ridimensionamenti non bloccano mai il thread principale: sostituisci la chiamata diretta `buildDocument(...)` del frammento qui sopra con `layoutWorker.build(...)` quando sei pronto a spostare l’impaginazione fuori dal thread principale.

### In che cosa l’output HTML differisce da canvas e PDF

`renderToHtml` colloca ogni riga, figura ed elemento di design esattamente dove li collocano il canvas e il PDF, ma attorno a essi dipinge meno:

| Funzione | Canvas (`renderPage`) | HTML (`renderToHtml`) | PDF (`renderToPdf`) |
| --- | --- | --- | --- |
| Sfondo della pagina | Bianco, con `page.backgroundColor` sul formato rifilato e sull’abbondanza. | **Trasparente**, a meno che tu non passi `background` o imposti `page.backgroundColor` (che allora riempie l’intero riquadro della pagina, area esterna compresa). | Bianco, con `page.backgroundColor` sul formato rifilato e sull’abbondanza. |
| Griglia della linea di base (`page.baselineGrid`) | Disegnata | Non disegnata | Disegnata |
| Filetto tra le colonne (`layout.columnRule`) | Disegnato | Non disegnato | Disegnato |
| Segni di taglio (`page.cutLines`) | Disegnati | Non disegnati; il riquadro della pagina include comunque l’area esterna attorno al formato rifilato. | Disegnati |
| Negativo della pagina | Opzione `pageNegative` | Non disponibile | Opzione `pageNegative` |
| Testo | Pixel | Testo selezionabile in elementi posizionati in modo assoluto, composto nelle famiglie di caratteri CSS: la pagina deve caricare gli stessi font. | Font incorporati dal tuo `fontProvider`; selezionabile, ricercabile e taggato. |
| Testo verticale (`layout.writingMode: 'vertical-rl'`) | Caratteri dipinti una cella alla volta, raddrizzati; forme verticali tramite un font gemello (`loadVerticalAlternates`). | Il flusso in un unico riquadro ruotato di un quarto di giro; ogni riga raddrizzata e composta con `writing-mode: vertical-rl`, così il browser prende le forme verticali e mette in piedi i caratteri; i numeri brevi in `text-combine-upright: all`; una lineetta, un’ellissi, un punto mediano o una tilde ondulata in un riquadro della sua cella (il browser la farebbe avanzare della sua larghezza orizzontale), una lineetta allungata per riempirla da `flow.dashAdvances`. | Caratteri in piedi tramite un gemello `Identity-V` di ogni font; vedi [Testo verticale nel PDF](https://postext.dev/it/docs/configuration-programmatic-usage.md#testo-verticale-nel-pdf). |
| Immagini | `registerResourceImage` | L’opzione `resourceImageUrl(fileId)`; senza di essa, un riquadro grigio segnaposto. | L’opzione `resourceBytes(fileId)`. |
| Formule | Tracciati vettoriali | `<svg>` in linea | Tracciati vettoriali |
| Link | Nessuno | I rimandi `:ref` puntano alla loro risorsa; le righe dell’indice no. | I rimandi `:ref` e le righe dell’indice, più la struttura (segnalibri). |

La pagina trasparente conta su un sito scuro: un’anteprima senza `background` mostra testo nero sullo sfondo scuro del sito. Passa `renderToHtml(doc, { background: '#ffffff' })`, oppure dai al documento un `page.backgroundColor`.

**Gli stili di testo della pagina ospite restano fuori.** Ogni riga è composta alle larghezze misurate dal motore, quindi un `letter-spacing`, `word-spacing`, `text-transform` o `font-variant` che l’output ereditasse dalla pagina circostante allargherebbe le sequenze di glifi e farebbe sovrapporre le righe. Per questo la radice `.pt-doc` reimposta le proprietà di testo ereditate (spaziatura tra lettere e tra parole, maiuscole e minuscole, rientro, spazi bianchi, stile, variante, peso ed estensione del font, funzioni OpenType e crenatura, interlinea, allineamento, ombra ed enfasi del testo, sillabazione, direzione, modo di scrittura, contorno e riempimento del testo, e l’ingrandimento automatico del testo su mobile) prima delle proprie dichiarazioni di impaginazione, così l’output appare uguale dentro una shadow root o sotto un elemento con stili propri. L’elenco è esportato come `HTML_TEXT_RESET`, una stringa di dichiarazioni CSS: un ospite che monta l’`innerHtml` delle pagine (da `renderToHtmlIndexed`) in contenitori propri la imposta sulla loro radice. Fino a postext 1.4 la radice non reimpostava nulla; la soluzione era un contenitore con `all: initial`.

## Generare PDF

L’output PDF vive in un pacchetto separato, **`postext-pdf`**, così che le integrazioni solo web non paghino il costo di `pdf-lib` e `@pdf-lib/fontkit`. Il backend PDF non rimisura il testo: consuma esattamente lo stesso `VDTDocument` che passeresti a `renderToCanvas` o `renderToHtml` e traduce le sue coordinate in pixel in punti PDF. I tre output sono quindi garantiti concordi su a capo, altezze delle colonne e collocazione delle risorse.

> **Nel browser, costruisci il VDT tramite il [Web Worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#eseguire-limpaginazione-in-un-web-worker).** `renderToPdf` in sé è veloce una volta che il VDT esiste: la parte costosa è la pipeline di impaginazione che lo ha prodotto. Eseguire quella pipeline nel worker mantiene reattiva l’interfaccia e permette a un’esportazione PDF di riutilizzare la stessa cache delle misure che l’anteprima dal vivo ha già scaldato. Vedi [Guidare l’esportazione PDF dal worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#guidare-lesportazione-pdf-dal-worker) per il flusso consigliato. Gli esempi sul thread principale qui sotto sono il riferimento per *che cosa significano gli argomenti*: per il codice dell’interfaccia, costruisci prima il VDT nel worker e chiama direttamente solo `renderToPdf`.

### Installazione

```bash
npm install postext postext-pdf
```

`postext` è una peer dependency di `postext-pdf`. Ogni release di `postext-pdf` richiede il `postext` con cui è stata pubblicata, o uno successivo della stessa major (il suo intervallo peer è `^` quella versione, `^1.5.0` per la 1.5.0), perché importa funzioni di supporto che `postext` ha aggiunto in quella release. Aggiorna i due pacchetti insieme e, su una CDN, fissali alla stessa versione.

### API pubblica

Il pacchetto espone un unico punto di ingresso e una manciata di tipi:

- **`renderToPdf(doc, options): Promise<Uint8Array>`**: prende un `VDTDocument` (o i capitoli di un libro come array di documenti) e restituisce i byte grezzi del PDF.
- **`PdfFontProvider`**: la firma della callback `(family, weight, style, request?) => Promise<Uint8Array | Uint8Array[]>` che `renderToPdf` usa per chiedere i byte di un font quando deve incorporare una nuova combinazione di famiglia, peso e stile. `request.codePoints` contiene i caratteri che le pagine compongono in quel font; la risposta è un file, o più file che insieme formano il font (vedi [Font cinesi, giapponesi e coreani](https://postext.dev/it/docs/configuration-programmatic-usage.md#font-cinesi-giapponesi-e-coreani)).
- **`declaredFontFile(bytes, { unicodeRange?, order? }): PdfFontFile`**: un file di un font che dice come la pagina lo ha dichiarato: l’`unicode-range` della sua regola `@font-face` e la sua posizione nell’ordine di dichiarazione. Un `PdfFontFile` è un `Uint8Array` con queste due proprietà facoltative (vedi [Font cinesi, giapponesi e coreani](https://postext.dev/it/docs/configuration-programmatic-usage.md#font-cinesi-giapponesi-e-coreani)). Da postext-pdf 1.30.
- **`RenderToPdfOptions`**: `{ fontProvider, resourceBytes?, outlines?, accessible?, colorSpace?, pageNegative?, characterGrid?, onProgress?, onWarning?, rasterizeSvg?, harfbuzzWasm?, print?, outputProfile?, profileBaseUrl?, date? }`. `outlines`, `accessible` e `colorSpace` ricadono sul `pdfGeneration` del documento quando sono omessi (vedi [Generazione PDF (configurazione)](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#generazione-del-pdf-configurazione)). `resourceBytes` è descritto in [Byte delle risorse e master di stampa](https://postext.dev/it/docs/configuration-programmatic-usage.md#byte-delle-risorse-e-master-di-stampa); `onWarning` in [Quali font vengono chiesti al provider](https://postext.dev/it/docs/configuration-programmatic-usage.md#quali-font-vengono-chiesti-al-provider) e in [Avvisi nel documento](https://postext.dev/it/docs/configuration-programmatic-usage.md#avvisi-nel-documento). `characterGrid: true` stampa la griglia che `cjk.grid.show` disegna sullo schermo e che altrimenti il PDF tralascia (vedi [Griglia dei caratteri](https://postext.dev/it/docs/configuration-east-asian.md#griglia-di-caratteri)). `harfbuzzWasm` indica da dove caricare `harfbuzz.wasm` di HarfBuzz (un URL, relativo alla pagina, o i byte del file) per un documento con testo da destra a sinistra o con lettere legate; se omesso, la copia accanto al modulo di postext-pdf, poi la stessa release di harfbuzzjs da jsDelivr ed esm.sh. `print` prende le impostazioni di produzione per la stampa (standard PDF/X, profilo di output, nero, preflight), con ripiego sul `print` del documento; con uno standard PDF/X, o con `colorSpace: 'cmyk'`, ogni colore viene separato tramite il profilo ICC di output, i cui byte sono dati da `outputProfile` (altrimenti viene scaricato da `profileBaseUrl`, per impostazione predefinita la copia sulla CDN di npm della cartella `icc/` di postext). `date` (un `Date`, un istante in millisecondi o una stringa di data, come il `mtime` di `createBundle`) è la data di creazione e di modifica del file e, in un file PDF/X, le date del suo pacchetto XMP e l’origine del suo identificatore di documento: lo stesso documento reso due volte con la stessa `date` dà gli stessi byte. Se è omessa, il file porta l’ora della chiamata. Una data non valida solleva un’eccezione.
- **`PdfWarning`**: un problema non bloccante segnalato tramite `onWarning`; si distingue per `kind`: `'fontFallback'` (`PdfFontFallbackWarning`), un font sostituito da un’altra variante della sua famiglia; `'missingGlyph'` (`PdfMissingGlyphWarning`), caratteri per cui nessun file di un font ha un glifo; `'variableFontDefaultInstance'` (`PdfVariableFontWarning`), un font variabile chiesto a un peso diverso dalla sua istanza predefinita; `'cffEmbeddedWhole'` (`PdfCffEmbeddedWholeWarning`), un font CFF di oltre 2 MB incorporato per intero; `'complexShapingUnavailable'` (`PdfComplexShapingWarning`), testo da destra a sinistra o con lettere legate disegnato senza HarfBuzz, che non è stato possibile caricare (`reason` elenca dove è stato cercato), per cui i segni dell’arabo sono fuori posto; `'outputProfileUnavailable'` (`PdfPrintWarning`), un rendering CMYK il cui profilo di output non si è potuto caricare, convertito con la formula semplificata; `'pageNegativeIgnored'` (`PdfPrintWarning`), il negativo della pagina escluso da un file PDF/X-1a; oppure `'missingImage'`, un’immagine senza byte disegnata come segnaposto (segnalato solo a un `onWarning` che passi tu; gli avvisi sui font, in sua assenza, vanno a `console.warn`).
- **`decompressWoff2(bytes): Uint8Array`**: funzione di supporto che trasforma un file WOFF2 in byte TTF, il formato che `pdf-lib` può incorporare direttamente.
- **`createPdfWorker(options?)`**, da `postext-pdf/worker`: lo stesso rendering in un Web Worker; vedi [Rendere il PDF in un worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#rendere-il-pdf-in-un-worker).

### Esempio minimo

```ts
import { buildDocument } from 'postext';
import { renderToPdf } from 'postext-pdf';

const vdt = buildDocument(
  { markdown: '# Chapter One\n\nThe story begins here…' },
  {
    page: { sizePreset: '17x24' },
    layout: { layoutType: 'double' },
    bodyText: { fontFamily: 'EB Garamond', fontSize: { value: 9, unit: 'pt' } }, // 9 pt overrides the 8 pt default
  },
);

const pdfBytes = await renderToPdf(vdt, {
  fontProvider: async (family, weight, style) => {
    // Return TTF bytes for this family/weight/style.
    // See the "Font provider" section below for a real implementation.
    const res = await fetch(`/fonts/${family}-${weight}${style === 'italic' ? 'i' : ''}.ttf`);
    return new Uint8Array(await res.arrayBuffer());
  },
});

// `pdfBytes` is a Uint8Array — save, download, or stream it.
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
window.open(url);
```

### Perché un provider di font?

`pdf-lib` incorpora nel PDF file di font veri: i font installati nel browser non sono disponibili al momento del rendering, e un font che hai caricato solo per la misurazione a schermo non basta, da solo, a produrre un PDF autonomo. `renderToPdf` scorre le pagine alla ricerca di ogni font che dipingono (uno per combinazione `family|weight|style`; vedi [Quali font vengono chiesti al provider](https://postext.dev/it/docs/configuration-programmatic-usage.md#quali-font-vengono-chiesti-al-provider)) e chiama il tuo provider una volta per ogni combinazione distinta. Il provider restituisce un `Uint8Array` di byte **TTF o OTF**, o un elenco di essi per un font servito in più file (vedi [Font cinesi, giapponesi e coreani](https://postext.dev/it/docs/configuration-programmatic-usage.md#font-cinesi-giapponesi-e-coreani)); `pdf-lib` crea un sottoinsieme dei contorni TrueType e incorpora per intero i file CFF (`.otf`). I font a cui il provider risponde con lo stesso file, come una variante regular che fa le veci del grassetto che manca a una famiglia, condividono un unico font incorporato. Un font con cui alla fine nessuna pagina disegna, come il font del testo di una figura SVG quando la figura è disegnata come immagine, viene lasciato fuori dal file.

**Usa font statici per ogni peso, non un unico font variabile.** Google Fonts serve spesso un solo WOFF2 variabile per famiglia, che copre l’intero asse dei pesi. `pdf-lib` può incorporare da un file variabile solo l’istanza predefinita, quindi un paragrafo in grassetto verrebbe reso con il peso regular. Fontsource pubblica file WOFF2 statici per ogni peso che risolvono il problema in modo pulito: è lo schema che usa la Sandbox. Un file variabile chiesto a un peso diverso dalla sua istanza predefinita viene segnalato con un avviso `variableFontDefaultInstance`.

**Ogni parola del testo finisce dove l’ha messa l’impaginazione.** Nei paragrafi, nelle voci di elenco, nelle citazioni, nei riquadri e nel resto del testo che scorre, ogni parola comincia nella posizione misurata dal VDT, quindi una differenza tra le larghezze del browser e quelle del font incorporato non si accumula mai lungo una riga. Una riga senza formattazione in linea viene dipinta come un unico oggetto di testo che sposta la penna tra le parole; le righe giustificate, quelle centrate e quelle con formattazione vengono dipinte parola per parola. Un carattere per cui il font non ha glifo, che il browser ha misurato in un altro font e che il PDF dipinge come il riquadro del glifo mancante del font, non sposta nessuna delle parole che lo seguono, e viene segnalato una volta per font con un avviso `missingGlyph`. Uno spazio che manca al font, come uno spazio unificatore stretto o uno spazio cifra, prende la larghezza che gli ha dato il browser, e i caratteri invisibili come il word joiner e lo spazio a larghezza zero non vengono disegnati. Un trattino unificatore (U+2011) che manca al font viene disegnato con il trattino del font (U+2010), o con il suo trattino-meno quando non ha nemmeno il trattino, come lo mostra il browser; Open Sans e Outfit, tra gli altri, non hanno né l’uno né l’altro. Nessuno dei due casi conta come glifo mancante. Ci sono due eccezioni. Una riga con lettere da destra a sinistra viene ancora dipinta come un’unica sequenza (vedi [Lingue e scritture](https://postext.dev/it/docs/configuration-text.md#lingue-e-scritture)). Il testo collocato da un design (testatine e piè di pagina, aperture, titoli dei riquadri e altri elementi di design) viene composto con le larghezze proprie del font incorporato, quindi un glifo che lì manca al font sposta ancora il resto della sua riga.

### Quali font vengono chiesti al provider

`renderToPdf` percorre le pagine nel modo in cui le dipinge e chiede al provider solo i font che disegnano:

- il font regular di un blocco che compone almeno una riga, e il grassetto, corsivo o grassetto corsivo di ogni sequenza composta effettivamente in quel modo;
- le sequenze dei chip, i segni degli elenchi e il testo degli slot del design (testatine, folio, fasce di apertura e di parte);
- il testo di didascalie, note e celle di tabella di ogni risorsa;
- i font nominati dal `<text>` di una figura SVG, quando la figura è incorporata. Una famiglia che il provider non riesce a fornire affatto passa alla famiglia successiva dell’elenco `font-family` dell’SVG.

Così a una famiglia dei titoli che nessuno inclina non viene mai chiesto il corsivo, e una figura senza nota non ha mai bisogno dei font delle note.

Quando il provider rifiuta un font, il rendering continua. Al suo posto viene incorporato un altro font della stessa famiglia, e viene segnalato un `PdfWarning`. Il sostituto è il primo font che si carica, provando i nove pesi standard (da 100 a 900) nell’ordine usato dalla selezione dei font del CSS, che è anche il font che il browser mostra nell’anteprima:

1. prima lo stesso stile. Per un peso da 400 a 500, vengono prima i pesi fino a 500, poi quelli più leggeri dal più vicino in giù, poi quelli più pesanti da 600 in su. Per un peso sotto 400, vengono prima i pesi più leggeri, dal più vicino in giù, poi quelli più pesanti. Per un peso sopra 500, vengono prima i pesi più pesanti, poi quelli più leggeri;
2. poi l’altro stile, corsivo per il tondo e tondo per il corsivo, al peso richiesto e agli altri pesi nello stesso ordine.

Una famiglia senza corsivi compone quindi in tondo le sue sequenze in corsivo, una famiglia che fornisce solo 400 e 700 prende un 600 come 700, e una famiglia che fornisce una sola variante compone tutto in quella. Al provider viene chiesto un font alla volta, in quell’ordine, e mai due volte lo stesso font, quindi un font che nessuno usa non viene mai incorporato. A una famiglia che il provider non riesce a servire affatto vengono chiesti tutti e 18 quei font prima che il rendering fallisca.

```ts
const bytes = await renderToPdf(doc, {
  fontProvider,
  onWarning: (w) => {
    // { kind: 'fontFallback', family: 'Oswald', weight: 700, style: 'italic',
    //   fallback: { weight: 700, style: 'normal' }, reason: '…', message: '…' }
    console.info(w.message);
  },
});
```

Senza `onWarning`, il messaggio va a `console.warn`. Il testo mantiene le sue posizioni, che vengono dal VDT e sono state misurate con i font che aveva il browser, quindi un sostituto con larghezze diverse può sembrare stretto o largo. Fornisci il font vero per correggerlo. Un rendering fallisce ancora (`postext-pdf: failed to load font(s): …`) solo quando il provider non riesce a fornire nessun font di una famiglia a un peso standard, né in tondo né in corsivo.

### Provider di font per il browser (Fontsource + WOFF2)

La Sandbox include `createPdfFontProvider()` (`packages/postext-sandbox/src/viewport/pdfFontProvider.ts`), che puoi copiare in qualsiasi app per browser. L’essenziale:

```ts
import type { PdfFontProvider } from 'postext-pdf';
import { decompressWoff2 } from 'postext-pdf';

const bytesCache = new Map<string, Promise<Uint8Array>>();

function fontsourceId(family: string): string {
  return family.toLowerCase().replace(/\s+/g, '-');
}

function fontsourceWoff2Url(
  family: string,
  weight: number,
  style: 'normal' | 'italic',
): string {
  const id = fontsourceId(family);
  return `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/files/${id}-latin-${weight}-${style}.woff2`;
}

export function createPdfFontProvider(): PdfFontProvider {
  return async (family, weight, style) => {
    const key = `${family}|${weight}|${style}`;
    const cached = bytesCache.get(key);
    if (cached) return cached;

    const promise = (async (): Promise<Uint8Array> => {
      const url = fontsourceWoff2Url(family, weight, style);
      const res = await fetch(url, { mode: 'cors' });
      if (!res.ok) throw new Error(`font fetch failed: ${res.status} ${url}`);
      // pdf-lib needs TTF bytes, so decompress the WOFF2 wrapper client-side.
      return decompressWoff2(new Uint8Array(await res.arrayBuffer()));
    })();

    bytesCache.set(key, promise);
    return promise;
  };
}
```

Una versione per la produzione dovrebbe anche:

- **Interrogare i pesi disponibili** (tramite `https://api.fontsource.org/v1/fonts/{id}`) e agganciare il peso richiesto al più vicino che la famiglia fornisce davvero, così una richiesta di `weight: 600` per una famiglia che ha solo `{400, 700}` va comunque a buon fine.
- **Ripiegare dal corsivo al tondo** quando una famiglia non ha una variante corsiva per il peso richiesto, invece di far fallire tutto il rendering.
- **Riutilizzare la cache tra un rendering e l’altro** (tieni `bytesCache` a livello di modulo, non per chiamata) così rigenerare il PDF dopo una modifica della configurazione non costa praticamente nulla.

### Font cinesi, giapponesi e coreani

Un font CJK non arriva come un unico file piccolo. Fontsource fornisce Noto Serif SC come un centinaio di file per peso, ognuno con una parte dei caratteri e dichiarato nel foglio di stile della famiglia con il suo `unicode-range` (`@fontsource/noto-serif-sc/400.css`); il browser scarica i file toccati dal testo di una pagina. Il file `latin` che scarica il provider qui sopra non contiene alcun carattere Han, e i sottoinsiemi con nome sono incompleti: `chinese-simplified` di Noto Serif SC non ha 釵, e `chinese-traditional` di Noto Serif TC non ha nessuno dei segni a larghezza piena （），！？：；.

Per questo un provider può rispondere per un font con più file. `renderToPdf` gli passa i caratteri che le pagine compongono in quel font (`request.codePoints`), raccolti da ogni capitolo prima di disegnare qualsiasi cosa, e il provider restituisce i file che li contengono, nell’ordine in cui il browser li consulta. Ogni file viene incorporato come sottoinsieme a sé e ogni carattere viene disegnato dal primo file che ha un glifo per esso: un capitolo che tocca 60 porzioni incorpora 60 piccoli sottoinsiemi. A un font chiesto di nuovo, per esempio per il testo di una figura SVG, vengono chiesti solo i caratteri che mancano ai suoi file. Un provider che restituisce un solo `Uint8Array` funziona come prima. Il provider della Sandbox legge il foglio di stile di Fontsource del peso e dello stile e scarica i file i cui intervalli contengono il testo; il nucleo:

```ts
import type { PdfFontProvider } from 'postext-pdf';
import { decompressWoff2 } from 'postext-pdf';

type Slice = { url: string; ranges: Array<[number, number]> };

async function fontsourceSlices(family: string, weight: number, style: 'normal' | 'italic'): Promise<Slice[]> {
  const id = family.toLowerCase().replace(/\s+/g, '-');
  const cssUrl = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/${weight}${style === 'italic' ? '-italic' : ''}.css`;
  const css = await (await fetch(cssUrl)).text();
  return [...css.matchAll(/@font-face\s*{([^}]*)}/g)].map(([, rule]) => ({
    url: new URL(/url\(\.?\/?([^)]+\.woff2)\)/.exec(rule)![1], cssUrl).href,
    ranges: /unicode-range:\s*([^;]+);/.exec(rule)![1].split(',').map((part) => {
      const [lo, hi = lo] = part.trim().slice(2).split('-');
      return [parseInt(lo, 16), parseInt(hi, 16)] as [number, number];
    }),
  }));
}

export const sliceFontProvider: PdfFontProvider = async (family, weight, style, request) => {
  // Where ranges overlap, the browser tries the last rule first.
  const slices = (await fontsourceSlices(family, weight, style)).reverse();
  const picked = new Set<Slice>();
  for (const cp of request?.codePoints ?? []) {
    const slice = slices.find((s) => s.ranges.some(([lo, hi]) => cp >= lo && cp <= hi));
    if (slice) picked.add(slice);
  }
  if (picked.size === 0) picked.add(slices[0]!);
  return Promise.all(slices.filter((s) => picked.has(s)).map(async (s) =>
    decompressWoff2(new Uint8Array(await (await fetch(s.url)).arrayBuffer()))));
};
```

Una famiglia latina passa per lo stesso codice: un testo inglese riceve solo il suo file `latin`, uno ceco riceve `latin` e `latin-ext`.

Un browser non si ferma al primo file con un glifo. Compone un carattere nel file dichiarato per ultimo tra quelli il cui `unicode-range` lo contiene, e i file di una famiglia si sovrappongono: Fontsource ripete le lettere ASCII di Noto Serif JP nel suo file `latin` e nelle sue porzioni numerate. Due caratteri composti in due file diversi non hanno crenatura tra loro, quindi è il file a decidere la larghezza: Chrome 154 compone `Shift_JIS` largo 4,25 em con le porzioni dichiarate dopo `latin`, mentre il solo file `latin`, che applica la crenatura a `t_` e `_J`, lo compone largo 4,32 em. Da postext-pdf 1.30 un provider può dire come la pagina ha dichiarato ogni file: `declaredFontFile(bytes, { unicodeRange, order })` restituisce gli stessi byte con l’`unicode-range` della regola del file, come lo scrive il CSS, e la posizione di quella regola tra le regole del font, un numero qualsiasi che cresce con l’ordine di dichiarazione (il più grande è dichiarato per ultimo).

```ts
import { declaredFontFile } from 'postext-pdf';

declaredFontFile(bytes, { unicodeRange: 'U+0000-00FF,U+0131,U+0152-0153', order: 0 });
```

Quando i file di un font portano i loro intervalli, in un documento modellato nella propria lingua ([`bodyText.shapingLanguage: 'document'`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), il valore predefinito), un carattere viene disegnato dal file dichiarato per ultimo il cui intervallo lo contiene e che ha un glifo per esso, come un browser sceglie tra le regole `@font-face`, e una lettera e i suoi segni combinanti restano in un solo file. Un carattere che nessun intervallo contiene viene disegnato dal primo file con un glifo. I file semplici, e ogni file in un documento con `'host'`, vengono letti nell’ordine dato, come prima, quindi un provider che descrive i suoi file elenca comunque per primo quello dichiarato per ultimo. A un font descritto in questo modo vengono chiesti di nuovo tutti i caratteri che non gli erano stati chiesti prima, perché un file dichiarato più tardi potrebbe contenerli. Il provider della Sandbox descrive i suoi file.

I caratteri per cui nessun file del font ha un glifo vengono disegnati con il glifo `.notdef` del font (un riquadro vuoto nella maggior parte dei font), e `renderToPdf` li segnala una volta per font dopo aver disegnato le pagine:

```ts
// { kind: 'missingGlyph', family: 'Noto Serif TC', weight: 400, style: 'normal',
//   characters: ['，', '！', '？'], message: '…' }
```

La Sandbox li elenca, insieme ai due avvisi qui sotto, nel suo pannello Verifiche dopo ogni PDF che genera. Quando il libro, le sue impostazioni o le sue risorse cambiano, vengono segnati come provenienti da un PDF precedente finché il successivo non li sostituisce; aprire un altro libro li cancella.

- **Il grassetto richiede un file statico per peso.** Fontsource serve ogni peso di Noto Serif SC e TC come file statici separati, quindi nella Sandbox il grassetto funziona. I file di Google Fonts (`NotoSerifSC[wght].ttf`, 25 MB) sono font variabili: pdf-lib ne incorpora l’istanza predefinita, quindi un font 700 viene stampato a 400, e `renderToPdf` lo segnala come `variableFontDefaultInstance`. Per un pacchetto, ricava con fontTools un’istanza statica per peso (`fonttools varLib.instancer NotoSerifSC[wght].ttf wght=700`) e riducila ai caratteri del libro con `pyftsubset`.
- **Usa le versioni TrueType.** Source Han Serif e i file `.otf` di Noto Serif CJK hanno contorni CFF, che postext-pdf incorpora per intero, da 8 a 25 MB per peso; un font CFF di oltre 2 MB viene segnalato come `cffEmbeddedWhole`. Delle versioni TrueType (Google Fonts, Fontsource) si incorpora solo il sottoinsieme dei glifi usati. Per il giapponese, Noto Serif JP e Noto Sans JP (Google Fonts, o le porzioni numerate di Fontsource), Shippori Mincho, Zen Old Mincho e BIZ UDMincho sono in TrueType; Source Han Serif JP e i file `.otf` `JP` di Noto Serif CJK sono CFF.
- **Forme giapponesi di un font pan-CJK.** Uno stesso code point di un Han, della punteggiatura o delle virgolette può essere disegnato in un modo in Giappone e in un altro in Cina, e un font pan-CJK (Source Han, Noto CJK) li contiene entrambi. Il PDF modella un documento giapponese (`locale: 'ja'`), e un isolamento in giapponese (`:ltr[…]{lang=ja}`) in qualsiasi documento, con il sistema linguistico OpenType `JAN `, così la funzione `locl` del font stampa le forme giapponesi che il canvas e l’HTML stampano tramite `lang`. Un isolamento in un’altra lingua dentro un libro giapponese viene modellato con le forme di quella lingua (quelle predefinite del font per il cinese con `'host'`) e taggato come `Span` con il suo `/Lang`. Con [`bodyText.shapingLanguage: 'document'`](https://postext.dev/it/docs/configuration-text.md#testo-corrente), il valore predefinito da postext 1.30, ogni documento e ogni isolamento è modellato nella propria lingua (`ZHS` per il cinese semplificato), come l’ha misurato l’impaginazione; con `'host'`, i documenti cinesi e gli altri sono modellati con le forme predefinite del font, come prima. Un font fatto per il giapponese come Noto Serif JP ha forme predefinite giapponesi; compone comunque le “ ” del testo giapponese nelle loro forme `JAN `.

Un carattere che manca in una famiglia non viene preso da un’altra: Noto Serif TC non prende in prestito da Noto Serif SC. Risolvi la copertura quando costruisci i file dei font; la vetrina 红楼梦 copia dal font SC i glifi che mancano ai suoi sottoinsiemi TC.

### Provider di font lato server (Node, file locali)

In Node puoi saltare del tutto il passaggio WOFF2 e leggere i file TTF/OTF dal disco:

```ts
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { PdfFontProvider } from 'postext-pdf';

const FONT_DIR = '/path/to/fonts';

function filename(family: string, weight: number, style: 'normal' | 'italic'): string {
  const slug = family.replace(/\s+/g, '');
  const styleSuffix = style === 'italic' ? 'Italic' : '';
  const weightName =
    weight >= 700 ? 'Bold'
    : weight >= 600 ? 'SemiBold'
    : weight >= 500 ? 'Medium'
    : weight >= 300 ? 'Light'
    : 'Regular';
  return `${slug}-${weightName}${styleSuffix}.ttf`;
}

export const localFontProvider: PdfFontProvider = async (family, weight, style) => {
  const buf = await readFile(join(FONT_DIR, filename(family, weight, style)));
  return new Uint8Array(buf);
};
```

### Byte delle risorse e master di stampa

`resourceBytes(fileId)` restituisce i byte grezzi di un’immagine, e il backend ne riconosce il formato:

- PNG, JPEG, GIF e WebP vengono incorporati come immagini;
- il markup SVG viene disegnato come tracciati vettoriali, con il suo `text` composto come testo vero nei font incorporati del documento, oppure rasterizzato a 600 dpi nel browser quando usa funzioni fuori dal sottoinsieme vettoriale. Uno `<style>` che contiene solo regole `@font-face` (font incorporati da un autore) viene saltato e mantiene vettoriale la figura (da postext-pdf 1.25); qualsiasi altro foglio di stile la trasforma in un raster, realizzato con i font nominati dal suo testo incorporati da `fontProvider` (a meno che `diagramStyle.inlineFonts` o lo `svg.inlineFonts` della risorsa non sia `false`);
- di un PDF viene incorporata così com’è la prima pagina, come form XObject.

Ogni immagine viene memorizzata nel file una sola volta, per quante volte venga disegnata. Un SVG disegnato come tracciati vettoriali diventa un form XObject che ogni pagina dipinge, quindi una cornice o un logo nel design della pagina di un documento di trenta pagine viene scritto una volta, non trenta; ogni pagina in più aggiunge qualche centinaio di byte. Fino a postext-pdf 1.4 ogni pagina portava la propria copia dei tracciati.

Una figura SVG può indicare un **master di stampa** in `svg.pdfFileId`: un PDF di una sola pagina della stessa figura, di solito l’originale da cui l’SVG è stato esportato. `renderToPdf` chiede prima a `resourceBytes` l’id del master. Incorpora quella pagina al posto dell’SVG, con i suoi font, sfumature e spazi colore intatti, ovunque l’SVG venga disegnato: come figura, come immagine di una cella di tabella (`TableCell.image`), come immagine di design o come icona di un riquadro (anche il suo `marker`). Il VDT porta l’id del master in ognuno di questi usi (`svg.pdfFileId` sulla risorsa della figura, `pdfFileId` su un’immagine di cella e su un blocco di immagine di design), così in un libro ogni capitolo riceve il master. I backend canvas e HTML continuano a disegnare l’SVG. Si usano invece i byte dell’SVG stesso in tre casi: il master manca, non è un PDF, oppure è attivo il solo inchiostro (`diagramStyle.singleInk` ricolora solo il markup SVG).

```ts
const resources: Resource[] = [{
  id: 'map', typeId: 'figure', kind: 'svg', createdAt: 0, updatedAt: 0,
  svg: { fileId: 'map.svg', width: 800, height: 600, pdfFileId: 'map.pdf' },
}];
const files = new Map([['map.svg', svgBytes], ['map.pdf', masterPdfBytes]]);
const pdf = await renderToPdf(buildDocument({ markdown, resources }, config), {
  fontProvider,
  resourceBytes: (fileId) => files.get(fileId),
});
```

Un ospite può anche restituire i byte del master per l’id dell’SVG stesso, come fa `bundleResourceBytes`; funzionano entrambi i modi.

### Testo verticale nel PDF

Una pagina verticale (`layout.writingMode: 'vertical-rl'`) viene disegnata tramite un riferimento ruotato di un quarto di giro, come la dipinge il canvas, e il suo testo è composto lungo la colonna:

- **I caratteri in piedi** sono mostrati tramite un secondo font Type0 dello stesso file incorporato: lo stesso CIDFont, le stesse larghezze e la stessa mappa ToUnicode, con `Encoding /Identity-V` (modo verticale). Una sequenza di caratteri è un unico oggetto di testo i cui glifi avanzano da soli di un em lungo la colonna (`DW2 [880 −1000]`), così i lettori selezionano ed estraggono una colonna come un’unica riga. I glifi sono modellati con le funzioni OpenType `vert` e `fwid`, che danno le loro forme verticali a parentesi, virgolette, segni di pausa della Cina continentale, ellissi e lineette; un carattere che sta già in piedi così com’è mantiene il suo glifo orizzontale. Niente del font viene incorporato due volte.
- **Le parole latine e i numeri lunghi** corrono di lato con il font orizzontale; **un numero in una sola cella** sta in piedi, compresso trasversalmente fino all’em quando è più largo; un segno per cui il font non ha forma verticale viene ruotato o spostato, come sul canvas.
- **La spaziatura tra lettere** tra i caratteri è scritta come numeri `TJ`, che in modo verticale spostano la penna lungo la colonna.
- **Ogni riga verticale** è marcata con un `/ActualText` del suo testo, così la copia e l’estrazione del testo la leggono come è scritta. `pdftotext` e pdf.js leggono le colonne dall’alto in basso, da destra a sinistra; pdf.js comincia una nuova riga a un numero composto in una sola cella.
- **Link, segnalibri e destinazioni** sono riportati sul foglio: un link su una riga verticale è un rettangolo alto e stretto, e un segnalibro apre la pagina in cima alla colonna del suo titolo.
- **Un PDF taggato** dichiara il modo di scrittura sul suo elemento `Document` (l’attributo di layout `WritingMode /TbRl`, che ogni elemento eredita); la validazione PDF/UA-1 (veraPDF) passa su un capitolo verticale.
- **Visualizzatori:** Acrobat, Anteprima, Chrome (PDFium), pdf.js e Poppler rendono i font verticali. Un libro rilegato a destra (`page.binding`) chiede anche ai visualizzatori di disporre le sue doppie pagine da destra a sinistra (`/Direction /R2L`, `/PageLayout /TwoPageRight`); Acrobat e Foxit lo seguono, Chrome no.

Un capitolo di 43 pagine composto in Noto Serif TC (i caratteri del libro, TrueType) pesa circa 820 KB, di cui la maggior parte sono i due sottoinsiemi di font.

### Link nel PDF

Le parole di un link Markdown (vedi [Formato del documento › Link](https://postext.dev/it/docs/document-format.md#link)) diventano annotazioni di link URI, una per ogni sequenza di parole collegate su una riga. Ognuna copre il riquadro della riga e non ha bordo. In un rendering accessibile, ogni sequenza è un elemento `Link` il cui `/Contents` è il suo testo. Vengono collegate solo le destinazioni assolute `http:`, `https:`, `mailto:`, `tel:` e `ftp:`, perché un URL relativo non ha una base dentro un PDF. I caratteri fuori dall’ASCII stampabile vengono codificati con il segno di percentuale. I rimandi `:ref` e le righe dell’indice mantengono i loro link dentro il documento.

### Esempio completo per il browser: costruire, rendere, scaricare

Mettendo tutto insieme: costruisci il VDT, rendilo in PDF e avvia un download dal browser:

```tsx
import { buildDocument, createMeasurementCache } from 'postext';
import { renderToPdf } from 'postext-pdf';
import { createPdfFontProvider } from './pdfFontProvider';

const fontProvider = createPdfFontProvider();

export async function downloadPdf(markdown: string, config: PostextConfig) {
  const cache = createMeasurementCache();
  const vdt = buildDocument({ markdown }, config, cache);

  const bytes = await renderToPdf(vdt, { fontProvider });

  const blob = new Blob([bytes.slice().buffer], { type: 'application/pdf' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'document.pdf';
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
```

**Importante:** chiama `ensureConfigFontsLoaded(config)` (o un equivalente) *prima* di `buildDocument` quando la tua configurazione fa riferimento a web font. L’impaginazione viene misurata con le metriche che il browser ha in quel momento per quella famiglia: se il font vero non è ancora arrivato, il VDT viene misurato con un font di ripiego e il PDF non corrisponderà all’output canvas o HTML. La Sandbox lo fa esplicitamente prima di ogni rendering (vedi `packages/postext-sandbox/src/viewport/PdfViewport.tsx`).

### Esempio dal vivo: un PDF nel browser

Il flusso completo qui sopra, in esecuzione nel browser: il pen importa `postext` e `postext-pdf` da una CDN, carica i web font, costruisce il documento, incorpora le varianti di Fontsource tramite il provider di font e passa i byte a un link che apre il file in una nuova scheda e a un link di download. Il PDF che ottieni ha gli stessi a capo dell’output canvas e HTML, veri font incorporati e segnalibri di struttura.

> **Esempio eseguibile: Postext · generare un PDF nel browser** — Impagina un documento Markdown con postext e rendilo in PDF con postext-pdf. ([codice](https://github.com/drnachio/postext/tree/main/docs/examples/render-pdf))

### Rendere il PDF in un worker

`postext-pdf/worker` sposta `renderToPdf` fuori dal thread principale. Il worker scrive il testo, le figure vettoriali, l’albero della struttura e il file stesso. Due compiti richiedono la pagina, quindi il worker li chiede al thread principale: scaricare i font e rasterizzare un SVG tramite un `<img>`. Per un libro di centinaia di pagine, il rendering richiede secondi che altrimenti bloccherebbero la pagina; per poche pagine, chiamare direttamente `renderToPdf` è più semplice.

```ts
import { createPdfWorker } from 'postext-pdf/worker';

const pdfWorker = createPdfWorker();
const bytes = await pdfWorker.render(docs, {
  fontProvider,                                     // runs on this thread
  resourceBytes: new Map([['map.svg', svgBytes]]),  // a Map; its buffers move to the worker
  onProgress: ({ phase, pages, totalPages }) => showProgress(phase, pages, totalPages),
  onWarning: (w) => console.info(w.message),
});
pdfWorker.dispose();
```

- `render(docs, options)` prende un documento o l’array dei documenti dei capitoli di un libro. Accetta le opzioni di `renderToPdf`, con due differenze. `resourceBytes` è una `Map<string, Uint8Array>` i cui buffer vengono trasferiti, quindi passa copie dei byte che vuoi conservare. `rasterizeSvg`, se è fornito, viene eseguito sul thread principale; per impostazione predefinita se ne occupano l’`Image` e il canvas della pagina stessa.
- Un handle rende un documento alla volta. `dispose()` termina il worker e rifiuta qualsiasi rendering ancora in sospeso.
- `createPdfWorker({ worker })` accetta un `Worker` creato da te, per gli strumenti di build che controllano gli URL dei worker. Quel worker deve eseguire `postext-pdf/worker/entry`.

**Da una CDN.** Per impostazione predefinita lo script del worker viene caricato dall’URL del pacchetto stesso (`new URL('./pdf.worker.js', import.meta.url)`). Una pagina su un’altra origine potrebbe non riuscire ad avviarlo: importato da `esm.sh`, `createPdfWorker()` solleva `Failed to construct 'Worker': Script at 'https://esm.sh/postext-pdf@…/pdf.worker.js' cannot be accessed from origin …`. Avvia invece un module worker della stessa origine che importa l’entry (se fissi una versione, fissa la stessa in entrambi gli URL):

```js
import { createPdfWorker } from 'https://esm.sh/postext-pdf/worker';

const entry = URL.createObjectURL(new Blob(
  ["import 'https://esm.sh/postext-pdf/worker/entry';"],
  { type: 'text/javascript' },
));
const pdfWorker = createPdfWorker({ worker: new Worker(entry, { type: 'module' }) });
```

Il worker di impaginazione di `postext/worker` richiede lo stesso involucro attorno a `postext/worker/entry` (vedi [Eseguire l’impaginazione in un Web Worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#eseguire-limpaginazione-in-un-web-worker)).

### PDF pronti per la stampa

Per i flussi di lavoro di stampa in produzione, regola queste opzioni di configurazione prima del rendering:

- **`page.cutLines.enabled: true`**: aggiunge l’abbondanza e i segni di taglio attorno al formato rifilato, e dà a ogni pagina un TrimBox e un BleedBox. Vedi [Linee di taglio](https://postext.dev/it/docs/configuration-page-layout.md#segni-di-taglio).
- **`print: { standard: 'pdfx4', outputProfile: 'fogra51' }`** (o `'pdfx1a'`): un file PDF/X con l’output intent, l’identificazione e i riquadri che controlla una tipografia; ogni colore e ogni immagine RGB separati tramite il profilo ICC, sovrastampa del 100 % K e grandi aree nere in nero ricco. Vedi [Produzione per la stampa (configurazione)](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#produzione-di-stampa-configurazione).
- **`colorSpace: 'cmyk'`** (o `pdfGeneration: { forceColorSpace: true, colorSpace: 'cmyk' }`): la stessa separazione senza l’identificazione PDF/X (i segni di taglio sono sempre in colore di registro). I master di stampa PDF vengono incorporati così come sono.
- **`page.dpi: 300`**: i px per pollice dell’impaginazione: una bitmap senza una risoluzione propria viene stampata a questa risoluzione alla sua dimensione naturale. Il [preflight](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#preflight) segnala le immagini sotto i 300 ppi alla dimensione di stampa.
- **`ColorValue.cmyk`**: un colore definito in CMYK viene stampato con i suoi valori esatti.
- **`{ pageNegative: true }`** in `RenderToPdfOptions`: inverte l’area del formato rifilato con un metodo di fusione Differenza (i segni di taglio restano non invertiti). Utile per i controlli di preflight sulla tipografia scuro su chiaro.

### Implementazione di riferimento

Il componente `PdfViewport` della Sandbox (`packages/postext-sandbox/src/viewport/PdfViewport.tsx`) collega i pezzi descritti sopra in un’anteprima dal vivo con i pulsanti per rigenerare, scaricare e stampare, ed è un buon punto di partenza per qualsiasi integrazione PDF nel browser. Costruisce il VDT tramite il worker di impaginazione condiviso (vedi [Eseguire l’impaginazione in un Web Worker](https://postext.dev/it/docs/configuration-programmatic-usage.md#eseguire-limpaginazione-in-un-web-worker)), così un clic su *Ricalcola* non blocca l’interfaccia mentre la pipeline è in esecuzione: il thread principale gestisce solo `renderToPdf` (che è già veloce una volta che il VDT esiste).

## Un libro in 3D (`postext-folio`)

`postext-folio` presenta sullo schermo un documento impaginato come un libro stampato aperto su una scrivania: doppie pagine secondo la regola del recto, e fogli che chi legge gira con i pulsanti ‹ ›, i tasti freccia, uno swipe, un clic su una pagina, oppure prendendo una pagina per il bordo e trascinandola. Ogni foglio si incurva in three.js secondo la sua carta e proietta un’ombra vera sulle pagine sottostanti. Il canvas WebGL disegna il libro allo stesso modo sia fermo sia mentre si gira, quindi una pagina non cambia mai aspetto quando si posa. È il visualizzatore delle ricette del Ricettario e della scheda **Folio** della Sandbox.

```bash
npm install postext postext-folio three
```

```ts
import { buildDocument } from 'postext';
import { createFolioFromDocument } from 'postext-folio';

const doc = buildDocument({ markdown }, config);
const book = createFolioFromDocument(document.getElementById('book')!, doc, {
  onChange: ({ pages }) => console.log('showing pages', pages),
});

// After an edit: the same viewer, on the same page.
book.setDocument(buildDocument({ markdown: edited }, config));
```

- **Le pagine vengono dipinte quando servono.** `createFolioFromDocument` dipinge ogni pagina con `renderPageToCanvas` esattamente ai pixel del dispositivo di uno slot di pagina (WebGL poi la mostra un texel per pixel, nitida come l’anteprima canvas), e solo le doppie pagine attorno a quella aperta (`window`, tre per lato per impostazione predefinita). Le pagine che escono da quella finestra vengono liberate, quindi un libro di mille pagine costa la memoria di poche. Un salto a una pagina lontana dipinge prima quella doppia pagina. Fino a dieci pagine di distanza i fogli si girano uno per uno; oltre, il blocco di pagine intermedie si solleva come un’unica lastra, spessa quanto quelle pagine (la somma dei loro spessori), e si posa dall’altra parte. `setDocument` mantiene il dipinto di ogni pagina che nella nuova impaginazione risulta uguale (`{ repaint: true }` le ridipinge tutte, dopo l’arrivo di un’immagine).
- **Il documento definisce il libro.** La sua prima pagina si apre da sola a destra quando è un recto (`pageIndexOffset` pari), un libro rilegato a destra (`page.binding: 'right'`, o un documento verticale) si presenta specchiato e si gira verso sinistra, le pagine bianche prendono il colore di sfondo della pagina, e la larghezza del formato rifilato della pagina (`pageWidthMm`) scala lo spessore della carta e i piatti. Un capitolo impaginato con una `continuation` conta le altre pagine del libro (`pageIndexOffset` prima, `bookPageCount` dopo) nello spessore dei due blocchi di pagine senza disegnarle (`extraPages`).
- **Il documento definisce l’aspetto.** La carta, la rilegatura, la scrivania e la luce sono le [impostazioni `folio`](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#visualizzatore-folio-configurazione) del documento (`doc.config.folio`). Una pagina composta dentro una sequenza `:::paper` porta la propria carta (`VDTPage.paper`), e il suo foglio viene disegnato con il colore, la superficie, lo spessore e la rigidità di quella carta. Con `binding.cover: 'pages'` la prima pagina è il piatto anteriore e l’ultima, quando è un verso, il piatto posteriore (`covers`). Un formato da quotidiano (`'broadsheet'`, `'berliner'`, `'tabloid'`, `'compact'`) le cui impostazioni non indicano né carta né rilegatura si presenta come carta da giornale piegata, anche quando l’ospite passa un proprio `folio`.
- **Il contenitore definisce la dimensione.** Il libro lo riempie, con i pulsanti e il conteggio delle pagine nei margini, quindi dai un’altezza al contenitore; un ridimensionamento ridipinge le pagine alla nuova dimensione. Sotto i 560 px di larghezza mostra una pagina alla volta (`mode: 'auto'`; `'single'` e `'double'` forzano l’una o l’altra modalità): il dorso corre lungo il bordo interno della pagina e il foglio gli si gira sopra, un trascinamento verso il dorso va avanti, uno swipe in direzione opposta torna indietro, un tocco gira la pagina.
- **Che cosa fa il puntatore.** `interaction` (e poi `setInteraction`) stabilisce che cosa fanno sul libro il tasto sinistro, un dito o una penna: `'hand'` (il valore predefinito) prende e gira le pagine, `'orbit'` ruota la vista come fa un trascinamento con il tasto destro (per trackpad e tablet), `'select'` lascia il puntatore all’ospite, per esempio per selezionare il testo. `pageAt(event)` restituisce la pagina sotto un puntatore e il punto su di essa (`{ page, x, y }`, frazioni della pagina a partire dal suo angolo in alto a sinistra), sul libro così come lo si vede, inclinato o ruotato; `pointOnScreen(point)` fa il percorso inverso, per disegnare un cursore o una selezione sopra la pagina. `refreshPage(src)` mostra di nuovo un canvas di pagina che l’ospite ha ridisegnato sul posto. La Sandbox li usa tutti per selezionare il testo e seguire il cursore dell’editor sulle pagine 3D.
- **Una lente d’ingrandimento per i caratteri piccoli.** `interaction: 'magnify'` tiene una lente rotonda con un bordo nero sopra il libro dove si trova il puntatore (un dito la tiene sopra di sé mentre tocca lo schermo). Mostra il libro come lo vede l’occhio di chi legge, illuminato e curvo come giace, ingrandito al massimo al centro e incurvato verso il bordo. La rotellina, `+` e `−` cambiano l’ingrandimento (`setMagnification(zoom)`, da 1,5 a 10; il valore predefinito mostra la pagina a circa 5,5 px CSS per millimetro) ed Esc la ripone; `magnifier: { zoom, diameter }` imposta entrambi fin dall’inizio. `createFolioFromDocument` ridipinge le pagine sotto la lente, abbastanza nitide per il suo centro, così il testo corrente di un quotidiano si legge; `createFolio` prende questi dipinti da `detail: { paint(index, deviceWidth), release() }`. La Sandbox la mette sul pulsante **Lente d’ingrandimento** (M). La lente seleziona anche il testo: sopra le pagine il cursore è un cursore di testo, `pageAt` restituisce il punto sotto il centro della lente (sopra il dito su uno schermo tattile), e nella Sandbox un clic lì colloca il cursore e un trascinamento seleziona.
- **Chi legge può guardarci intorno.** Un trascinamento con il tasto destro fa ruotare la vista attorno al libro (fino a 70° dalla verticale), anche mentre i fogli si girano; `resetView()` la riporta dolcemente al `tilt` e allo `yaw` delle impostazioni, e `getView()` restituisce la vista così come la si vede ora (`{ tilt, yaw }`, in gradi) da memorizzare come quelle impostazioni. La Sandbox li mette su due pulsanti, **Reimposta la vista** e **Salva come vista predefinita**.
- **I video si riproducono sulle pagine.** Un clic sul poster di un video lo riproduce sulla pagina, in ogni modalità di `interaction`, e continua a suonare mentre il suo foglio si gira; un altro clic lo mette in pausa, e si ferma quando il libro si posa su una doppia pagina che non lo mostra. Un video con `player.autoplay` parte da solo la prima volta che viene mostrata la sua doppia pagina; uno che si riproduce anche insieme agli altri (`player.exclusive: false`) parte, senza audio, ogni volta e si ferma quando la doppia pagina viene girata, anche più di uno alla volta. Opzioni `videos`, `videoUrl` e `onVideo`, e `stopVideo()` sul visualizzatore; vedi [Formato del documento › Video sulle pagine di Folio](https://postext.dev/it/docs/document-format.md#video-sulle-pagine-di-folio).
- **Prima i font e le immagini.** Come per `renderPage`, i font che il documento usa devono essere caricati in `document.fonts` e le sue immagini delle risorse registrate con `registerResourceImage` prima che le pagine vengano dipinte.
- **Accessibile.** Il visualizzatore è un gruppo che può ricevere il focus e accetta ←/→ (specchiati per un libro rilegato a destra), Pagina su/giù, Home e Fine; i suoi pulsanti e il conteggio delle pagine hanno un’etichetta (`labels` li traduce), e ogni canvas di pagina porta un testo `alt` (`alt: (index) => …`).
- **Senza WebGL2**, o quando chi legge chiede di ridurre il movimento, le doppie pagine semplicemente cambiano. I libri WebGL sono pesanti per un telefono (una texture per ogni lato di pagina): la Sandbox offre la scheda Folio solo dove WebGL2 è disponibile e il lato corto dello schermo è di almeno 600 px. `canFlip()` dice se qui i fogli si gireranno in 3D: WebGL2, e nessuna riduzione del movimento.

### Aspetto

L’opzione `appearance`, e poi `setAppearance`, ridefiniscono ciò che dice il documento. Tutto ciò che resta fuori mantiene il valore del documento:

```ts
const book = createFolioFromDocument(container, doc, {
  appearance: {
    folio: {
      tilt: 22,
      paper: { type: 'bookWove', texture: 'laid' },
      binding: { type: 'hardcover', coverColor: { hex: '#5a1f1f', model: 'hex' } },
      surface: { type: 'walnut' },
      lighting: { environment: 'lamp' },
    },
    // Photographed desks: a folder laid out as postext.dev's /folio/textures/
    // (manifest.json and one folder per desk). Without it, procedural maps.
    textureBaseUrl: '/folio/textures',
    // The picture for `folio.binding.spineImage`: the resource's URL,
    // or a drawn canvas or image.
    spineImage: spineUrl,
  },
});

// A settings panel: the book redrawn in place, nothing painted again.
book.setAppearance({ folio: { ...folio, lighting: { environment: 'daylight' } } });
book.resetView();
```

| Campo | Che cosa fa |
| --- | --- |
| `folio` | Le [impostazioni `folio`](https://postext.dev/it/docs/configuration-fonts-colors-viewers.md#visualizzatore-folio-configurazione): inclinazione, carta, rilegatura, superficie, illuminazione. Se è fornito, sostituisce quello del documento. |
| `pageWidthMm` | La larghezza del formato rifilato di una pagina in mm, rispetto a cui si scalano lo spessore della carta e i piatti. Dal documento: la pagina rifilata ai suoi dpi. Predefinito 150 per `createFolio`. |
| `extraPages` | `{ before, after }`: pagine del libro al di fuori di quelle fornite, contate per lo spessore dei blocchi di pagine e mai disegnate. |
| `covers` | `{ front, back }`: la prima pagina fornita è la copertina anteriore, l’ultima la copertina posteriore (quando cade su un verso). Si girano come piatti e non viene disegnata alcuna custodia. Dal documento: `binding.cover: 'pages'` su un libro che comincia dalla sua prima pagina e finisce con l’ultima. |
| `spineImage` | L’immagine stampata sul dorso, come URL, canvas o immagine. `createFolioFromDocument` non cerca le risorse: passa l’immagine della risorsa indicata da `folio.binding.spineImage`. Ignorato con la cucitura a punto metallico. |
| `textureBaseUrl` | Dove vengono servite le texture fotografate delle scrivanie. Finché non si caricano, o senza di esso, la scrivania viene disegnata con mappe procedurali. |

`createFolio(container, { pages })` è lo stesso visualizzatore su pagine qualsiasi: URL di immagini, elementi `<img>` o `<canvas>`, e `""` per una pagina bianca; una pagina può essere `{ src, alt, paper }`, con `paper` una carta in stile `:::paper` per quel foglio. `PageFlipper` è il solo motore three.js, per un ospite che dispone da sé il DOM delle proprie doppie pagine; `FlatPageFlipper` è lo sfogliamento piatto che precede il libro 3D, mantenuto per il tavolo luminoso del Ricettario. L’elenco completo delle opzioni è nel [README del pacchetto](https://www.npmjs.com/package/postext-folio).

### Esempio dal vivo: un libro in 3D

Il pen importa `postext` e `postext-folio` da una CDN, impagina un breve documento e lo apre come un libro. Prendi la pagina di destra per il bordo e trascinala.

> **Esempio eseguibile: Postext · un documento come libro in 3D** — Impagina un documento Markdown con postext e sfoglia le sue pagine in 3D con postext-folio. ([codice](https://github.com/drnachio/postext/tree/main/docs/examples/render-folio))

### Esempio dal vivo: immagini di pagina

`createFolio` con pagine disegnate su canvas, un’ultima pagina bianca e il colore della carta.

> **Esempio eseguibile: Postext · un libro di immagini in 3D** — Sfoglia in 3D pagine qualsiasi con postext-folio: URL di immagini, elementi img o canvas. ([codice](https://github.com/drnachio/postext/tree/main/docs/examples/folio-images))

## Libri EPUB (`postext-epub`)

`postext-epub` scrive un libro impaginato come file EPUB 3.3, nel browser o in Node, senza server. Legge gli stessi documenti di capitolo che `renderToPdf` riceve per un libro, quindi numeri di pagina, note, citazioni, rimandi, indice e indice analitico arrivano già risolti, e restituisce il file come byte. È il generatore dietro la scheda **EPUB 3** della Sandbox.

```bash
npm install postext postext-epub
```

`postext` è una dipendenza peer, come per `postext-pdf`: aggiorna i due pacchetti insieme e, su una CDN, fissali alla stessa versione.

### Layout fisso e fluido

EPUB 3 definisce due rendition, impostate dalla proprietà `rendition:layout` del pacchetto; `layout` ne sceglie una:

|  | `layout: 'fixed'` | `layout: 'reflowable'` |
| --- | --- | --- |
| Nome EPUB | `pre-paginated` (layout fisso, FXL) | `reflowable`, il valore predefinito di EPUB |
| Documenti di contenuto | Un documento XHTML per ogni pagina stampata, al formato rifilato della pagina in px CSS | Un documento XHTML per capitolo (una parte ne apre uno proprio) |
| Che cosa conserva | La pagina: colonne, elementi flottanti, testatine, aperture, a capo e posizioni, nei font incorporati. Il testo resta testo vero: selezionabile, ricercabile, leggibile ad alta voce | Il testo e la sua struttura: titoli, paragrafi ricostruiti dalle righe, elenchi, riquadri come aside, figure e tabelle dopo il testo che le cita, note, link, segnaposto delle pagine stampate. Un foglio di stile derivato dalla configurazione |
| A che cosa rinuncia | Alla scelta del carattere, del corpo e dei margini da parte di chi legge; su uno schermo piccolo la pagina viene ridotta | Alle colonne, alle testatine, al progetto grafico della pagina e agli a capo esatti |
| Doppie pagine e direzione | `page-spread-left` / `page-spread-right` in base alla parità e alla rilegatura; un libro rilegato a destra si legge da destra a sinistra | Direzione di lettura in base alla rilegatura; il cinese verticale mantiene `vertical-rl`, l’arabo è `dir="rtl"` |
| Adatto a | Pagine progettate: libri illustrati, manuali, cataloghi, riviste; schermi grandi | Testo continuo: romanzi, saggi, relazioni; telefoni e lettori e-ink |

Entrambe le rendition hanno la stessa navigazione: un indice ricavato dai titoli e dalle pagine di parte, un elenco delle pagine con le etichette delle pagine stampate, i punti di riferimento (copertina, indice stampato, inizio del corpo) e un NCX per i sistemi di lettura meno recenti.

### Scrivere un libro

```ts
import { openBundle, buildBundle } from 'postext';
import { renderToEpub } from 'postext-epub';

const bundle = await openBundle(fileBytes);
const docs = buildBundle(bundle); // one VDTDocument per chapter, in book order

const bytes = await renderToEpub(docs, {
  layout: 'reflowable',
  metadata: { title: 'Lantern', creators: ['Ada Lovelace'], language: 'en' },
  fonts: bundle.fonts.map((f) => ({ family: f.family, weight: f.weight, style: f.style, bytes: new Uint8Array(f.bytes), format: f.format })),
  resourceBytes: (fileId) => {
    const data = bundle.files.get(fileId);
    return data ? { bytes: data, mediaType: '' } : undefined;
  },
  onWarning: (w) => console.warn(w),
});
```

Un singolo documento è un libro di un capitolo: `renderToEpub([doc], options)`.

- **`renderToEpub(docs, options): Promise<Uint8Array>`** scrive il file. `options` è `{ layout, metadata, fonts?, svgFonts?, resourceBytes?, cover?, onProgress?, onWarning?, signal? }`.
- **`metadata`**: `title` e `language` (un tag BCP 47) sono obbligatori; `subtitle`, `creators`, `identifier`, `date`, `publisher`, `rights`, `description` e `modified` sono facoltativi. Un ISBN nudo diventa `urn:isbn:…`. Senza un `identifier` il libro riceve un `urn:uuid:` derivato dal titolo, dagli autori e dalla lingua, così una nuova versione dello stesso libro mantiene il suo posto nella biblioteca di chi legge. Passa anche `modified` per ottenere un output identico byte per byte.
- **`fonts`**: i font da incorporare, `{ family, weight, style, bytes, format, unicodeRange? }` con `format` tra `woff2`, `woff`, `ttf`, `otf`. Ogni font diventa un file e una regola `@font-face`; più file con il loro `unicodeRange` formano un unico font (le porzioni di Google Fonts). Una famiglia, un peso o uno stile usati dalle pagine senza un font incorporato vengono segnalati come `missingFont`, e i sistemi di lettura li sostituiscono con i propri. Incorpora solo font la cui licenza lo consente: un font con `redistributable: false` non viene mai scritto nel file (il libro può essere composto con esso, ma il suo file resta fuori, anche dalle immagini SVG).
- **`resourceBytes(fileId)`**: le immagini che le pagine collocano, in modo sincrono o asincrono, come `{ bytes, mediaType }`; un `mediaType` vuoto viene ricavato dai byte. Fornisci le bitmap come sono conservate e gli SVG come sorgente, non il master di stampa PDF (`svg.pdfFileId`). Ogni immagine viene conservata una sola volta. In un libro a un solo inchiostro (`diagramStyle.singleInk`) gli SVG vengono ricolorati nel file. Un SVG riceve incorporati i font che il suo testo nomina, perché un sistema di lettura lo mostra come un’immagine che non vede i font del libro (vedi [Font nel testo SVG](https://postext.dev/it/docs/configuration-resources.md#font-nel-testo-svg)): da `fonts` (le porzioni che contengono i suoi caratteri), poi da **`svgFonts.provider`** per una famiglia che il testo del libro non usa. Le famiglie di font marcati `redistributable: false`, e quelle indicate da `svgFonts.withhold(family)`, restano fuori e vengono segnalate una volta ciascuna come `fontWithheld`; una famiglia senza font viene segnalata come `svgFontUnavailable`, i font oltre `svgFonts.maxBytes` (2 MiB) come `svgFontsTooLarge`. `svgFonts.inline: false`, `diagramStyle.inlineFonts: false` e il `svg.inlineFonts: false` di una risorsa conservano i byte così come sono forniti. Un’immagine senza byte viene segnalata come `missingImage` e lasciata come cornice vuota.
- **`cover`**: `{ bytes, mediaType, alt? }`, un’immagine JPEG, PNG, WebP o SVG. Il libro si apre allora su un documento di copertina che la contiene, ed essa diventa il `cover-image` del pacchetto (la miniatura in una biblioteca). Senza copertina, il layout fisso indica la sua prima pagina come copertina e il libro a layout fluido non ha un’immagine di copertina.
- **`onProgress({ phase, done, total })`**: `resources` (font e immagini), `documents` (pagine per un layout fisso, capitoli per uno fluido), poi `package`. **`signal`** interrompe tra un passo e l’altro.
- **`readEpub(bytes)`** rilegge un file per un visualizzatore, senza `DOMParser`: layout, metadati, direzione di lettura, ogni file per percorso, il manifest, lo spine, l’indice, l’elenco delle pagine, il viewport del layout fisso e la copertina. Il lettore della Sandbox è costruito su di essa.

Entrambe le rendition contengono i metadati di EPUB Accessibility 1.1 (modalità di accesso, funzionalità come l’indice e i numeri delle pagine stampate, rischi e un riepilogo) e per impostazione predefinita non dichiarano alcuna conformità WCAG. L’elenco completo delle opzioni e i limiti si trovano nel [README del pacchetto](https://www.npmjs.com/package/postext-epub).

### Verificare un file con EPUBCheck

[W3C EPUBCheck](https://www.w3.org/publishing/epubcheck/) è il validatore di riferimento per EPUB; le librerie digitali verificano con esso i file che ricevono. Una volta installato (`brew install epubcheck`, oppure la versione Java), `epubcheck book.epub` elenca errori, avvisi e note d’uso; le note d’uso che l’output di Postext lascia (`CSS-028`, `OBS-001`, `HTM_062`) sono solo informative. Nel repository di Postext, `pnpm --filter postext-epub epubcheck` verifica i libri di esempio della suite di test, `node packages/postext-epub/scripts/epubcheck.mjs book.postext --layout both` impagina un file `.postext` o una cartella di preset e verifica entrambe le rendition, e `pnpm --filter postext-epub validate` esegue l’intera matrice di libri (la guida, i preset della vetrina, i libri cinesi, arabi e del Ricettario), ciascuno dei quali passa senza errori né avvisi.

## Pacchetti (file `.postext`)

Un **file `.postext`** è un libro intero in un solo file: un archivio zip che contiene un manifest `preset.json`, un file markdown per capitolo, i contenuti delle risorse (bitmap, SVG, master di stampa PDF) e i file dei font che la configurazione nomina. La [Sandbox](https://postext.dev/it/docs/sandbox.md#esportazione-e-importazione) lo esporta e lo importa e la [skill per agenti](https://postext.dev/it/docs/skill.md) lo consegna. Anche il pacchetto `postext` può crearlo e aprirlo, così un libro può passare tra questi strumenti e il tuo programma senza perdere nulla.

```
my-book.postext
├── preset.json            manifest: name, locale, chapters, config, resources, fonts
├── chapters/01-dusk.md
├── chapters/02-night.md
├── resources/lantern.svg
└── fonts/ebgaramond-400-normal.woff2
```

Il manifest è descritto campo per campo nell’appendice [Formato dei pacchetti di preset](https://postext.dev/it/docs/sandbox.md#formato-dei-pacchetti-preset) della Sandbox. Un file può contenere anche `layouts.json`, il numero di pagine calcolato dalla Sandbox, così lì il libro si apre già impaginato, oppure uno per ogni edizione di un libro multilingue (`layouts.zh-Hant.json`, letto per primo). `openBundle` li ignora.

L’API è esportata da `postext` stesso e dal sottopercorso `postext/bundle`, che aggiunge le funzioni di basso livello. Importa da `postext` quando devi anche disegnare. In questo modo gli adattatori dei pacchetti e i renderer condividono un’unica istanza del modulo, il che conta su una CDN come esm.sh, dove ogni punto di ingresso è una build separata.

### Aprire un pacchetto

`openBundle` riceve i byte del file (un `Uint8Array`, un `ArrayBuffer`, oppure un `Blob` / `File` da un `<input type="file">`) e restituisce tutto ciò di cui hanno bisogno il motore e i suoi backend:

```ts
import { openBundle } from 'postext';

const bundle = await openBundle(await file.arrayBuffer(), { locale: 'es' });

bundle.chapters;   // [{ title, file, markdown }, …] in book order
bundle.config;     // PostextConfig, ready for buildDocument
bundle.resources;  // Resource[]
bundle.files;      // Map<path, Uint8Array>: every file of the bundle
```

| Campo | Che cosa contiene |
| --- | --- |
| `manifest` | Il `preset.json` convalidato. |
| `id`, `name`, `description` | Presi dal manifest. |
| `locale`, `locales` | La lingua in cui è stato letto il contenuto, e ogni lingua che un pacchetto bilingue contiene. `options.locale` ne sceglie una: prima il tag esatto, poi la lingua di base, poi la lingua propria del pacchetto. |
| `chapters` | `{ title, file, markdown }` per capitolo. Un capitolo senza titolo nel manifest prende il testo del suo primo titolo `#`. |
| `config` | La tavolozza di colori predefinita e i tipi di risorsa nella lingua del pacchetto, poi il `config` del manifest, poi le sostituzioni della lingua. La lingua del pacchetto è il `locale` qui sopra, quindi un pacchetto in una sola lingua riceve le proprie etichette qualunque cosa chieda `options.locale`. Un manifest che non indica nessuna lingua prende quella impostata dal suo `config` (`locale`, poi la lingua di sillabazione), altrimenti `options.locale`. `customFonts` elenca le famiglie di font del pacchetto. È la stessa configurazione con cui la Sandbox apre il pacchetto. |
| `resources` | Le risorse, con le didascalie della lingua scelta. Una dimensione assente dal manifest viene letta dal file. |
| `fonts` | Una voce per font: `{ family, weight, style, format, file, bytes }`. |
| `files` | Ogni file dell’archivio, indicizzato per percorso. |
| `thumbnail`, `canvasScope` | Il percorso dell’immagine di copertina, e come il pacchetto chiede di essere visualizzato: il `view` del manifest, con sopra il `localized[…].view` della lingua servita. |
| `start` | Il punto in cui inizia il libro, per un pacchetto che contiene una parte di un libro più lungo: lo `start` del manifest, oppure il `localized[…].start` della lingua servita. Assente per un libro che inizia dal suo principio. |
| `warnings` | Problemi non bloccanti: un file di font non supportato, un master di stampa mancante. |

Il **`fileId` di un contenuto è il suo percorso dentro il pacchetto**. `resource.svg.fileId`, `resource.bitmap.fileId` e il `fileId` di ogni variante di `customFonts` si possono cercare direttamente in `bundle.files`. `openBundle` genera un errore quando i byte non sono uno zip, quando non c’è un `preset.json` valido (alla radice o sotto un’unica cartella di primo livello), o quando manca un file che il manifest nomina.

#### Pacchetti scritti da postext 1.4 o precedenti

Ogni manifest scritto da `createBundle` e dalla Sandbox porta `configVersion: 14`: le regole di configurazione per cui è stato scritto il suo `config`. Un manifest senza questo campo è stato scritto da postext 1.4 o da una versione precedente, che impostava diciotto cose in modo diverso:

- **Interruzioni prima dei titoli** (regole 3): fino alla 1.4, un oggetto `headings` senza interruzione per l’H1 non ne aveva nessuna (vedi [Sostituzioni per livello](https://postext.dev/it/docs/configuration-text.md#sovrascritture-per-livello)).
- **La dimensione delle formule** (regole 4): fino alla 1.4, le formule risultavano 1,131 volte più grandi di quanto indica `fontSizeScale` (vedi [Dimensione delle formule](https://postext.dev/it/docs/configuration-text.md#matematica)).
- **Lo spazio sotto una risorsa in linea** (regole 5): fino alla 1.4, il testo dopo una figura o una tabella `placement.position: 'here'` riprendeva alla linea successiva della griglia, senza la distanza degli elementi flottanti sotto di essa (vedi `layout.inlineResourceGap` in [Impaginazione](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione)).
- **Lo spazio intorno a una risorsa in linea dentro un riquadro** (regole 6): fino alla 1.4, una risorsa del genere stava a contatto con il testo del riquadro che la conteneva (vedi `layout.inlineResourceGapInBoxes` in [Impaginazione](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione)).
- **Stili in linea nei titoli** (regole 6): fino alla 1.4, un titolo stampava le parole dei suoi segni `*italic*`, `**bold**` e altri nel proprio stile semplice (vedi `headings.inlineMarks` in [Titoli](https://postext.dev/it/docs/configuration-text.md#titoli)).
- **La dimensione di un capolettera** (regole 6): fino alla 1.4, il `dropCap` di un testo di design senza `fontSize` era alto quanto tutti i riquadri di riga che occupa, con la sommità sopra la prima riga (vedi `dropCap` in [Elementi di testo](https://postext.dev/it/docs/configuration-page-layout.md#elementi-di-testo)).
- **Lo spazio sotto una riga che termina con i due punti** (regole 6): fino alla 1.4, `keepColonWithList` considerava sufficiente per l’elenco una riga di spazio sotto la riga che termina con i due punti, e una prima voce di due righe che le regole di orfane e vedove tengono unita passava alla colonna successiva senza di essa (vedi `bodyText.colonListRoom`).
- **Le righe che lascia il taglio di un riquadro** (regole 6): fino alla 1.4, un riquadro diviso dentro un paragrafo o una voce di elenco poteva lasciarne una sola riga su un lato, purché ciascun lato del riquadro contenesse in tutto le sue `splitMinLines` righe (vedi `layout.boxChildSplitMinLines` in [Impaginazione](https://postext.dev/it/docs/configuration-page-layout.md#impaginazione)).
- **A capo dopo una lineetta** (regole 7): fino alla 1.4, Knuth–Plass non terminava mai una riga dopo una lineetta lunga o media composta senza spazi tra due parole (`say—that’s`), e il divisore riga per riga del testo formattato lo faceva solo tra due lettere (vedi `bodyText.breakAfterDashes` in [Testo corrente](https://postext.dev/it/docs/configuration-text.md#testo-corrente)).
- **Testo in bandiera** (regole 7): fino alla 1.4, il testo corrente in bandiera veniva composto riga per riga, riempiendo ogni riga prima della successiva, qualunque cosa dicesse `optimalLineBreaking` (vedi `bodyText.optimalRagged` in [Testo corrente](https://postext.dev/it/docs/configuration-text.md#testo-corrente)).
- **La divisione sotto un titolo** (regole 8): fino alla 1.4, il paragrafo sotto un titolo al piede di una colonna teneva lì tutte le righe che ci stavano, per quanto poche ne passassero alla colonna successiva (vedi `headings.keepWithNextSplit` in [Titoli](https://postext.dev/it/docs/configuration-text.md#titoli)).
- **Lo spazio sotto un contenitore `:::paragraphs`** (regole 8): fino alla 1.4, lo spazio dello stile veniva messo sotto l’ultimo paragrafo prima dell’aggancio alla griglia, lo spazio superiore del blocco successivo (il `marginTop` di un titolo) veniva aggiunto sotto di esso, e la spaziatura tra paragrafi del testo veniva tralasciata (vedi `bodyText.paragraphContainerSpacing` in [Testo corrente](https://postext.dev/it/docs/configuration-text.md#testo-corrente)).
- **A capo dopo il trattino di una parola composta** (regole 8): fino alla 1.4, Knuth–Plass non terminava mai una riga giustificata dopo un trattino tra due lettere (`well-known`) in un paragrafo senza formattazione in linea, mentre lo faceva in uno con formattazione (vedi `bodyText.breakAfterHyphens` in [Testo corrente](https://postext.dev/it/docs/configuration-text.md#testo-corrente)).
- **Poesie senza separatore** (regole 9): fino alla 1.22, una poesia `:::verse` le cui righe non contenevano `||` veniva composta come emistichi singoli, ogni riga centrata (vedi `bodyText.verse.layout` in [Versi](https://postext.dev/it/docs/configuration-text.md#versi)).
- **Un rientro di prima riga accanto a un rientro sporgente** (regole 9): fino alla 1.22, l’`hangingIndent` di uno stile di paragrafo sostituiva il suo `firstLineIndent`, e la prima riga iniziava a `indent` (vedi [Stili di paragrafo](https://postext.dev/it/docs/configuration-styles.md#stili-di-paragrafo)).
- **Una barra rovesciata a fine riga** (regole 9): fino alla 1.22, una barra rovesciata che terminava una riga di un paragrafo, di una citazione o di una voce di elenco, e `\\` prima di uno spazio, venivano stampati, e le righe si univano con uno spazio (vedi `bodyText.hardLineBreaks` in [Testo corrente](https://postext.dev/it/docs/configuration-text.md#testo-corrente)).
- **Recinzioni di codice** (regole 9): fino alla 1.22, una recinzione ```` ``` ```` o `~~~` e le righe al suo interno venivano lette come Markdown: righe unite in paragrafi, una riga `#` diventava un titolo, le recinzioni venivano stampate (vedi `codeStyle.blocks` in [Listati di codice](https://postext.dev/it/docs/configuration-styles.md#listati-di-codice)).
- **Righe di versi che vanno a capo** (regole 10): nella 1.23, una riga di una poesia composta riga per riga più larga della giustezza andava a capo con la sua spaziatura naturale tra le parole, per quanto poco sbordasse (vedi `bodyText.verse.tighten` in [Versi](https://postext.dev/it/docs/configuration-text.md#versi)).

`openBundle` e `readBundle` leggono il `config` di un manifest del genere, e la configurazione `localized` di ogni lingua, tramite `migrateConfig`, che scrive esplicitamente le interruzioni impaginate dalla 1.4 e moltiplica la scala delle formule per 1,131 (dividendo per lo stesso valore i suoi margini delle formule in evidenza in `em`). Un manifest marcato da `3` a `7`, scritto da una prerelease della 1.5, riceve solo i vincoli delle regole successive al suo marchio. Con `3` sono la dimensione delle formule, la distanza in linea, i cinque vincoli delle regole 6, i due delle regole 7 e i tre delle regole 8; con `4`, la distanza in linea e i vincoli delle regole 6, 7 e 8; con `5`, i vincoli delle regole 6, 7 e 8; con `6`, i vincoli delle regole 7 e 8; con `7`, solo i vincoli delle regole 8. La divisione sotto un titolo (`pinLegacyHeadingSplit`) viene scritta come `headings.keepWithNextSplit: 'fill'` sugli `headings` che i livelli lasciano in vigore, quando un capitolo letto ha un titolo e la configurazione non indica un valore proprio, mantiene attivo `headings.keepWithNext` e non disattiva `bodyText.avoidOrphans`. Gli a capo nelle parole composte (`pinLegacyHyphenBreaks`) vengono scritti come `bodyText.breakAfterHyphens: false` sul `bodyText` in vigore, quando un capitolo letto compone un trattino tra due lettere e la configurazione né lo imposta già né disattiva `optimalLineBreaking`. Lo spazio sotto i contenitori (`pinLegacyParagraphContainerSpacing`) viene scritto come `bodyText.paragraphContainerSpacing: 'add'` sul `bodyText` in vigore, quando la configurazione dichiara uno stile di paragrafo (in `paragraphStyles` o nelle sostituzioni del visualizzatore HTML), un capitolo letto apre un contenitore `:::paragraphs` su una riga a sé e la configurazione non lo imposta già. Gli a capo dopo le lineette (`pinLegacyDashBreaks`) vengono scritti come `bodyText.breakAfterDashes: false` sul `bodyText` in vigore, quando un capitolo letto compone una lineetta lunga o media senza spazi tra due parole (prima una lettera, una cifra o una punteggiatura di chiusura, dopo una lettera, una cifra o una parentesi o virgoletta di apertura; una virgoletta prima della lineetta conta quando prima della virgoletta c’è una lettera, una cifra, una punteggiatura di chiusura o uno spazio indivisibile, come in `"no"—and`, non in `said "—Hola`; un segno in linea che tocca la lineetta o la virgoletta, come il `**` di `**riddles.**—I`, conta su entrambi i lati) e la configurazione non lo imposta già. La divisione in righe in bandiera (`pinLegacyRaggedBreaking`) viene scritta come `bodyText.optimalRagged: false` sul `bodyText` in vigore, quando la configurazione compone in bandiera una parte del testo corrente (un `textAlign` diverso da `'justify'` sul testo corrente, su uno stile di paragrafo, sul corpo di un riquadro, sul corpo di una parte o sul corpo di uno stile di sezione (`headingStyles[].bodyStyle`), oppure nelle sostituzioni del visualizzatore HTML), non lo imposta già e non disattiva `optimalLineBreaking`. La distanza nei riquadri (`pinLegacyBoxResourceGap`) viene scritta come `layout.inlineResourceGapInBoxes: false` sul `layout` in vigore, quando una risorsa è inserita su una riga a sé dentro un `:::callout` dei capitoli letti e la configurazione non lo imposta già. Il taglio dei riquadri (`pinLegacyBoxChildCut`) viene scritto come `layout.boxChildSplitMinLines: 1` sul `layout` in vigore, quando un capitolo letto apre un `:::callout` su una riga a sé e la configurazione non lo imposta già. I segni nei titoli (`pinLegacyHeadingMarks`) vengono scritti come `headings.inlineMarks: false` sugli `headings` che i livelli lasciano in vigore, quando un titolo dei capitoli letti contiene un segno (`*`, `_`, `^`, `~`, `:smallcaps[` o un link nel suo testo) e la configurazione non indica un valore proprio. I capilettera (`pinLegacyDropCapSize`) ricevono la loro dimensione della 1.4 scritta esplicitamente come `dropCap.fontSize`, ovunque si trovino: nell’unità dell’interlinea dell’elemento quando questa è una lunghezza, altrimenti nell’unità del suo corpo. Lo spazio sotto la riga con i due punti (`pinLegacyColonListRoom`) viene scritto come `bodyText.colonListRoom: 'line'` sul `bodyText` in vigore, quando un elenco dei capitoli letti segue una riga che termina con i due punti (righe vuote in mezzo ammesse) e la configurazione né indica uno spazio né disattiva `keepColonWithList`. La distanza in linea (`pinLegacyInlineGap`) viene scritta come `layout.inlineResourceGap: 'above'` sul `layout` che i livelli lasciano in vigore, quando una riga dei capitoli letti inserisce una risorsa (`::resource{id="…"}` da solo sulla sua riga, come lo legge il parser: una menzione nel testo corrente o in un segmento di codice non conta) e la configurazione non indica una distanza propria. La dimensione viene vincolata sul `math` che i livelli lasciano in vigore (il `math` proprio di una lingua sostituisce quello condiviso), e solo quando i capitoli letti contengono un `$`: un pacchetto senza formule conserva il suo `config` come è scritto. Quando né il manifest né la lingua indicano un `math`, quello in vigore è il `baseConfig` di `readBundle` (quello del lettore), e anch’esso viene vincolato, perché la 1.4 componeva le formule del pacchetto a quella dimensione: un `fontSizeScale: 1.5` di base si legge come 1,5 × 1,1312. Le interruzioni prima dei titoli della base vengono prese così come sono. Un pacchetto vecchio conserva quindi ciò che queste regole impaginavano, e `bundle.config` mostra le interruzioni, la dimensione delle formule, le distanze, i segni nei titoli, le dimensioni dei capilettera, lo spazio sotto la riga con i due punti, il taglio dei riquadri, gli a capo dopo le lineette, gli a capo nelle parole composte, la divisione in bandiera, la divisione sotto un titolo e lo spazio sotto i contenitori con cui viene impaginato. Le correzioni di impaginazione della 1.5 non hanno vincoli e si applicano a esso come a qualsiasi libro, quindi una pagina che toccano può ancora spostarsi (vedi [Dimensione delle formule](https://postext.dev/it/docs/configuration-text.md#matematica) per l’elenco). Un `preset.json` scritto a mano per le regole attuali imposta `"configVersion": 14`; marcare con questo valore il manifest di un pacchetto vecchio è anche il modo, in una riga, per leggerlo con le regole attuali (un pacchetto senza versione perde allora anche il vincolo delle interruzioni prima dei titoli). Un manifest marcato `8`, scritto da postext dalla 1.5 alla 1.22, riceve solo i quattro vincoli delle regole 9, come del resto ogni manifest più vecchio: la disposizione dei versi (`pinLegacyVerseLayout`) viene scritta come `bodyText.verse.layout: 'bayt'` sul `bodyText` in vigore, quando un capitolo letto compone una poesia `:::verse` la cui recinzione non indica una disposizione e le cui righe non contengono un separatore di emistichi, e la configurazione non lo imposta già; i rientri abbinati (`pinLegacyPairedIndents`) eliminano il `firstLineIndent` di ogni stile di paragrafo (in `paragraphStyles` o nelle sostituzioni del visualizzatore HTML) che imposta anche un `hangingIndent` diverso da zero; gli a capo forzati (`pinLegacyHardBreaks`) vengono scritti come `bodyText.hardLineBreaks: false` sul `bodyText` in vigore, quando un capitolo letto termina con una barra rovesciata una riga di un paragrafo, di una citazione o di una voce di elenco e il blocco prosegue sotto, oppure compone `\\` prima di uno spazio e di altro testo (esclusi codice e formule in linea, formule in evidenza, titoli e poesie `:::verse`), e la configurazione non lo imposta già; e le recinzioni di codice (`pinLegacyCodeBlocks`) vengono scritte come `codeStyle.blocks: false`, quando un capitolo letto apre una recinzione ```` ``` ```` o `~~~` (tre o più caratteri, con al massimo tre spazi di rientro) e la configurazione non lo imposta già. Un manifest marcato `9`, scritto da postext 1.23, riceve solo il vincolo delle regole 10, come del resto ogni manifest più vecchio: le righe di versi che vanno a capo (`pinLegacyVerseTightening`) vengono scritte come `bodyText.verse.tighten: false` sul `bodyText` in vigore, quando un capitolo letto compone una poesia riga per riga (una recinzione `:::verse` che indica `layout=lines`, oppure che non indica una disposizione su righe senza separatore di emistichi mentre la configurazione non compone queste poesie come bayt) e la configurazione non lo imposta già. Un manifest marcato `10`, scritto da postext 1.24, riceve solo il vincolo delle regole 11, come del resto ogni manifest più vecchio: il bilanciamento di una griglia di caratteri (`pinLegacyGridBalancing`) viene scritto come `headings.balancing.enabled: true` sugli `headings` in vigore, quando la configurazione unita imposta `cjk.grid.enabled` nel testo orizzontale e non imposta essa stessa `enabled`, perché la 1.24 bilanciava una pagina del genere per impostazione predefinita. Le regole 11 impediscono anche gli a capo di un solo carattere nei titoli di opere, compongono i numeri cerchiati come caratteri cinesi e compongono il testo di design CJK con le regole del testo corrente (#637): un manifest marcato `10` o più vecchio riceve `cjk.titleMinChars: 1` (`pinLegacyTitleBreaks`) quando un capitolo letto compone un titolo di opera (《 〈 o `:book[`), `cjk.circledNumbers: 'western'` (`pinLegacyCircledNumbers`) quando uno compone un numero cerchiato (U+2460–U+24FF, U+2776–U+2793), e `cjk.composeDesignText: false` (`pinLegacyDesignText`) quando un capitolo letto o la configurazione stessa contiene testo CJK, ciascuno sul `cjk` in vigore e solo quando la configurazione non lo imposta già. Tagliano anche le tabelle in linea e compongono `:::columns` nel testo corrente (#634): un manifest marcato `10` o più vecchio riceve `tableStyle.splitInline: false` (`pinLegacyInlineTableSplit`) sul `tableStyle` in vigore quando un capitolo letto inserisce una risorsa, e `layout.flowColumns: false` (`pinLegacyFlowColumns`) sul `layout` in vigore quando un capitolo letto apre una recinzione `:::columns` su una riga a sé, ciascuno solo quando la configurazione non lo imposta già. Offrono inoltre a un elemento flottante la cima della colonna di un’apertura a tutta larghezza (#639): un manifest marcato `10` o più vecchio riceve `layout.floatsUnderOpener: false` (`pinLegacyOpenerHeadFloats`) sul `layout` in vigore quando la configurazione unita imposta un livello o uno stile di titolo con `span: 'page'` e un capitolo letto ha un titolo, solo quando la configurazione non lo imposta già. Le regole 12 permettono a una pagina su una griglia di caratteri di trattenere le sue righe (#717): un manifest marcato `11` o più vecchio la cui configurazione unita attiva `cjk.grid.enabled` riceve `bodyText.avoidWidows: true`, e nella regione `japan` `bodyText.avoidOrphans: true` (`pinLegacyGridBreakRules`), sul `bodyText` in vigore, ciascuno solo quando la configurazione non lo imposta già. Mantengono anche due caratteri di una nota su due righe da ciascun lato di un a capo (#720): un manifest del genere riceve `cjk.warichu.minChars: 1` (`pinLegacyNoteBreaks`) sul `cjk` in vigore quando un capitolo letto compone una nota `:warichu[` o la configurazione unita imposta `citations.notes: 'warichu'`, solo quando la configurazione non lo imposta già. E nella regione Giappone una parola occidentale prende lo spazio Han–latino accanto alle sue parentesi e ai suoi segni (#721): un manifest marcato `11` o più vecchio la cui configurazione unita si risolve nella regione Giappone riceve `cjk.latinSpacingEdge: 'letters'` (`pinLegacyLatinSpacingEdge`) sul `cjk` in vigore quando un capitolo letto contiene kana o kanji, solo quando la configurazione non lo imposta già. E una risorsa composta nel testo prende `placement.captionSide` come fa un elemento flottante di colonna (#729): un manifest marcato `11` o più vecchio la cui configurazione unita imposta `layout.layoutType: 'oneAndHalf'` riceve `layout.inlineCaptionSide: false` (`pinLegacyInlineCaptionSide`) quando un capitolo letto inserisce una risorsa, solo quando la configurazione non lo imposta già.

Le regole 12 cambiano anche il modo in cui si bilancia una colonna corta, e un manifest marcato `11` o più vecchio mantiene ciò con cui è stato impaginato. L’ordine delle leve (#707): riceve `headings.balancing.maxLinesBeforeLoose` impostato al suo `maxLinesPerHeading`, o a `4` quando non ne imposta nessuno (`pinLegacyLooseOrder`), sugli `headings` in vigore, quando un capitolo letto ha un titolo e la configurazione unita bilancia le sue colonne, consente a un titolo di occupare più di due righe e non imposta già la chiave. La riga sotto una figura su più colonne (#708): riceve `headings.balancing.stretchAfterWideFloats: 'always'` (`pinLegacyWideFloatStretch`) sugli `headings` in vigore, quando un capitolo letto inserisce o cita una risorsa o fa flottare un riquadro, e la configurazione unita bilancia le sue colonne, non disattiva `stretchAfterFloats` e non imposta già la chiave. Le interruzioni dentro i gruppi di un indice analitico (#712): riceve `index.groups.splitMinEntries: 1` (`pinLegacyIndexGroupSplit`) sull’`index` in vigore, quando un capitolo letto compone una recinzione `:::index` su una riga a sé e la configurazione non imposta già la chiave. L’unità del taglio a livello (#711): riceve `headings.balancing.trailingStep: 'grid'` (`pinLegacyTrailingStep`) sugli `headings` in vigore, quando un capitolo letto può comporre testo fuori dall’interlinea del testo corrente (una recinzione `:::`, una recinzione di codice, una citazione, un elenco, una formula in evidenza o una citazione bibliografica), e la configurazione unita bilancia le sue colonne, mantiene attivo `trailing` o `beforeSpan` e non imposta già la chiave.

Le regole 12 vanno a capo agli spazi nell’etichetta di un rimando quando l’etichetta è fatta di parole (#702). Un manifest marcato `11` o più vecchio riceve `crossRefs.breakLabels: false` (`pinLegacyRefLabelBreaks`) sul `crossRefs` in vigore, quando un capitolo letto contiene un rimando (`:ref{…}`, `\ref{…}`, `\eqref{…}` o un `@id` di pandoc-crossref) e la configurazione non indica l’impostazione: ogni etichetta resta l’unica parola con cui era stata impaginata.

Le regole 12 compongono una parola senza lettere (`46.`, `1367)`) nella scrittura del testo che la circonda (#725). Un manifest marcato `11` o più vecchio riceve `bodyText.scriptContext: false` (`pinLegacyScriptContext`) sul `bodyText` in vigore, qualunque cosa contengano i suoi capitoli, quando la configurazione non indica l’impostazione: una parola del genere viene composta da sola, come era stata impaginata. La regola agisce in un font che disegna la punteggiatura in base alla scrittura, e quali font carica un libro non si sa quando se ne legge il manifest.

Le regole 13 trasformano in un’impostazione la lingua in cui viene composto il testo (#764), e le regole 14 rendono predefinita la lingua propria del documento (#770). Un manifest marcato `13` o più vecchio riceve `bodyText.shapingLanguage: 'host'` (`pinLegacyShapingLanguage`) sul `bodyText` in vigore, qualunque cosa contengano i suoi capitoli, quando la configurazione non indica l’impostazione; uno che indica un valore lo mantiene. Il suo testo viene misurato nella lingua della pagina che ospita l’impaginazione, e in nessuna lingua in un worker, come era stato impaginato. Un manifest marcato `13` è stato scritto da postext 1.29: la 1.29.1 e la 1.29.2 impaginavano un libro che non indica un valore nella lingua dell’applicazione ospite, come fa il vincolo; la 1.29.0, il cui valore predefinito era `'document'`, lo impaginava in quella del documento con un canvas e un PDF che non seguivano la misurazione, e anch’esso si legge con `'host'`. Un manifest marcato `14` che non indica un valore viene impaginato nella lingua del documento.

Le regole 13 disegnano anche la casella di una voce di attività (`- [ ]`, `- [x]`): un quadrato disegnato dal motore, con un segno di spunta quando la voce è completata, che occupa uno spazio proprio prima del testo della voce (#765). Un manifest marcato `12` o più vecchio riceve `unorderedLists.taskBox: 'character'` (`pinLegacyTaskBox`) sull’`unorderedLists` in vigore, quando un capitolo contiene una voce di attività e la configurazione non indica l’impostazione. La sua casella resta il carattere `☐` o `☑` composto nel carattere dell’elenco, e il testo della voce inizia dopo la larghezza di quel carattere, come era stato impaginato. Quella larghezza dipendeva dal font che conteneva il carattere, nella maggior parte dei libri un font di sistema, e non si conosce quando si legge il manifest.

```ts
import { CONFIG_VERSION, migrateConfig } from 'postext/bundle';

migrateConfig({ headings: { fontFamily: 'Georgia' } }, undefined, { content: 'A book with no maths.' });
// => { headings: { fontFamily: 'Georgia', levels: [{ level: 1, breakBefore: { enabled: false } }] } }
migrateConfig({ math: { fontSizeScale: 1.2 } }, 3);
// => { math: { fontSizeScale: 1.35746…, marginTop: { value: 0.7072, unit: 'em' }, marginBottom: { value: 0.7072, unit: 'em' } },
//      layout: { inlineResourceGap: 'above', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 },
//      headings: { inlineMarks: false, keepWithNextSplit: 'fill' }, bodyText: { colonListRoom: 'line', breakAfterDashes: false, breakAfterHyphens: false, verse: { layout: 'bayt', tighten: false }, hardLineBreaks: false }, codeStyle: { blocks: false } }
migrateConfig({ layout: { layoutType: 'single' } }, 4, { content: 'Text.\n\n::resource{id="fig"}' });
// => { layout: { layoutType: 'single', inlineResourceGap: 'above' } }
migrateConfig({ layout: { layoutType: 'single' } }, 5, { content: ':::callout\nText.\n\n::resource{id="fig"}\n:::' });
// => { layout: { layoutType: 'single', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 } }
migrateConfig({ bodyText: { textAlign: 'left' } }, 6, { content: 'I say—that is all.' });
// => { bodyText: { textAlign: 'left', breakAfterDashes: false, optimalRagged: false } }
migrateConfig({ paragraphStyles: [{ id: 'verse' }] }, 7, { content: ':::paragraphs{style="verse"}\nA line.\n:::' });
// => { paragraphStyles: [{ id: 'verse' }], bodyText: { paragraphContainerSpacing: 'add' } }
migrateConfig({ bodyText: { fontFamily: 'Georgia' } }, 7, { content: 'A well-known tale.' });
// => { bodyText: { fontFamily: 'Georgia', breakAfterHyphens: false } }
migrateConfig(config, CONFIG_VERSION); // today's rules: `config` itself
```

`content` è il markdown che la configurazione impagina (una stringa o un elenco di capitoli). Senza di esso la dimensione delle formule viene vincolata ogni volta che la matematica è attiva, lo spazio sotto i contenitori ogni volta che la configurazione dichiara uno stile di paragrafo, e le due distanze, i segni nei titoli, lo spazio sotto la riga con i due punti, il taglio dei riquadri, gli a capo dopo le lineette, la divisione sotto un titolo e gli a capo nelle parole composte sempre, perché il motore non può sapere se il libro contiene una formula, un contenitore `:::paragraphs`, una figura in linea, un titolo con segni, un elenco introdotto dai due punti, un riquadro, una lineetta senza spazi, un titolo o una parola composta. La divisione in bandiera viene vincolata dalla sola configurazione, con o senza contenuto. Migra una configurazione salvata una sola volta e salvala di nuovo con `CONFIG_VERSION`: il vincolo delle formule moltiplica la scala, quindi una configurazione migrata due volte crescerebbe due volte. Anche la disposizione dei versi e le recinzioni di codice vengono vincolate senza contenuto, perché il libro potrebbe contenere una poesia `:::verse` senza separatore o una recinzione, e i rientri abbinati vengono vincolati dalla sola configurazione. Lo stesso vale per le righe di versi che vanno a capo della 1.23, perché il libro potrebbe comporre una poesia riga per riga.

### Impaginare e disegnare un pacchetto

Quattro funzioni collegano un pacchetto aperto al motore e ai backend:

- **`loadBundleFonts(bundle)`** registra i font del pacchetto in `document.fonts`, e i loro byte nel registro dei font del motore per le immagini SVG (`registerFontBytes`). Attendine il completamento prima di impaginare, perché l’impaginazione misura il testo con i font di cui dispone il browser. Le famiglie che il pacchetto nomina ma non contiene (Google Fonts) devi comunque caricarle tu, come per qualsiasi altro documento.
- **`registerBundleImages(bundle)`** decodifica le immagini per il backend canvas (`renderPage`, `renderToCanvas`), poster dei video compresi. **`bundleImageUrl(bundle)`** è la funzione di risoluzione `resourceImageUrl` per `renderToHtml`, e **`bundleVideoUrl(bundle)`** la sua funzione di risoluzione `resourceVideoUrl` per i file video contenuti in un pacchetto. Entrambe ricolorano le figure SVG quando `diagramStyle.singleInk` è attivo, una volta sola: ricolorano il markup e contrassegnano le immagini perché nessun backend le tinga di nuovo (vedi [Un solo inchiostro su canvas e in HTML](https://postext.dev/it/docs/configuration-resources.md#inchiostro-unico-sul-canvas-e-in-html)). Entrambe incorporano inoltre in ogni SVG i font che il suo testo nomina, prima dai font propri del pacchetto, poi dai font registrati nel motore (vedi [Font nel testo SVG](https://postext.dev/it/docs/configuration-resources.md#font-nel-testo-svg)); `registerBundleImages(bundle, { onWarning })` e `bundleImageUrl(bundle, { onWarning })` segnalano una famiglia senza font.
- **`buildBundle(bundle)`** impagina i capitoli in ordine e restituisce un `VDTDocument` per capitolo. Ogni capitolo prosegue quello precedente: contatori dei titoli e delle risorse, la parte aperta, la parità delle pagine e la numerazione delle pagine. Un capitolo che stampa l’indice (`:::toc`) o l’indice analitico (`:::index`) riceve la struttura dell’intero libro. Accetta le stesse opzioni di `buildDocument`, più `config` per sostituire la configurazione del pacchetto, `cache` per condividere una cache delle misure e `metadata` (vedi sotto).
- **`bundleResourceBytes(bundle)`** e **`bundleFontProvider(bundle, { decodeWoff2, fallback })`** sono le opzioni `resourceBytes` e `fontProvider` del `renderToPdf` di `postext-pdf`. Il fornitore di font sceglie dal pacchetto il peso più vicino dello stile richiesto. Per un font `.woff2` ha bisogno di `decompressWoff2`, e per una famiglia che il pacchetto non contiene chiama `fallback` con gli argomenti del renderer, `request` compreso, e passa avanti qualunque cosa restituisca. Un fallback che scarica solo il file `latin` di una famiglia stampa come riquadri vuoti una famiglia cinese che il pacchetto non incorpora; uno che risponde con porzioni, come `sliceFontProvider` in [Font cinesi, giapponesi e coreani](https://postext.dev/it/docs/configuration-programmatic-usage.md#font-cinesi-giapponesi-e-coreani), la stampa per intero.

```ts
import { openBundle, loadBundleFonts, registerBundleImages, buildBundle, renderPage,
  bundleResourceBytes, bundleFontProvider } from 'postext';
import { renderToPdf, decompressWoff2 } from 'postext-pdf';

const bundle = await openBundle(bytes);
await loadBundleFonts(bundle);
await registerBundleImages(bundle);

const docs = buildBundle(bundle);                        // one VDTDocument per chapter
const firstPage = renderPage(docs[0].pages[0], docs[0]); // a <canvas>

const pdf = await renderToPdf(docs, {                    // the whole book
  fontProvider: bundleFontProvider(bundle, { decodeWoff2: decompressWoff2, fallback: fontsource }),
  resourceBytes: bundleResourceBytes(bundle),
});
```

Per impaginare da te un singolo capitolo, passa `bundle.chapters[i].markdown`, `bundle.resources` e `bundle.config` a `buildDocument`, come faresti per qualsiasi documento.

**I metadati del libro.** Come nella Sandbox, il front matter del primo capitolo è quello del libro: `buildBundle` passa il suo `title`, il suo `author` e il resto a ogni capitolo, così le testatine `{title}` e `{author}` valgono su ogni pagina e il `doc.metadata` di ogni capitolo li contiene. Un blocco di front matter in cima a un capitolo successivo viene ignorato: viene individuato dalle sue righe `---` e svuotato senza essere analizzato, quindi un YAML che il parser rifiuterebbe non fa danni. `options.metadata` fornisce i valori che il front matter non imposta (prevale il front matter). Anche il numero di pagine del libro arriva a ogni capitolo: `{bookTotalPages}` lo stampa, mentre `{totalPages}` conta le pagine del capitolo (vedi [Numero di pagine del libro](https://postext.dev/it/docs/configuration-page-layout.md#numero-di-pagine-del-libro)).

```ts
const docs = buildBundle(bundle, { metadata: { author: 'A. Author' } });
docs[3].metadata.title;   // the first chapter's `title:`
```

**Dove inizia il libro.** Un pacchetto può contenere una parte di una pubblicazione più lunga: le pagine da 58 a 61 di un fascicolo, il capitolo 4 di un manuale. Il suo `start` dice che cosa viene prima, nei termini della `continuation` di `buildDocument`: le pagine prima della prima (`pageIndexOffset`, che decide su quale lato cade la pagina 1, e con esso i margini speculari e le testatine pari e dispari), la numerazione delle pagine in vigore (`pageNumbering`), i contatori dei titoli (`headings`), la parte aperta (`part`) e i contatori di risorse, enunciati, note a piè di pagina e righe. `createBundle` lo scrive come `start` di `preset.json`, `openBundle` lo restituisce come `bundle.start`, e `buildBundle` impagina con esso il primo capitolo, come farebbe `buildDocument({ markdown, continuation: start }, config)`, e da lì concatena i capitoli successivi; anche `{bookTotalPages}` conta le pagine che precedono il libro. Un manifest senza `start` si legge come prima, e un lettore che non conosce il campo lo ignora. `bookPageCount` non ne fa parte: le pagine le conta il lettore. In un pacchetto con più lingue, `localized[…].start` dà a un’edizione un inizio proprio.

```ts
const { bytes } = await createBundle({
  name: 'Field notes, chapter 4',
  markdown,
  config,
  // Page 58, an even page, in chapter 4.
  start: { pageIndexOffset: 57, pageNumbering: { startAt: 58 }, headings: { h1: 3 } },
});
const bundle = await openBundle(bytes);
bundle.start;                 // { pageIndexOffset: 57, pageNumbering: { startAt: 58 }, headings: { h1: 3, h2: 0, … } }
const [doc] = buildBundle(bundle);
doc.pages[0].pageLabel;       // '58'
```

### Esempio dal vivo: aprire un pacchetto

Il pen carica dal repository un libro di esempio di due capitoli (`lantern.postext`, con un carattere proprio, una figura SVG e una tabella). Registra i font e le immagini del pacchetto, impagina il libro con `buildBundle` e disegna ogni pagina. *Make the PDF* disegna gli stessi documenti con `postext-pdf`, incorporando i font del pacchetto. Scegli un tuo file `.postext`, per esempio esportato dalla Sandbox, per vederlo allo stesso modo.

> **Esempio eseguibile: Postext · aprire un pacchetto .postext** — Apri un file .postext con postext, impagina il libro e disegnalo su canvas e in PDF. ([codice](https://github.com/drnachio/postext/tree/main/docs/examples/open-bundle))

### Creare un pacchetto

`createBundle` scrive un file `.postext` a partire da un documento: i suoi capitoli, la configurazione, le risorse e i contenuti a cui fanno riferimento.

```ts
import { createBundle } from 'postext';

const { bytes, manifest, warnings } = await createBundle({
  name: 'The Lantern',
  locale: 'en',
  chapters: [
    { markdown: '# Dusk\n\nIt is drawn in :ref{id="lantern"}.' },
    { title: 'Night', markdown: '# Night\n\n…' },
  ],
  config,
  resources: [{
    id: 'lantern', typeId: 'figure', kind: 'svg', caption: 'The lantern.',
    svg: { fileId: 'lantern.svg', width: 240, height: 150 },
    createdAt: 0, updatedAt: 0,
  }],
  files: { 'lantern.svg': svgMarkup, 'garamond-regular': fontBytes },
});
```

| Input | Significato |
| --- | --- |
| `name`, `id`, `description`, `locale` | I metadati del manifest. `id` vale per impostazione predefinita uno slug di `name`. |
| `chapters` o `markdown` | Il libro, un `{ title?, markdown }` per capitolo, oppure un singolo documento. |
| `config` | Il `PostextConfig`. I valori uguali a quelli predefiniti vengono lasciati fuori dal manifest. |
| `resources` | Le risorse. Un’immagine indica il suo contenuto con `bitmap.fileId` / `svg.fileId` (e `svg.pdfFileId` per un master di stampa). |
| `files` | I contenuti per `fileId` (un oggetto o una `Map`): le immagini a cui fanno riferimento le risorse e i file dei font a cui fanno riferimento le varianti di `config.customFonts`. I valori possono essere un `Uint8Array`, un `ArrayBuffer`, un `Blob` o una stringa (markup SVG). |
| `thumbnail` | `{ data, mime }`: un’immagine di copertina (PNG, JPEG, WebP, GIF o SVG). |
| `canvasScope` | `'book'` chiede ai visualizzatori di impaginare l’intero libro come un unico canvas. |
| `start` | Il punto in cui inizia il libro, per un pacchetto che contiene una parte di un libro più lungo: la `continuation` con cui verrebbe costruito un singolo documento. Viene scritto come `start` del manifest. |
| `mtime` | La data di modifica scritta su ogni file dell’archivio (una `Date`, un timestamp o una stringa di data). Se la ometti, è l’ora della chiamata, quindi due chiamate con lo stesso input producono byte diversi. Passa una data fissa e lo stesso input produce gli stessi byte, che puoi sottoporre a hash o confrontare. Uno zip conserva data e ora senza fuso orario, a passi di due secondi, dal 1980 al 2099, e la data viene scritta nell’ora locale della macchina. Per avere byte identici su ogni macchina, costruisci la data a partire da campi locali, come `new Date(1980, 0, 1)`: un timestamp o una stringa che termina con `Z` indica un istante, che cade in un’ora locale diversa in ogni fuso orario (`'1980-01-01T00:00:00Z'` a ovest di UTC è ancora 1979). Una data fuori da quegli anni, in ora locale, genera un errore. |
| `localized` | Altre lingue dello stesso libro, per tag di lingua: `{ es: { chapters?, config?, resources? } }`. Gli input qui sopra diventano allora il contenuto di `locale`, che è obbligatorio. Vedi [Pacchetti bilingui](https://postext.dev/it/docs/configuration-programmatic-usage.md#pacchetti-bilingui). |

Restituisce i `bytes` dell’archivio, il `manifest` scritto come `preset.json`, ogni file come `files` (percorso → byte) e un elenco di `warnings`. I file prendono il nome dall’id della loro risorsa (`resources/lantern.svg`) o dal nome del file del font (`fonts/…`), e i capitoli dal loro ordine e dal titolo (`chapters/01-dusk.md`). I font sono dichiarati nei `fonts` del manifest, mai dentro `config.customFonts`. Alcune cose vengono lasciate fuori, ciascuna con un avviso:
- una risorsa o un font il cui contenuto non è in `files`
- un font `.woff` (il backend PDF non può incorporarlo)
- una famiglia marcata `redistributable: false`

Nel browser, passa `bytes` a un link di download: `URL.createObjectURL(new Blob([bytes], { type: 'application/zip' }))`. In Node, scrivili con `fs.writeFile`. `createBundle` e `openBundle` non hanno bisogno di un DOM. Le dist usano percorsi di modulo senza estensione, quindi in Node puro, senza bundler, serve un hook di risoluzione. Il file `docs/examples/open-bundle/build-sample.mjs` del repository ne mostra uno in poche righe.

### Pacchetti bilingui

Un file `.postext` può contenere un libro in più lingue, e `openBundle(bytes, { locale })` lo legge in una qualsiasi di esse. `createBundle` ne scrive uno a partire da `localized`: una voce per ogni lingua aggiuntiva, ciascuna con ciò che differisce dal contenuto principale (l’input `locale`):

```ts
const { bytes, manifest } = await createBundle({
  name: 'The Lantern',
  locale: 'en',
  chapters: [{ markdown: '# Dusk\n\n…' }, { markdown: '# Night\n\n…' }],
  config,
  resources: [lanternFigure, hoursTable],
  files: { 'lantern.svg': svgEn, 'lantern-es.svg': svgEs },
  localized: {
    es: {
      chapters: [{ markdown: '# Anochecer\n\n…' }, { markdown: '# Noche\n\n…' }],
      config: { headings: { levels: [{ level: 1, numberingTemplate: 'Capítulo {1}' }] } },
      resources: [
        { id: 'lantern', caption: 'El farol.', svg: { fileId: 'lantern-es.svg', width: 240, height: 150 } },
        { id: 'hours', caption: 'Horas de luz.' },
      ],
    },
  },
});

const es = await openBundle(bytes, { locale: 'es' });   // Spanish chapters, config and captions
```

- **`chapters`**: il libro in quella lingua. I file dei capitoli vanno in una cartella per lingua (`chapters/en/01-dusk.md`, `chapters/es/01-anochecer.md`) e i `chapters` del manifest diventano una mappa lingua → capitoli. Una lingua senza `chapters` legge quelli principali; quando nessuna lingua ha capitoli propri, restano un unico elenco.
- **`config`**: la configurazione per quella lingua. Ogni chiave di primo livello sostituisce per intero la chiave condivisa quando il pacchetto viene letto in quella lingua, quindi `headings` qui sopra sostituisce l’intero oggetto `headings`. Le chiavi omesse, o uguali a quelle condivise, sono condivise e non vengono scritte, quindi passare la configurazione completa della lingua funziona come passare le poche chiavi che cambiano. Una chiave impostata ai valori predefiniti mentre quella condivisa non lo è (`layout: {}`) viene scritta così com’è, quindi azzera il valore condiviso. I font sono condivisi: le famiglie dei `customFonts` di una lingua si aggiungono ai `fonts` del pacchetto.
- **`resources`**: i testi delle risorse condivise, abbinati per `id`: `caption`, `note`, `altText` e il `table` di una tabella. Un’immagine che contiene parole può avere una grafica propria: `bitmap.fileId` o `svg.fileId` (e `svg.pdfFileId`) indicano un altro contenuto in `files`, scritto come `resources/es/lantern.svg`. Gli altri campi, come il tipo o la collocazione, sono condivisi. Un id che non è tra le `resources` viene lasciato fuori con un avviso, e un’immagine mancante per una lingua lascia quella lingua sull’immagine condivisa, anche in questo caso con un avviso.

Il manifest elenca ogni lingua in `locales` (`['en', 'es']`), mantiene quella principale come `locale` e conserva le altre sotto `localized`. `openBundle` senza lingua legge la lingua principale.

**Quale lingua riceve chi legge.** `openBundle(bytes, { locale })` serve la lingua esatta, altrimenti la sua lingua di base (`es-MX` legge `es`), altrimenti quella principale, e `bundle.locale` indica quale ha servito. Capitoli e testi provengono sempre dalla stessa lingua. La lingua principale mantiene i testi condivisi anche quando `localized` contiene una sua variante regionale: un pacchetto `pt-PT` con una voce `pt-BR` legge le didascalie brasiliane solo per `pt-BR`, e quelle condivise per `pt-PT` e `pt`.

### Esempio dal vivo: creare un pacchetto

Il pen costruisce un libro di due capitoli con una figura SVG ed elenca i file che `createBundle` ha scritto insieme al manifest. Offre l’archivio da scaricare, poi lo riapre con `openBundle` e ne disegna la prima pagina: il giro completo in poche righe. Importa il file scaricato nella Sandbox per continuare a lavorarci lì.

> **Esempio eseguibile: Postext · creare un pacchetto .postext** — Scrivi un file .postext con createBundle di postext, scaricalo e aprilo di nuovo. ([codice](https://github.com/drnachio/postext/tree/main/docs/examples/create-bundle))

### Lavorare con i pacchetti

Dato che la Sandbox, la skill per agenti e il pacchetto `postext` leggono e scrivono tutti lo stesso file, un file `.postext` è un modo comodo per passare un libro da uno strumento all’altro:

- **Parti da un pacchetto.** Porta una pubblicazione esistente con la [skill per agenti](https://postext.dev/it/docs/skill.md), oppure progetta un libro nella [Sandbox](https://postext.dev/it/sandbox.md) ed esportalo (**Scarica (.postext)** nel menu ⋯ della sua riga nel pannello Libri). Carica il file dal tuo programma con `openBundle` per disegnarlo su canvas, in HTML o in PDF. Tieni il file come sorgente del libro: modifica i capitoli, la configurazione o le risorse nel codice e riscrivilo con `createBundle`, oppure ricaricalo semplicemente ogni volta che cambia.
- **Correggi e rifinisci nella Sandbox.** Quando qualcosa nell’output del tuo programma va sistemato (una figura che finisce sulla pagina sbagliata, uno stile di titolo, il bilanciamento delle colonne), esporta con `createBundle` ciò che il tuo programma impagina. Importa quel file nella Sandbox (*Libri → Nuovo → Apri un file .postext…*), correggi il testo, il progetto grafico o le figure con l’anteprima dal vivo, il pannello Verifiche e la vista PDF, poi esportalo di nuovo. Il tuo programma carica allora il file corretto con `openBundle`. Oppure riporta le modifiche nel tuo codice: il `config` del manifest contiene solo i valori diversi da quelli predefiniti, quindi si legge come un breve diff.

### API di basso livello

`postext/bundle` esporta anche i mattoni su cui si basano `openBundle` e `createBundle`, per le applicazioni ospiti che conservano o servono i pacchetti a modo loro (una cartella decompressa servita via HTTP, record in un database):

- **`openBundleZip(bytes)` / `zipBundle(files, { mtime })`**: il livello dell’archivio. L’apertura tollera una cartella di primo livello e ignora le voci `__MACOSX` e i file nascosti. I percorsi che escono dal pacchetto vengono rifiutati. `mtime` data i file come fa l’input di `createBundle`.
- **`readBundle(manifest, readFile, options)`** trasforma un manifest più una callback `readFile(path)` in capitoli, configurazione, risorse, immagini e font. `options` imposta la lingua, come vengono chiamati gli id dei file (`ids`), la configurazione di base (`baseConfig`, sotto quella del manifest; per impostazione predefinita la tavolozza e i tipi di risorsa di `bundleBaseConfig` nella lingua del pacchetto, che restituisce `resolveBundleConfigLocale(manifest, locale)`, e un’applicazione ospite che passa il proprio `baseConfig` dovrebbe localizzarlo in quella lingua; sotto un manifest precedente a `configVersion: 4` il suo `math` viene vincolato insieme a quello del pacchetto, sotto uno precedente alla 5 la distanza in linea del suo `layout`, sotto uno precedente alla 6 la distanza nei riquadri del suo `layout`, lo spazio sotto la riga con i due punti del suo `bodyText`, i segni in linea dei suoi `headings` e le dimensioni dei suoi capilettera, sotto uno precedente alla 7 gli a capo dopo le lineette e la divisione in bandiera del suo `bodyText`, e sotto uno precedente alla 8 la divisione sotto un titolo dei suoi `headings` e gli a capo nelle parole composte e lo spazio sotto i contenitori `:::paragraphs` del suo `bodyText`, vedi [Pacchetti scritti da postext 1.4 o precedenti](https://postext.dev/it/docs/configuration-programmatic-usage.md#pacchetti-scritti-da-postext-14-o-precedenti)) e come vengono misurate le dimensioni intrinseche. `readResolution` legge dal file la risoluzione di ogni bitmap e la mette in `bitmap.fileResolution`; vale true per impostazione predefinita quando il pacchetto imposta `layout.bitmapResolution: 'file'`.
- **`planBundle(meta, content)` / `resolveBundleFiles(plan, sources)`**: il lato della scrittura, diviso in un piano puro (nomi dei file e manifest) e nella risoluzione dei byte tramite le callback `readBlob` / `readFont`.
- **`isBundleManifest(value)`**, i selettori di lingua (`pickChapterSpecs`, `pickLocaleOverrides`, `pickBundleView`, `resolveBundleLocale`, `resolveBundleConfigLocale`), `svgSize` / `bitmapSize` / `bitmapInfo` (i pixel di una bitmap e la risoluzione che il suo file dichiara) e i tipi del formato (`BundleManifest`, `BundleResourceSpec`, `BundleFontFamilySpec`, …).
- **`CONFIG_VERSION`, `migrateConfig(config, configVersion, { content })`, `pinLegacyHeadingBreaks(config)`, `pinLegacyMathSize(config)`, `pinLegacyInlineGap(config)`, `pinLegacyBoxResourceGap(config)`, `pinLegacyHeadingMarks(config)`, `pinLegacyDropCapSize(config)`, `pinLegacyColonListRoom(config)`, `pinLegacyBoxChildCut(config)`, `pinLegacyDashBreaks(config)`, `pinLegacyRaggedBreaking(config)`, `pinLegacyHeadingSplit(config)`, `pinLegacyParagraphContainerSpacing(config)`, `pinLegacyHyphenBreaks(config)` e `LEGACY_MATH_SIZE`** (0,5 ÷ 0,442): una configurazione salvata nei termini attuali (vedi [Pacchetti scritti da postext 1.4 o precedenti](https://postext.dev/it/docs/configuration-programmatic-usage.md#pacchetti-scritti-da-postext-14-o-precedenti)). `readBundle` la applica; anche un’applicazione ospite che conserva le configurazioni a modo suo può farlo, una volta per ogni copia salvata.

La Sandbox è costruita su queste funzioni. Vi aggiunge i propri id di archiviazione e i record di pagine `layouts.json`.
