# Uso programático: paquetes .postext

> Abrir, componer, renderizar y crear archivos .postext desde el código, los paquetes bilingües, los archivos de versiones anteriores y la API de bajo nivel

- Versión HTML: https://postext.dev/es/docs/configuration-bundles
- Última actualización: 2026-10-16
- Tiempo de lectura: 30 min
- Otros idiomas: [en](https://postext.dev/en/docs/configuration-bundles.md), [ca](https://postext.dev/ca/docs/configuration-bundles.md), [de](https://postext.dev/de/docs/configuration-bundles.md), [fr](https://postext.dev/fr/docs/configuration-bundles.md), [it](https://postext.dev/it/docs/configuration-bundles.md), [pt](https://postext.dev/pt/docs/configuration-bundles.md), [zh](https://postext.dev/zh/docs/configuration-bundles.md), [ja](https://postext.dev/ja/docs/configuration-bundles.md), [ar](https://postext.dev/ar/docs/configuration-bundles.md)

## En pocas palabras

Esta página es para quienes escriben código. Un archivo .postext guarda un libro entero en un solo archivo: su texto, sus ajustes, sus fuentes y sus imágenes. La página enseña a abrir uno de esos archivos, componer sus páginas y dibujarlas. Enseña a crear un archivo nuevo a partir de tu propio texto y a guardar dos idiomas de un libro en el mismo archivo. También explica cómo se leen los archivos creados con versiones anteriores de Postext.

## Paquetes (archivos `.postext`)

Un **archivo `.postext`** es un libro entero en un solo archivo: un zip con un manifiesto `preset.json`, un archivo markdown por capítulo, los datos de los recursos (mapas de bits, SVG, másters PDF de impresión) y los archivos de las fuentes que nombra la configuración. El [Sandbox](https://postext.dev/es/docs/sandbox-books-saving.md#exportar-e-importar) lo exporta e importa y el [skill para agentes](https://postext.dev/es/docs/skill.md) lo entrega. El paquete `postext` también sabe crearlo y abrirlo, así que un libro puede pasar de esas herramientas a tu propio programa, y al revés, sin perder nada.

```
mi-libro.postext
├── preset.json            manifiesto: nombre, idioma, capítulos, configuración, recursos, fuentes
├── chapters/01-anochecer.md
├── chapters/02-noche.md
├── resources/farol.svg
└── fonts/ebgaramond-400-normal.woff2
```

El manifiesto se describe campo a campo en el apéndice [Formato de paquete de preset](https://postext.dev/es/docs/sandbox-embedding.md#formato-de-paquete-de-preset) del Sandbox. Un archivo puede llevar además `layouts.json`, el recuento de páginas del Sandbox, para que el libro se abra ya paginado allí, o uno por edición de un libro en varios idiomas (`layouts.zh-Hant.json`, que se lee primero). `openBundle` los ignora.

La API se exporta desde `postext` y desde el subpath `postext/bundle`, que añade las utilidades de bajo nivel. Importa desde `postext` cuando además vayas a renderizar. Así los adaptadores de paquetes y los renderizadores comparten una sola instancia del módulo, lo que importa en un CDN como esm.sh, donde cada punto de entrada es un build distinto.

### Abrir un paquete

`openBundle` recibe los bytes del archivo (un `Uint8Array`, un `ArrayBuffer`, o un `Blob` / `File` de un `<input type="file">`) y devuelve todo lo que necesitan el motor y sus backends:

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

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

bundle.chapters;   // [{ title, file, markdown }, …] en el orden del libro
bundle.config;     // PostextConfig, lista para buildDocument
bundle.resources;  // Resource[]
bundle.files;      // Map<ruta, Uint8Array>: todos los archivos del paquete
```

| Campo | Qué contiene |
| --- | --- |
| `manifest` | El `preset.json` ya validado. |
| `id`, `name`, `description` | Tomados del manifiesto. `name` y `description` son los de la edición leída: los `localized[…].name` y `description` del idioma servido cuando los da, y si no, los del manifiesto. |
| `locale`, `locales` | El idioma en que se leyó el contenido, y todos los idiomas que trae un paquete bilingüe. `options.locale` elige uno: primero la etiqueta exacta, luego el idioma base, luego el idioma propio del paquete. |
| `chapters` | `{ title, file, markdown }` por capítulo. Un capítulo sin título en el manifiesto toma el texto de su primer encabezado `#`. |
| `config` | La paleta de colores por defecto y los tipos de recurso en el idioma del paquete, después la `config` del manifiesto y después los ajustes propios del idioma. El idioma del paquete es el `locale` de arriba, así que un paquete en un solo idioma recibe sus propios rótulos, pida lo que pida `options.locale`. Un manifiesto que no nombra ningún idioma toma el que fija su `config` (`locale` y, si falta, el de la separación silábica) y, si tampoco lo hay, `options.locale`. `customFonts` lista las familias tipográficas del paquete. Es la misma configuración con la que el Sandbox abre el paquete. |
| `resources` | Los recursos, con los pies del idioma elegido. Un tamaño que falte en el manifiesto se lee del archivo. |
| `fonts` | Una entrada por variante: `{ family, weight, style, format, file, bytes }`. |
| `files` | Todos los archivos del zip, por su ruta. |
| `thumbnail`, `canvasScope` | La ruta de la portada, y cómo pide el paquete que se muestre: la `view` del manifiesto, con la `localized[…].view` del idioma servido por encima. |
| `start` | Dónde empieza el libro, para un paquete que contiene parte de uno más largo: el `start` del manifiesto, o el `localized[…].start` del idioma servido. Falta en un libro que empieza por su principio. |
| `warnings` | Problemas que no impiden abrirlo: un archivo de fuente no admitido, un máster de impresión que falta. |

El **`fileId` de cada archivo es su ruta dentro del paquete**. `resource.svg.fileId`, `resource.bitmap.fileId` y el `fileId` de cada variante de `customFonts` se buscan directamente en `bundle.files`. `openBundle` lanza un error si los bytes no son un zip, si no hay un `preset.json` válido (en la raíz o bajo una única carpeta) o si falta un archivo que el manifiesto nombra.

#### Paquetes escritos por postext 1.4 o anterior

Todo manifiesto que escriben `createBundle` y el Sandbox lleva `configVersion: 14`: las reglas de configuración para las que se escribió su `config`. Un manifiesto sin él lo escribió postext 1.4 o anterior, que resolvía dieciocho cosas de otra manera:

- **Los saltos de encabezado** (reglas 3): hasta la 1.4, un objeto `headings` sin salto de H1 no tenía ninguno (ver [Configuración por nivel](https://postext.dev/es/docs/configuration-headings.md#configuración-por-nivel)).
- **El tamaño de las fórmulas** (reglas 4): hasta la 1.4, las fórmulas salían 1,131 veces más grandes de lo que dice `fontSizeScale` (ver [Tamaño de las fórmulas](https://postext.dev/es/docs/configuration-lists-math.md#matemáticas)).
- **El espacio bajo un recurso en línea** (reglas 5): hasta la 1.4, el texto que sigue a una figura o tabla con `placement.position: 'here'` continuaba en la siguiente línea de la rejilla, sin el hueco de los flotantes debajo (ver `layout.inlineResourceGap` en [Disposición](https://postext.dev/es/docs/configuration-page-layout.md#disposición)).
- **El espacio en torno a un recurso en línea dentro de un recuadro** (reglas 6): hasta la 1.4, ese recurso quedaba pegado al texto del recuadro que lo rodea (ver `layout.inlineResourceGapInBoxes` en [Disposición](https://postext.dev/es/docs/configuration-page-layout.md#disposición)).
- **Las marcas en línea de los encabezados** (reglas 6): hasta la 1.4, un encabezado imprimía las palabras de su `*cursiva*`, su `**negrita**` y sus otras marcas con su propio estilo, sin marcas (ver `headings.inlineMarks` en [Encabezados](https://postext.dev/es/docs/configuration-headings.md#encabezados)).
- **El tamaño de una letra capital** (reglas 6): hasta la 1.4, el `dropCap` de un texto de diseño sin `fontSize` era tan alto como todas las cajas de línea que abarca, con la parte alta por encima de la primera línea (ver `dropCap` en [Elementos de texto](https://postext.dev/es/docs/configuration-headers-footers.md#elementos-de-texto)).
- **El sitio bajo una línea de dos puntos** (reglas 6): hasta la 1.4, `keepColonWithList` daba por bueno para la lista una línea de sitio bajo la línea que termina en dos puntos, y un primer elemento de dos líneas que las reglas de huérfanas y viudas mantienen entero pasaba a la columna siguiente sin ella (ver `bodyText.colonListRoom`).
- **Las líneas que deja el corte de una caja** (reglas 6): hasta la 1.4, una caja que se partía dentro de un párrafo o de un elemento de lista podía dejar una sola línea suya a un lado, siempre que cada lado de la caja sumara sus `splitMinLines` líneas (ver `layout.boxChildSplitMinLines` en [Disposición](https://postext.dev/es/docs/configuration-page-layout.md#disposición)).
- **Los cortes de línea tras una raya** (reglas 7): hasta la 1.4, Knuth-Plass nunca terminaba una línea tras una raya o una semirraya puesta entre dos palabras sin espacios (`Madrid–Barcelona`, `I.—Que trata`), y el divisor línea a línea del texto con formato solo entre dos letras (ver `bodyText.breakAfterDashes` en [Texto de cuerpo](https://postext.dev/es/docs/configuration-text.md#texto-de-cuerpo)).
- **El texto en bandera** (reglas 7): hasta la 1.4, el texto corrido en bandera se componía línea a línea, llenando cada línea antes de pasar a la siguiente, dijera lo que dijera `optimalLineBreaking` (ver `bodyText.optimalRagged` en [Texto de cuerpo](https://postext.dev/es/docs/configuration-text.md#texto-de-cuerpo)).
- **La división bajo un encabezado** (reglas 8): hasta la 1.4, el párrafo que sigue a un encabezado al pie de una columna conservaba allí todas las líneas que cupieran, aunque pasaran muy pocas a la columna siguiente (ver `headings.keepWithNextSplit` en [Encabezados](https://postext.dev/es/docs/configuration-headings.md#encabezados)).
- **El espacio bajo un contenedor `:::paragraphs`** (reglas 8): hasta la 1.4, el espacio del estilo se ponía bajo el último párrafo antes del ajuste a la rejilla, el espacio del bloque siguiente (el `marginTop` de un encabezado) se sumaba debajo y la separación entre párrafos del texto no contaba (ver `bodyText.paragraphContainerSpacing` en [Texto de cuerpo](https://postext.dev/es/docs/configuration-text.md#texto-de-cuerpo)).
- **Los cortes de línea tras el guion de un compuesto** (reglas 8): hasta la 1.4, Knuth-Plass nunca terminaba una línea justificada tras un guion entre dos letras (`físico-química`) en un párrafo sin formato en línea, y sí lo hacía en uno con formato (ver `bodyText.breakAfterHyphens` en [Texto de cuerpo](https://postext.dev/es/docs/configuration-text.md#texto-de-cuerpo)).
- **Los poemas sin separador** (reglas 9): hasta la 1.22, un poema `:::verse` cuyas líneas no llevaban `||` se componía en hemistiquios sueltos, cada línea centrada (véase `bodyText.verse.layout` en [Verso](https://postext.dev/es/docs/configuration-text.md#verso)).
- **Una sangría de primera línea junto a una francesa** (reglas 9): hasta la 1.22, la `hangingIndent` de un estilo de párrafo sustituía a su `firstLineIndent`, y la primera línea empezaba en `indent` (véase [Estilos de párrafo](https://postext.dev/es/docs/configuration-styles.md#estilos-de-párrafo)).
- **Una barra invertida al final de una línea** (reglas 9): hasta la 1.22, una barra invertida al final de una línea de un párrafo, una cita o un elemento de lista, y `\\` ante un espacio, se imprimían, y las líneas se unían con un espacio (ver `bodyText.hardLineBreaks` en [Texto de cuerpo](https://postext.dev/es/docs/configuration-text.md#texto-de-cuerpo)).
- **Las vallas de código** (reglas 9): hasta la 1.22, una valla ```` ``` ```` o `~~~` y las líneas que encierra se leían como Markdown: las líneas se unían en párrafos, una línea con `#` se convertía en encabezado y las vallas se imprimían (ver `codeStyle.blocks` en [Listados de código](https://postext.dev/es/docs/configuration-styles.md#listados-de-código)).
- **Los versos partidos** (reglas 10): en la 1.23, un verso de un poema compuesto verso a verso más ancho que la caja se partía con su espaciado natural, por poco que sobrara (ver `bodyText.verse.tighten` en [Verso](https://postext.dev/es/docs/configuration-text.md#verso)).

`openBundle` y `readBundle` leen el `config` de un manifiesto así, y la configuración de cada idioma en `localized`, a través de `migrateConfig`, que escribe los saltos tal como los componía la 1.4 y multiplica la escala de las fórmulas por 1,131 (y divide por ese factor los márgenes de las fórmulas en bloque dados en `em`). A un manifiesto marcado con `3` a `7`, que escribió una versión preliminar de la 1.5, solo se le aplican las fijaciones de las reglas posteriores a su marca: con `3`, el tamaño de las fórmulas, el hueco en línea, las cinco fijaciones de las reglas 6, las dos de las reglas 7 y las tres de las reglas 8; con `4`, el hueco en línea y las de las reglas 6, 7 y 8; con `5`, las de las reglas 6, 7 y 8; con `6`, las de las reglas 7 y 8; con `7`, solo las de las reglas 8. La división bajo un encabezado (`pinLegacyHeadingSplit`) se escribe como `headings.keepWithNextSplit: 'fill'` sobre el `headings` que dejan en vigor las capas, cuando algún capítulo leído tiene un encabezado y la configuración no nombra un valor propio, mantiene activado `headings.keepWithNext` y no desactiva `bodyText.avoidOrphans`. Los cortes tras el guion de un compuesto (`pinLegacyHyphenBreaks`) se escriben como `bodyText.breakAfterHyphens: false` sobre el `bodyText` en vigor cuando un capítulo leído tiene un guion entre dos letras y la configuración ni lo fija ya ni desactiva `optimalLineBreaking`. El espacio bajo los contenedores (`pinLegacyParagraphContainerSpacing`) se escribe como `bodyText.paragraphContainerSpacing: 'add'` sobre el `bodyText` en vigor cuando la configuración declara algún estilo de párrafo (en `paragraphStyles` o en los ajustes propios del visor HTML), un capítulo leído abre un contenedor `:::paragraphs` en una línea propia y la configuración no lo fija ya. Los cortes tras una raya (`pinLegacyDashBreaks`) se escriben como `bodyText.breakAfterDashes: false` sobre el `bodyText` en vigor cuando un capítulo leído tiene una raya o una semirraya entre palabras sin espacios (una letra, una cifra o un signo de cierre delante, y una letra, una cifra o un signo de apertura detrás; unas comillas delante de la raya cuentan cuando las precede una letra, una cifra, un signo de cierre o un espacio de no separación, como en `«no»—y`, no en `dijo "—Hola`; una marca en línea pegada a la raya o a las comillas, como los `**` de `**I.**—Que`, cuenta a cualquiera de los dos lados) y la configuración no lo fija ya. La composición del texto en bandera (`pinLegacyRaggedBreaking`) se escribe como `bodyText.optimalRagged: false` sobre el `bodyText` en vigor cuando la configuración pone en bandera algún texto corrido (un `textAlign` distinto de `'justify'` en el texto de cuerpo, un estilo de párrafo, el cuerpo de un recuadro, el de una parte o el de un estilo de sección (`headingStyles[].bodyStyle`), o en los ajustes propios del visor HTML), no lo fija ya y no desactiva `optimalLineBreaking`. El hueco en los recuadros (`pinLegacyBoxResourceGap`) se escribe como `layout.inlineResourceGapInBoxes: false` sobre el `layout` en vigor cuando un recurso se inserta en su propia línea dentro de un `:::callout` de los capítulos leídos y la configuración no lo fija ya. El corte de las cajas (`pinLegacyBoxChildCut`) se escribe como `layout.boxChildSplitMinLines: 1` sobre el `layout` en vigor cuando un capítulo leído abre un `:::callout` en una línea propia y la configuración no lo fija ya. Las marcas de los encabezados (`pinLegacyHeadingMarks`) se escriben como `headings.inlineMarks: false` sobre el `headings` que dejan en vigor las capas, cuando algún encabezado de los capítulos leídos lleva una marca (`*`, `_`, `^`, `~`, `:smallcaps[` o un enlace en su título) y la configuración no nombra un valor propio. A las letras capitales (`pinLegacyDropCapSize`) se les escribe su tamaño de la 1.4 como `dropCap.fontSize`, estén donde estén: en la unidad del interlineado del elemento cuando es una longitud, y si no en la unidad de su cuerpo. El sitio bajo los dos puntos (`pinLegacyColonListRoom`) se escribe como `bodyText.colonListRoom: 'line'` sobre el `bodyText` en vigor cuando una lista de los capítulos leídos sigue a una línea que termina en dos puntos (aunque haya líneas en blanco entre ellas) y la configuración ni nombra un sitio ni desactiva `keepColonWithList`. El hueco en línea (`pinLegacyInlineGap`) se escribe como `layout.inlineResourceGap: 'above'` sobre el `layout` que dejan en vigor las capas, cuando una línea de los capítulos leídos inserta un recurso (`::resource{id="…"}` sola en su línea, tal como la lee el analizador: una mención en el texto o en un fragmento de código no cuenta) y la configuración no nombra un hueco propio. El tamaño se fija sobre el `math` que dejan en vigor las capas (el `math` propio de un idioma sustituye al compartido), y solo cuando los capítulos leídos tienen algún `$`: un paquete sin fórmulas conserva su `config` tal como se escribió. Si ni el manifiesto ni el idioma dan un `math`, el que queda en vigor es el de la `baseConfig` de `readBundle` (la del lector), y también se fija, porque la 1.4 componía las fórmulas del paquete a ese tamaño: una base con `fontSizeScale: 1.5` se lee como 1,5 × 1,1312. Los saltos de encabezado de la base se toman tal como vienen. Así un paquete antiguo conserva lo que componían esas reglas, y `bundle.config` muestra los saltos, el tamaño de las fórmulas, los huecos, las marcas de los encabezados, el tamaño de las letras capitales, el sitio bajo los dos puntos, el corte de las cajas, los cortes tras una raya, los cortes tras el guion de un compuesto, la composición del texto en bandera, la división bajo un encabezado y el espacio bajo los contenedores con los que se compone. Las correcciones de composición de la 1.5 no tienen fijación y se le aplican como a cualquier libro, así que una página a la que afecten aún puede moverse (la lista está en [Tamaño de las fórmulas](https://postext.dev/es/docs/configuration-lists-math.md#matemáticas)). Un `preset.json` escrito a mano para las reglas actuales pone `"configVersion": 14`; marcar así el manifiesto de un paquete antiguo es también la forma de leerlo, con una línea, con las reglas actuales (un paquete sin versión pierde entonces también la fijación de sus saltos de encabezado). Un manifiesto marcado `8`, escrito por postext 1.5 a 1.22, recibe solo los cuatro fijados de las reglas 9, como también todos los anteriores: la composición del verso (`pinLegacyVerseLayout`) se escribe como `bodyText.verse.layout: 'bayt'` en el `bodyText` vigente, cuando un capítulo leído compone un poema `:::verse` cuya apertura no nombra composición y cuyas líneas no llevan separador de hemistiquios, y la configuración no lo fija ya; las sangrías emparejadas (`pinLegacyPairedIndents`) quitan la `firstLineIndent` de todo estilo de párrafo (en `paragraphStyles` o en los ajustes del visor HTML) que fija también una `hangingIndent` distinta de cero; los saltos de línea forzados (`pinLegacyHardBreaks`) se escriben como `bodyText.hardLineBreaks: false` en el `bodyText` vigente, cuando un capítulo leído termina una línea de un párrafo, una cita o un elemento de lista con una barra invertida y el bloque sigue debajo, o pone `\\` ante un espacio y más texto (salvo el código en línea y las fórmulas, las fórmulas destacadas, los títulos y los poemas `:::verse`), y la configuración no lo fija ya; y las vallas de código (`pinLegacyCodeBlocks`) se escriben como `codeStyle.blocks: false`, cuando un capítulo leído abre una valla ```` ``` ```` o `~~~` (de tres o más caracteres, con hasta tres espacios delante) y la configuración no lo fija ya. Un manifiesto marcado `9`, escrito por postext 1.23, recibe solo la fijación de las reglas 10, como también todos los anteriores: los versos partidos (`pinLegacyVerseTightening`) se escriben como `bodyText.verse.tighten: false` en el `bodyText` vigente, cuando un capítulo leído compone un poema verso a verso (una apertura `:::verse` que nombra `layout=lines`, o que no nombra composición sobre versos sin separador de hemistiquios mientras la configuración no componga esos poemas como bayts) y la configuración no lo fija ya. Un manifiesto marcado `10`, escrito por postext 1.24, recibe solo la fijación de las reglas 11, como también todos los anteriores: el equilibrado de una retícula de caracteres (`pinLegacyGridBalancing`) se escribe como `headings.balancing.enabled: true` en el `headings` vigente, cuando la configuración combinada fija `cjk.grid.enabled` en texto horizontal y no fija ella misma `enabled`, porque la 1.24 equilibraba esas páginas por defecto. Las reglas 11 también impiden que un título de obra se parta tras un solo carácter, componen los números en círculo como caracteres chinos y el texto de diseño CJK con las reglas del cuerpo (#637): un manifiesto marcado `10` o anterior recibe `cjk.titleMinChars: 1` (`pinLegacyTitleBreaks`) cuando un capítulo leído tiene un título (《, 〈 o `:book[`), `cjk.circledNumbers: 'western'` (`pinLegacyCircledNumbers`) cuando uno tiene un número en círculo (U+2460–U+24FF, U+2776–U+2793) y `cjk.composeDesignText: false` (`pinLegacyDesignText`) cuando un capítulo leído o la propia configuración tienen texto CJK; cada uno en el `cjk` vigente y solo si la configuración no lo fija ya. También cortan las tablas en línea y componen `:::columns` en el texto corrido (#634): un manifiesto marcado `10` o anterior recibe `tableStyle.splitInline: false` (`pinLegacyInlineTableSplit`) en el `tableStyle` vigente cuando un capítulo leído inserta un recurso, y `layout.flowColumns: false` (`pinLegacyFlowColumns`) en el `layout` vigente cuando un capítulo leído abre una valla `:::columns` en una línea propia, cada uno solo si la configuración no lo fija ya. También ofrecen a un flotante la cabeza de la columna de una apertura a ancho de página (#639): un manifiesto marcado `10` o anterior recibe `layout.floatsUnderOpener: false` (`pinLegacyOpenerHeadFloats`) en el `layout` vigente cuando la configuración combinada define un nivel o un estilo de encabezado con `span: 'page'` y un capítulo leído tiene un encabezado, solo si la configuración no lo fija ya. Las reglas de 12 hacen que una página en una retícula de caracteres contenga sus líneas (#717): un manifiesto marcado `11` o anterior cuya configuración combinada activa `cjk.grid.enabled` recibe `bodyText.avoidWidows: true`, y en la región `japan` `bodyText.avoidOrphans: true` (`pinLegacyGridBreakRules`), en el `bodyText` vigente, cada una solo si la configuración no la fija ya. También mantienen dos caracteres de una nota en dos filas a cada lado de un corte de línea (#720): un manifiesto así recibe `cjk.warichu.minChars: 1` (`pinLegacyNoteBreaks`) en el `cjk` vigente cuando un capítulo leído tiene una nota `:warichu[` o la configuración combinada fija `citations.notes: 'warichu'`, solo si la configuración no lo fija ya. Y en la región de Japón una palabra occidental lleva el espacio entre chino y latín junto a sus paréntesis y signos (#721): un manifiesto marcado `11` o anterior cuya configuración combinada se resuelve a la región de Japón recibe `cjk.latinSpacingEdge: 'letters'` (`pinLegacyLatinSpacingEdge`) en el `cjk` vigente cuando un capítulo leído tiene kana o kanji, solo si la configuración no lo fija ya. Y un recurso puesto en el texto admite `placement.captionSide` como un flotante de columna (#729): un manifiesto marcado `11` o anterior cuya configuración combinada fija `layout.layoutType: 'oneAndHalf'` recibe `layout.inlineCaptionSide: false` (`pinLegacyInlineCaptionSide`) cuando un capítulo leído incrusta un recurso, solo si la configuración no lo fija ya.

Las reglas de 12 cambian además cómo se equilibra una columna corta, y un manifiesto marcado `11` o anterior conserva aquello con lo que se compuso. El orden de las palancas (#707): recibe `headings.balancing.maxLinesBeforeLoose` con el valor de su propio `maxLinesPerHeading`, o `4` si no fija ninguno (`pinLegacyLooseOrder`), en el `headings` vigente, cuando un capítulo leído tiene un encabezado y la configuración combinada equilibra sus columnas, deja que un encabezado tome más de dos líneas y no fija ya la clave. La línea bajo una figura a varias columnas (#708): recibe `headings.balancing.stretchAfterWideFloats: 'always'` (`pinLegacyWideFloatStretch`) en el `headings` vigente, cuando un capítulo leído incrusta o cita un recurso o hace flotar una caja, y la configuración combinada equilibra sus columnas, no desactiva `stretchAfterFloats` y no fija ya la clave. Los cortes dentro de los grupos de un índice (#712): recibe `index.groups.splitMinEntries: 1` (`pinLegacyIndexGroupSplit`) en el `index` vigente, cuando un capítulo leído tiene un cerco `:::index` en una línea propia y la configuración no fija ya la clave. La unidad del corte a nivel (#711): recibe `headings.balancing.trailingStep: 'grid'` (`pinLegacyTrailingStep`) en el `headings` vigente, cuando un capítulo leído puede poner texto fuera del interlineado base (un cerco `:::`, un cerco de código, una cita en bloque, una lista, una fórmula en bloque o una cita bibliográfica), y la configuración combinada equilibra sus columnas, mantiene activo `trailing` o `beforeSpan` y no fija ya la clave.

Las reglas de 12 cortan el rótulo de una referencia por sus espacios cuando el rótulo son palabras (#702). Un manifiesto marcado `11` o anterior recibe `crossRefs.breakLabels: false` (`pinLegacyRefLabelBreaks`) en el `crossRefs` vigente, cuando un capítulo leído contiene una referencia (`:ref{…}`, `\ref{…}`, `\eqref{…}` o un `@id` de pandoc-crossref) y la configuración no nombra el ajuste: cada rótulo sigue siendo la palabra única con la que se compuso.

Las reglas de 12 componen una palabra sin letras (`46.`, `1367)`) en la escritura del texto que la rodea (#725). Un manifiesto marcado `11` o anterior recibe `bodyText.scriptContext: false` (`pinLegacyScriptContext`) sobre el `bodyText` vigente, contengan lo que contengan sus capítulos, cuando la configuración no nombra el ajuste: esa palabra se compone aislada, como se maquetó. La regla actúa en una fuente que dibuja su puntuación según la escritura, y al leer un manifiesto no se sabe qué fuentes carga el libro.

Las reglas de 13 convierten en un ajuste el idioma en el que se compone el texto (#764), y las de 14 hacen del idioma del propio documento su valor por defecto (#770). Un manifiesto marcado `13` o anterior recibe `bodyText.shapingLanguage: 'host'` (`pinLegacyShapingLanguage`) sobre el `bodyText` vigente, contengan lo que contengan sus capítulos, cuando la configuración no nombra el ajuste; el que nombra un valor lo conserva. Su texto se mide en el idioma de la página que aloja la maquetación, y en ninguno dentro de un worker, como se maquetó. Un manifiesto marcado `13` lo escribió postext 1.29: la 1.29.1 y la 1.29.2 maquetaban en el idioma del anfitrión un libro que no nombra ningún valor, como hace la fijación; la 1.29.0, cuyo valor por defecto era `'document'`, lo maquetaba en el del documento con un canvas y un PDF que no seguían la medida, y también se lee con `'host'`. Un manifiesto marcado `14` que no nombra ningún valor se maqueta en el idioma del documento.

Las reglas de 13 también dibujan la casilla de un elemento de tarea (`- [ ]`, `- [x]`): un cuadrado que dibuja el motor, con una marca cuando la tarea está hecha, y que ocupa un espacio propio antes del texto del elemento (#765). Un manifiesto marcado `12` o anterior recibe `unorderedLists.taskBox: 'character'` (`pinLegacyTaskBox`) sobre el `unorderedLists` vigente, cuando algún capítulo contiene un elemento de tarea y la configuración no nombra el ajuste. Su casilla sigue siendo el carácter `☐` o `☑` compuesto en la fuente de la lista, y el texto del elemento empieza tras el ancho de ese carácter, como se maquetó. Ese ancho lo daba la fuente que tuviera el carácter, una del sistema en la mayoría de los libros, y al leer el manifiesto no se conoce.

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

migrateConfig({ headings: { fontFamily: 'Georgia' } }, undefined, { content: 'Un libro sin fórmulas.' });
// => { 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: 'Texto.\n\n::resource{id="fig"}' });
// => { layout: { layoutType: 'single', inlineResourceGap: 'above' } }
migrateConfig({ layout: { layoutType: 'single' } }, 5, { content: ':::callout\nTexto.\n\n::resource{id="fig"}\n:::' });
// => { layout: { layoutType: 'single', inlineResourceGapInBoxes: false, boxChildSplitMinLines: 1 } }
migrateConfig({ bodyText: { textAlign: 'left' } }, 6, { content: 'Te digo—y eso es todo.' });
// => { bodyText: { textAlign: 'left', breakAfterDashes: false, optimalRagged: false } }
migrateConfig({ paragraphStyles: [{ id: 'verso' }] }, 7, { content: ':::paragraphs{style="verso"}\nUn verso.\n:::' });
// => { paragraphStyles: [{ id: 'verso' }], bodyText: { paragraphContainerSpacing: 'add' } }
migrateConfig({ bodyText: { fontFamily: 'Georgia' } }, 7, { content: 'Un ejercicio teórico-práctico.' });
// => { bodyText: { fontFamily: 'Georgia', breakAfterHyphens: false } }
migrateConfig(config, CONFIG_VERSION); // reglas actuales: el propio `config`
```

`content` es el markdown que compone la configuración (una cadena o una lista de capítulos). Sin él, los dos huecos, las marcas de los encabezados, el sitio bajo los dos puntos, el corte de las cajas, los cortes tras una raya, la división bajo un encabezado y los cortes tras el guion de un compuesto se fijan siempre, el tamaño de las fórmulas siempre que las matemáticas estén activadas y el espacio bajo los contenedores siempre que la configuración declare algún estilo de párrafo, porque el motor no puede saber si el libro tiene alguna fórmula, algún contenedor `:::paragraphs`, alguna figura en línea, algún encabezado con marcas, alguna lista introducida con dos puntos, alguna caja, alguna raya entre palabras, algún encabezado o algún compuesto. La composición del texto en bandera se fija solo según la configuración, haya contenido o no. Migra una configuración guardada una sola vez y vuelve a guardarla con `CONFIG_VERSION`: la fijación del tamaño multiplica la escala, así que una configuración migrada dos veces crecería dos veces. Sin él también se fijan la composición del verso y las vallas de código, porque el libro puede tener un poema `:::verse` sin separador o una valla, y las sangrías emparejadas se fijan solo por la configuración. También se fijan los versos partidos de la 1.23, porque el libro puede componer un poema verso a verso.

### Componer y renderizar un paquete

Cuatro utilidades conectan un paquete abierto con el motor y los backends:

- **`loadBundleFonts(bundle)`** registra las fuentes del paquete en `document.fonts`, y sus bytes en el registro de fuentes del motor para las imágenes SVG (`registerFontBytes`). Espérala antes de componer, porque la composición mide el texto con las fuentes que tiene el navegador. Las familias que el paquete nombra pero no incluye (Google Fonts) tienes que cargarlas tú, como en cualquier otro documento.
- **`registerBundleImages(bundle)`** decodifica las imágenes para el backend canvas (`renderPage`, `renderToCanvas`), portadas de vídeo incluidas. **`bundleImageUrl(bundle)`** es el resolvedor `resourceImageUrl` de `renderToHtml`, y **`bundleVideoUrl(bundle)`** su resolvedor `resourceVideoUrl` para los archivos de vídeo que lleva un paquete. Las dos recolorean las figuras SVG cuando `diagramStyle.singleInk` está activo, una sola vez: recolorean el marcado y marcan las imágenes para que ningún backend vuelva a teñirlas (consulta [Tinta única en canvas y en HTML](https://postext.dev/es/docs/configuration-resources.md#tinta-única-en-canvas-y-en-html)). Las dos incrustan además en cada SVG las variantes que nombra su texto, primero desde las fuentes del propio paquete y después desde las variantes registradas en el motor (consulta [Fuentes en el texto de los SVG](https://postext.dev/es/docs/configuration-resources.md#fuentes-en-el-texto-de-los-svg)); `registerBundleImages(bundle, { onWarning })` y `bundleImageUrl(bundle, { onWarning })` avisan de una familia sin variante.
- **`buildBundle(bundle)`** compone los capítulos en orden y devuelve un `VDTDocument` por capítulo. Cada capítulo continúa al anterior: contadores de encabezados y de recursos, la parte abierta, la paridad de página y la numeración. Un capítulo que imprime el índice de contenidos (`:::toc`) o el analítico (`:::index`) recibe el esquema del libro entero. Admite las mismas opciones que `buildDocument`, más `config` para sustituir la configuración del paquete, `cache` para compartir una caché de medidas y `metadata` (ver más abajo).
- **`bundleResourceBytes(bundle)`** y **`bundleFontProvider(bundle, { decodeWoff2, fallback })`** son las opciones `resourceBytes` y `fontProvider` de `renderToPdf` de `postext-pdf`. El proveedor de fuentes elige del paquete el peso más cercano del estilo pedido. Para una variante `.woff2` necesita `decompressWoff2`, y para una familia que el paquete no incluye llama a `fallback` con los argumentos del renderizador, `request` incluido, y entrega lo que devuelva. Un `fallback` que solo descarga el archivo `latin` de la familia imprime como cajas vacías una familia china que el paquete no incrusta; uno que responde con los fragmentos, como `sliceFontProvider` en [Fuentes chinas, japonesas y coreanas](https://postext.dev/es/docs/configuration-pdf-output.md#fuentes-chinas-japonesas-y-coreanas), la imprime completa.

```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);                        // un VDTDocument por capítulo
const primeraPagina = renderPage(docs[0].pages[0], docs[0]); // un <canvas>

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

Para componer un solo capítulo por tu cuenta, pasa `bundle.chapters[i].markdown`, `bundle.resources` y `bundle.config` a `buildDocument`, como con cualquier documento.

**Los metadatos del libro.** Como en el Sandbox, el frontmatter del primer capítulo es el del libro: `buildBundle` entrega su `title`, su `author` y los demás a todos los capítulos, de modo que las cabeceras con `{title}` y `{author}` se mantienen en todas las páginas y el `doc.metadata` de cada capítulo los lleva. Un bloque de frontmatter al principio de un capítulo posterior se ignora: se localiza por sus líneas `---` y se deja en blanco sin analizarlo, así que un YAML que el analizador rechazaría no causa ningún problema. `options.metadata` aporta valores que el frontmatter no fija (el frontmatter prevalece). El recuento de páginas del libro también llega a todos los capítulos: `{bookTotalPages}` lo imprime, mientras que `{totalPages}` cuenta el capítulo (consulta [Recuento de páginas del libro](https://postext.dev/es/docs/configuration-headers-footers.md#recuento-de-páginas-del-libro)).

```ts
const docs = buildBundle(bundle, { metadata: { author: 'A. Autora' } });
docs[3].metadata.title;   // el `title:` del primer capítulo
```

**Dónde empieza el libro.** Un paquete puede contener parte de una publicación más larga: las páginas 58 a 61 de un número, el capítulo 4 de un libro de texto. Su `start` dice qué lo precede, en los términos de la `continuation` de `buildDocument`: las páginas anteriores a la primera (`pageIndexOffset`, que decide de qué lado cae la página 1 y, con él, los márgenes en espejo y las cabeceras de páginas pares e impares), la numeración de página vigente (`pageNumbering`), los contadores de encabezados (`headings`), la parte abierta (`part`) y los contadores de recursos, enunciados, notas al pie y líneas. `createBundle` lo escribe como `start` en `preset.json`, `openBundle` lo devuelve en `bundle.start`, y `buildBundle` compone con él el primer capítulo, como haría `buildDocument({ markdown, continuation: start }, config)`, y encadena a partir de ahí los siguientes; `{bookTotalPages}` cuenta también las páginas anteriores al libro. Un manifiesto sin `start` se lee como antes, y un lector que no conoce el campo lo ignora. `bookPageCount` no forma parte de él: las páginas las cuenta quien lee. En un paquete con varios idiomas, `localized[…].start` da a una edición su propio comienzo.

```ts
const { bytes } = await createBundle({
  name: 'Field notes, chapter 4',
  markdown,
  config,
  // Página 58, par, en el capítulo 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'
```

### Ejemplo en vivo: abrir un paquete

El pen carga un libro de ejemplo de dos capítulos (`lantern.postext`, con su propia tipografía, una figura SVG y una tabla) desde el repositorio. Registra las fuentes e imágenes del paquete, compone el libro con `buildBundle` y pinta todas las páginas. *Make the PDF* renderiza los mismos documentos con `postext-pdf`, incrustando las fuentes del paquete. Elige un `.postext` tuyo, por ejemplo uno exportado del Sandbox, para verlo igual.

> **Ejemplo ejecutable: Postext · abrir un paquete .postext** — Abrir un archivo .postext con postext, componer el libro y renderizarlo en canvas y PDF. ([código](https://github.com/drnachio/postext/tree/main/docs/examples/open-bundle))

### Crear un paquete

`createBundle` escribe un archivo `.postext` a partir de un documento: sus capítulos, su configuración, sus recursos y los datos a los que estos hacen referencia.

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

const { bytes, manifest, warnings } = await createBundle({
  name: 'El farol',
  locale: 'es',
  chapters: [
    { markdown: '# Anochecer\n\nSe dibuja en :ref{id="farol"}.' },
    { title: 'Noche', markdown: '# Noche\n\n…' },
  ],
  config,
  resources: [{
    id: 'farol', typeId: 'figure', kind: 'svg', caption: 'El farol.',
    svg: { fileId: 'farol.svg', width: 240, height: 150 },
    createdAt: 0, updatedAt: 0,
  }],
  files: { 'farol.svg': svgMarkup, 'garamond-regular': fontBytes },
});
```

| Entrada | Significado |
| --- | --- |
| `name`, `id`, `description`, `locale` | Los metadatos del manifiesto. Por defecto `id` es un slug de `name`. |
| `chapters` o `markdown` | El libro, un `{ title?, markdown }` por capítulo, o un documento único. |
| `config` | La `PostextConfig`. Los valores iguales a los de por defecto no se escriben en el manifiesto. |
| `resources` | Los recursos. Una imagen nombra sus datos con `bitmap.fileId` / `svg.fileId` (y `svg.pdfFileId` para un máster de impresión). |
| `files` | Los datos por `fileId` (un objeto o un `Map`): las imágenes que citan los recursos y los archivos de fuente que citan las variantes de `config.customFonts`. Cada valor puede ser un `Uint8Array`, un `ArrayBuffer`, un `Blob` o un texto (el código de un SVG). |
| `thumbnail` | `{ data, mime }`: una portada (PNG, JPEG, WebP, GIF o SVG). |
| `canvasScope` | `'book'` pide a los visores que compongan el libro entero como un solo lienzo. |
| `start` | Dónde empieza el libro, para un paquete que contiene parte de uno más largo: la `continuation` con la que se compondría un documento suelto. Se escribe como `start` del manifiesto. |
| `mtime` | La fecha de modificación que se escribe en cada archivo del zip (un `Date`, una marca de tiempo o una fecha en texto). Si falta, es la hora de la llamada, así que dos llamadas con los mismos datos dan bytes distintos. Con una fecha fija, los mismos datos dan siempre los mismos bytes, que se pueden comparar o resumir con un hash. Un zip guarda una fecha y una hora sin zona horaria, en pasos de dos segundos, de 1980 a 2099, y la fecha se escribe en la hora local de la máquina. Para que los bytes coincidan en cualquier máquina, construye la fecha con campos locales, como `new Date(1980, 0, 1)`: una marca de tiempo o un texto que acaba en `Z` nombran un instante, que cae en una hora local distinta en cada zona horaria (`'1980-01-01T00:00:00Z'` todavía es 1979 al oeste de UTC). Una fecha fuera de esos años, en hora local, lanza un error. |
| `localized` | Otros idiomas del mismo libro, por etiqueta de idioma: `{ en: { name?, description?, chapters?, config?, resources? } }`. Las entradas anteriores pasan a ser el contenido de `locale`, que entonces es obligatorio. Consulta [Paquetes bilingües](https://postext.dev/es/docs/configuration-bundles.md#paquetes-bilingües). |

Devuelve los `bytes` del archivo, el `manifest` escrito como `preset.json`, todos los archivos en `files` (ruta → bytes) y una lista de `warnings`. Los archivos se nombran por el id de su recurso (`resources/farol.svg`) o por el nombre del archivo de fuente (`fonts/…`), y los capítulos por su orden y título (`chapters/01-anochecer.md`). Las fuentes se declaran en el campo `fonts` del manifiesto, nunca dentro de `config.customFonts`. Algunas cosas se dejan fuera, cada una con un aviso:
- un recurso o una variante tipográfica cuyos datos no están en `files`
- una variante `.woff` (el backend PDF no puede incrustarla)
- una familia marcada `redistributable: false`

En el navegador, entrega `bytes` a un enlace de descarga: `URL.createObjectURL(new Blob([bytes], { type: 'application/zip' }))`. En Node, escríbelos con `fs.writeFile`. `createBundle` y `openBundle` no necesitan DOM. Los dists usan rutas de módulo sin extensión, así que con Node a secas, sin bundler, necesitan un hook de resolución. El archivo `docs/examples/open-bundle/build-sample.mjs` del repositorio muestra uno en pocas líneas.

### Paquetes bilingües

Un archivo `.postext` puede llevar un libro en varios idiomas, y `openBundle(bytes, { locale })` lo lee en cualquiera de ellos. `createBundle` escribe uno a partir de `localized`: una entrada por cada idioma adicional, con lo que difiere del contenido principal (el de `locale`):

```ts
const { bytes, manifest } = await createBundle({
  name: 'El farol',
  locale: 'es',
  chapters: [{ markdown: '# Anochecer\n\n…' }, { markdown: '# Noche\n\n…' }],
  config,
  resources: [figuraFarol, tablaHoras],
  files: { 'farol.svg': svgEs, 'lantern.svg': svgEn },
  localized: {
    en: {
      name: 'The Lantern',
      chapters: [{ markdown: '# Dusk\n\n…' }, { markdown: '# Night\n\n…' }],
      config: { headings: { levels: [{ level: 1, numberingTemplate: 'Chapter {1}' }] } },
      resources: [
        { id: 'farol', caption: 'The lantern.', svg: { fileId: 'lantern.svg', width: 240, height: 150 } },
        { id: 'horas', caption: 'Hours of light.' },
      ],
    },
  },
});

const en = await openBundle(bytes, { locale: 'en' });   // capítulos, configuración y pies en inglés
```

- **`chapters`**: el libro en ese idioma. Los archivos de capítulo van a una carpeta por idioma (`chapters/es/01-anochecer.md`, `chapters/en/01-dusk.md`) y el campo `chapters` del manifiesto pasa a ser un mapa idioma → capítulos. Un idioma sin `chapters` lee los principales; cuando ningún idioma tiene capítulos propios, siguen siendo una sola lista.
- **`config`**: la configuración de ese idioma. Cada clave de primer nivel sustituye por completo a la compartida cuando el paquete se lee en ese idioma, así que `headings` sustituye aquí el objeto `headings` entero. Las claves que faltan, o que son iguales a las compartidas, se comparten y no se escriben, de modo que pasar la configuración completa del idioma funciona igual que pasar las pocas claves que cambian. Una clave con sus valores por defecto cuando la compartida no los tiene (`layout: {}`) se escribe tal cual, así que restablece el valor compartido. Las fuentes se comparten: las familias del `customFonts` de un idioma se suman al `fonts` del paquete.
- **`resources`**: el texto de los recursos compartidos, emparejados por `id`: `caption`, `note`, `altText` y el `table` de una tabla. Una imagen con palabras puede tener su propio dibujo: `bitmap.fileId` o `svg.fileId` (y `svg.pdfFileId`) nombran otros datos de `files`, que se escriben como `resources/en/farol.svg`. Los demás campos, como el tipo o la colocación, se comparten. Un id que no está entre los `resources` se deja fuera con un aviso, y si falta la imagen de un idioma, ese idioma se queda con la compartida, también con un aviso.
- **`name`**, **`description`**: el título y la descripción del libro en ese idioma, que se escriben como `localized[…].name` y `description` cuando no coinciden con los compartidos. `openBundle` los devuelve como `bundle.name` y `bundle.description` en ese idioma, el Sandbox lista la edición con ese título (al importarla y en su biblioteca) y `postext info` y `postext check` los imprimen. Una entrada del `index.json` de una fuente de presets puede copiarlos en `localized` (`localized.ca.name`) para que la biblioteca nombre cada edición antes de abrirla. Un idioma sin ellos conserva el `name` y la `description` del manifiesto.

El manifiesto enumera todos los idiomas en `locales` (`['es', 'en']`), conserva el principal como `locale` y guarda el resto en `localized`. `openBundle` sin idioma lee el principal.

**Qué idioma recibe el lector.** `openBundle(bytes, { locale })` sirve el idioma exacto, si no su idioma base (`es-MX` lee `es`), y si no el principal; `bundle.locale` dice cuál sirvió. Capítulos y textos vienen siempre del mismo idioma. El idioma principal conserva los textos compartidos aunque `localized` traiga una variante regional suya: un paquete `pt-PT` con una entrada `pt-BR` lee los pies brasileños solo para `pt-BR`, y los compartidos para `pt-PT` y `pt`.

### Ejemplo en vivo: crear un paquete

El pen compone un libro de dos capítulos con una figura SVG y lista los archivos que escribió `createBundle` junto con el manifiesto. Ofrece el archivo para descargar, lo vuelve a abrir con `openBundle` y pinta su primera página: el viaje de ida y vuelta completo en pocas líneas. Importa el archivo descargado en el Sandbox para seguir trabajando en él allí.

> **Ejemplo ejecutable: Postext · crear un paquete .postext** — Escribir un archivo .postext con createBundle de postext, descargarlo y volver a abrirlo. ([código](https://github.com/drnachio/postext/tree/main/docs/examples/create-bundle))

### Trabajar con paquetes

Como el Sandbox, el skill para agentes y el paquete `postext` leen y escriben el mismo archivo, un `.postext` es una forma cómoda de pasar un libro de una herramienta a otra:

- **Partir de un paquete.** Porta una publicación existente con el [skill para agentes](https://postext.dev/es/docs/skill.md), o diseña un libro en el [Sandbox](https://postext.dev/es/sandbox.md) y expórtalo (**Descargar (.postext)** en el menú ⋯ de su fila del panel Libros). Carga el archivo desde tu programa con `openBundle` para renderizarlo en canvas, HTML o PDF. Conserva el archivo como fuente del libro: edita en código los capítulos, la configuración o los recursos y vuelve a escribirlo con `createBundle`, o simplemente vuelve a cargarlo cada vez que cambie.
- **Depurar y ajustar en el Sandbox.** Cuando algo de lo que produce tu programa necesita retoques (una figura que cae en la página equivocada, un estilo de encabezado, el equilibrio de columnas), exporta lo que compone tu programa con `createBundle`. Importa ese archivo en el Sandbox (*Libros → Nuevo → Abrir un archivo .postext…*), corrige el texto, el diseño o las figuras con la vista previa en vivo, el panel Revisión y la vista PDF, y vuelve a exportarlo. Después tu programa carga el archivo corregido con `openBundle`. O copia lo que cambió de vuelta a tu código: la `config` del manifiesto solo guarda los valores distintos de los de por defecto, así que se lee como una diferencia corta.

### API de bajo nivel

`postext/bundle` exporta también las piezas sobre las que se construyen `openBundle` y `createBundle`, para aplicaciones que guardan o sirven paquetes a su manera (un directorio descomprimido por HTTP, registros en una base de datos):

- **`openBundleZip(bytes)` / `zipBundle(files, { mtime })`**: la capa del zip. Al abrir tolera una carpeta raíz e ignora las entradas `__MACOSX` y los archivos ocultos. Se rechazan las rutas que se salen del paquete. `mtime` fecha los archivos como el dato del mismo nombre de `createBundle`.
- **`readBundle(manifest, readFile, options)`** lee un manifiesto más una función `readFile(ruta)` y devuelve capítulos, configuración, recursos, imágenes y fuentes. `options` fija el idioma, cómo se nombran los identificadores de archivo (`ids`), la configuración base (`baseConfig`, bajo la del manifiesto; por defecto la paleta y los tipos de recurso de `bundleBaseConfig` en el idioma del paquete, que devuelve `resolveBundleConfigLocale(manifest, locale)`, y quien pase su propia `baseConfig` debe traducirla a ese idioma; con un manifiesto anterior a `configVersion: 4`, su `math` se fija con el del paquete; con uno anterior a 5, el hueco en línea de su `layout`, con uno anterior a 6, el hueco en los recuadros de su `layout`, el sitio bajo los dos puntos de su `bodyText`, las marcas en línea de sus `headings` y el tamaño de sus letras capitales, con uno anterior a 7, los cortes tras una raya y la composición del texto en bandera de su `bodyText`, y con uno anterior a 8, la división bajo un encabezado de sus `headings` y los cortes tras el guion de un compuesto y el espacio bajo los contenedores `:::paragraphs` de su `bodyText`; ver [Paquetes escritos por postext 1.4 o anterior](https://postext.dev/es/docs/configuration-bundles.md#paquetes-escritos-por-postext-14-o-anterior)) y cómo se miden los tamaños intrínsecos. `readResolution` lee la resolución de cada mapa de bits en su archivo y la guarda en `bitmap.fileResolution`; por defecto es true cuando el paquete fija `layout.bitmapResolution: 'file'`.
- **`planBundle(meta, content)` / `resolveBundleFiles(plan, sources)`**: el lado de escritura, separado en un plan puro (nombres de archivo y manifiesto) y la resolución de los bytes mediante las funciones `readBlob` / `readFont`.
- **`isBundleManifest(value)`**, las funciones que eligen idioma (`pickChapterSpecs`, `pickLocaleOverrides`, `pickBundleView`, `resolveBundleLocale`, `resolveBundleConfigLocale`), `svgSize` / `bitmapSize` / `bitmapInfo` (los píxeles de un mapa de bits y la resolución que indica su archivo) y los tipos 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)` y `LEGACY_MATH_SIZE`** (0,5 ÷ 0,442): una configuración guardada, en los términos actuales (ver [Paquetes escritos por postext 1.4 o anterior](https://postext.dev/es/docs/configuration-bundles.md#paquetes-escritos-por-postext-14-o-anterior)). `readBundle` la aplica; una aplicación que guarda configuraciones a su manera también puede hacerlo, una vez por copia guardada.

El Sandbox está construido sobre estas piezas. Añade sus propios identificadores de almacenamiento y los registros de páginas de `layouts.json`.
