# Arquitectura: el pipeline de composición

> La pasada de colocación y sus rondas: cómo coloca el motor las líneas, los flotantes, las notas y las columnas, y cómo repite hasta que la composición se estabiliza

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

## En pocas palabras

Esta página es para programadores que quieren saber cómo coloca Postext cada cosa en la página. Postext recorre el texto de principio a fin y pone en su sitio cada línea, imagen, tabla, nota y recuadro. Si un paso posterior cambia algo anterior, como el número de página de una referencia, vuelve a empezar la pasada. Se detiene cuando ya nada se mueve. La página enumera cada paso, lo que decide y cuántas veces puede repetirse.

## Pipeline de composición

Una sola pasada de colocación, repetida tantas veces como necesite la composición. Ese es todo el pipeline de composición.

La pasada (`placeDocumentPass`, en `packages/postext/src/pipeline/build.ts`) recorre el contenido del primer bloque al último. Con cada bloque hace todo lo necesario para componerlo: medirlo al ancho de la columna en la que va a entrar, ver si cabe, partirlo o pasarlo a la columna siguiente según las reglas de corte y de cohesión, ofrecer un hueco a los flotados que cita y devolver el flujo a la rejilla base (ver [Sistema de ritmo vertical](https://postext.dev/es/docs/architecture.md#sistema-de-ritmo-vertical)). Los flotados, las viudas y la rejilla no tienen pasada propia: se resuelven a medida que cae cada bloque.

Algunas decisiones no se pueden tomar dentro de un solo recorrido, porque dependen de dónde acaba: los números de página que imprime un índice, la altura a la que una caja a ancho de página corta las columnas de encima, las líneas que aún le faltan a una columna corta. Para esas, `buildDocument` repite la pasada con *indicaciones* sacadas del resultado de la anterior. Las cinco etapas siguientes acompañan a un bloque a lo largo de la pasada; [El controlador de pasadas](https://postext.dev/es/docs/architecture-layout-pipeline.md#el-controlador-de-pasadas) y [Equilibrado de columnas](https://postext.dev/es/docs/architecture-layout-pipeline.md#equilibrado-de-columnas) tratan las repeticiones.

### Etapa 1: Estructuración del contenido

- **Entrada:** `PostextContent` sin procesar
- **Acción:** Analizar el markdown en bloques de contenido, resolver las referencias a recursos contra `resources[]` por ID, sacar del flujo las definiciones de notas al pie (`[^id]:`) y numerar sus llamadas en orden de cita, numerar los encabezados y planificar los flotados: qué recursos flotan y tras qué bloque (`notes[]` no se lee)
- **Salida:** Una lista plana de bloques de contenido (tipados y con contenido, pero sin asignación de página ni columna) y el plan de flotados
- **Se ejecuta:** al principio de cada pasada de colocación. El análisis se lee de su memo a partir de la segunda pasada; lo demás se calcula de nuevo

### Etapa 2: Medición de texto

- **Entrada:** Un bloque de contenido con texto y el ancho de la columna en la que va a entrar
- **Acción:** Medir las líneas del bloque a ese ancho a través del **módulo de medida** dedicado (`packages/postext/src/measure/`), que añade separación silábica, justificación, tramos enriquecidos en línea y división de líneas Knuth-Plass por encima de Pretext. El resultado son las `VDTLine[]` del bloque y su altura total
- **Detalle clave:** La medición se cachea. `cachedMeasureBlock` / `cachedMeasureRichBlock` (en `measure/cache.ts`) usan como clave el texto, las fuentes, el ancho y cada opción que afecta a la composición, de modo que re-medir un párrafo sin cambios es una consulta a un mapa
- **Salida:** Las líneas del bloque, con dimensiones precisas en píxeles
- **Se ejecuta:** cuando el recorrido llega al bloque, no antes, porque el ancho depende de dónde cae: una columna junto a una columna lateral, las líneas que corren junto a una figura rodeada de texto, las subcolumnas de un grupo `:::columns`. Un bloque que pasa a una columna de otro ancho se mide otra vez a ese ancho

El módulo se reparte limpiamente por responsabilidad: `plain.ts` mide tramos planos, `rich.ts` mide tramos mixtos de negrita/cursiva/matemáticas, `font.ts` construye las cadenas de fuente y gobierna el ciclo de vida de las cachés, y `canvas.ts` envuelve las primitivas crudas de anchura de texto del canvas. Un detalle de ciclo de vida importa en la práctica: las anchuras de glifo medidas contra una fuente de respaldo no deben sobrevivir a la llegada de la fuente real. `canvas.ts` guarda sus anchuras por cadena de fuente junto con una generación de medida; `evictFontFamilies` (al que llaman un build que descubre que han cambiado las caras de una familia, `watchFonts`, `prepareFonts` y `loadBundleFonts`) descarta las anchuras de esas familias y vacía la caché de Pretext, y una `MeasurementCache` llenada en una generación anterior descarta sus bloques en esas familias en su siguiente consulta. `clearMeasurementCache()` hace lo mismo con todas las familias. Las propias caras se cargan antes de componer en `fonts/prepare.ts` (`prepareFonts`), y `fonts/faces.ts` recupera las caras que usó un documento compuesto, para los avisos `fontFallback`.

Los párrafos en chino, japonés y coreano siguen otro camino dentro del mismo módulo. Un párrafo con más caracteres CJK que espacios entre palabras pasa al **compositor CJK** (`cjkCompose.ts`). Divide el texto en unidades (un carácter, un tramo de texto latino, una raya o unos puntos suspensivos de dos cuadratines, una caja indivisible como una etiqueta o una llamada de nota), mide cada unidad una sola vez y llena las líneas por orden, con las reglas de principio y final de línea de `cjkClasses.ts`: una línea admite un signo que no puede abrir la siguiente cediendo blanco de la puntuación (`cjkPunctuation.ts`) antes de bajar un carácter. Después, una línea justificada se ensancha entre sus caracteres. El resultado son `VDTLine` normales cuyos segmentos llevan lo que los renderizadores necesitan para pintarlos tal como se midieron: `tracking` (píxeles tras cada carácter, ya incluidos en el ancho del segmento), `inkOffset` para un signo que cedió blanco, `hangs`, los espacios entre chino y latín como segmentos `autospace`, y las marcas, las lecturas ruby y las filas warichu de las anotaciones chinas. En un documento japonés el mismo compositor sigue JLReq: las clases de kinsoku de los niveles japoneses se leen, en el momento del corte, de los caracteres de los extremos de cada unidad, los blancos de la puntuación se comprimen y se devuelven en el orden de JLReq, `rubyJis.ts` dispone los furigana (espaciado 1:2:1, desborde sobre kana, palabras jukugo) y las marcas de kanbun ensanchan la unidad de su carácter. El coste es lineal en la longitud del párrafo: el capítulo 1 de 紅樓夢 (6949 caracteres) se compone midiendo 1298 caracteres, una vez cada carácter distinto, donde medir prefijos del párrafo costaba 636 948. Véase [Composición china](https://postext.dev/es/docs/chinese-layout.md).

Por debajo, aquí es donde Pretext demuestra su valor. La llamada a `prepare()` es la parte costosa — analiza el texto usando el motor de fuentes del canvas y cachea el resultado. ¿Pero la llamada a `layout()`? Aritmética pura, prácticamente gratis. Esa división lo cambia todo. Una vez que el texto está preparado, el motor puede volver a partirlo con un coste insignificante: a otro ancho, para un párrafo que continúa en una columna más estrecha, o con una línea más, para un párrafo que el equilibrado de columnas compone corrido. Preparar una vez, componer tantas veces como haga falta.

```typescript
// Simplificado: cómo usa pretext el módulo de medida internamente
const prepared = prepare(paragraphText, '16px/1.5 Inter');
const { height } = layout(prepared, columnWidth, 24); // line-height de 24px
// => "Este párrafo mide 168px de alto a 320px de ancho de columna — son 7 líneas."
```

### Etapa 3: Colocación en páginas y columnas

- **Entrada:** Un bloque medido
- **Acción:** Componer el bloque en la columna actual. Crear nodos `VDTPage` y `VDTColumn` a medida que el flujo los necesita. Registrar `availableHeight` por columna. Cuando un bloque no cabe, partirlo (Etapa 5) o avanzar a la siguiente columna o página
- **Estrategia:** Colocación greedy de primer ajuste. Los saltos de columna y página siguen la asignación válida más simple
- **Salida:** El bloque tiene `pageIndex`, `columnIndex` y `bbox` asignados

Este es el momento en que el VDT se convierte en un documento de verdad. Antes de esta etapa, un bloque es una entrada de una lista plana, sin dirección. La colocación asigna cada uno a una página y columna, como verter agua en una cuadrícula de recipientes: llenar la columna 1 hasta que se desborde, pasar a la columna 2, y cuando la página esté llena empezar una nueva.

Antes de que aterrice ningún bloque de contenido en una página, la pasada reserva espacio para elementos estructurales — cabeceras y pies de página (compuestos como slots de diseño a partir de `config.header` / `config.footer`) y las bandas de flotados ya pendientes para la página recién abierta. Estas reservas reducen el `availableHeight` de cada columna, así que cuando los bloques de contenido empiezan a fluir, el motor ya sabe exactamente cuánto espacio hay disponible.

**Bandas de columnas y columnas a ancho de página.** `page.columns` es un array plano en orden de lectura, pero una página no siempre es una única fila de columnas. En una disposición multicolumna, un bloque en línea a ancho de página corta la página en *bandas* apiladas. Cuentan como tal cuatro cosas: un `:::callout` cuyo estilo o cuya valla fija `span: 'page'`, un grupo `:::columns{span="page"}` en el texto corrido, un `::resource` en línea cuya colocación se resuelve a `{ position: 'here', span: 'page' }` y una tira de cómic compuesta a lo ancho de la página allí donde está escrita (`:::strip{span=page}`). Los tres últimos se envuelven, antes de colocarlos, en una caja a ancho de página sin marco (`pipeline/flowColumns.ts`, `pipeline/spanEmbeds.ts`), de modo que un solo mecanismo compone los cuatro. En el corte, las columnas de texto de la banda actual se cierran en la línea de corte (su altura se recorta y `availableHeight` queda a cero), el bloque recibe su propia columna a todo el ancho con `kind: 'span'`, y debajo se añade una banda nueva de columnas de texto (`band + 1`, con la misma x y anchura y el mismo fondo que la banda a la que sustituye). Las columnas solo se añaden al final, así que `columnIndex` sigue apuntando a `page.columns[i]`, y los renderizadores no necesitan ningún trazado especial: cada columna recorta a su propio bbox (ensanchado por `columnClipRect`: 2pt para la tinta de los glifos más lo que sobresalgan por los lados de la columna sus diseños superpuestos, como la pestaña de un título o el distintivo de un recuadro, y lo que suba por encima de su parte alta el diseño de un encabezado; el pie sigue siendo el borde; el mismo rectángulo en los backends canvas y PDF), y el filete entre columnas se dibuja por banda entre columnas de texto adyacentes, empezando bajo la banda que ocupa un encabezado a ancho de página en la cabeza de la página (`columnRuleSegments`), con el filete propio de la página cuando una sección con estilo fija uno (`VDTPage.columnRule`, leído con `pageColumnRule`). El equilibrado de columnas ignora las columnas a ancho de página, las columnas laterales y las bandas de altura cero. Un bloque a ancho de página corta directamente donde la banda está nivelada (inicio de página, justo después de un encabezado de apertura, de otro bloque a ancho de página o de una banda de flotantes superior); si llega a una banda desigual propone en su lugar un *tope de banda* (`packages/postext/src/pipeline/bandCaps.ts`) — las columnas de la banda que abre un bloque de contenido dado se acortan a `ceil(Σ usado / N / rejilla)` líneas — y `buildDocument` repite la pasada de colocación con ese tope (agrandándolo línea a línea cuando la banda acortada desborda, hasta `MAX_BAND_CAP_RETRIES` = 3 veces, y descartándolo después, con lo que el bloque pasa a la cabeza de la página siguiente), de modo que el texto llena las columnas acortadas bajo todas las reglas de colocación, termina nivelado en el corte, y las columnas cerradas conservan la holgura que quede como `availableHeight` para que el equilibrado la absorba. El mismo mecanismo nivela la banda de *cierre* de cada capítulo y del documento (`headings.balancing.trailing`): una apertura de capítulo, un `:::part`, un aviso `placement: 'fixed'` que cierra el capítulo o el final del documento alcanzado con las columnas de la banda actual desiguales propone un tope `kind: 'trailing'`, indexado por el bloque límite; como un tope se indexa por el bloque que abre su banda — y ese bloque se mueve cada vez que una página anterior absorbe líneas de equilibrado —, los topes de cierre se resuelven *después* de que el equilibrado de columnas se haya asentado, con sus ajustes congelados, y una breve ronda de pulido deja que las palancas rellenen lo que el corte dejó corto. Los avisos `placement: 'fixed'` salen del flujo: la caja se ancla al área de contenido / caja de corte / caja de sangre de la página, las columnas de texto que cubre ceden esa zona (recortada por abajo o por arriba como una banda de flotantes, pasando a la página siguiente si hay conflicto), y la caja con sus hijos va a `page.floats`.

**Páginas verticales.** Con `layout.writingMode: 'vertical-rl'` la pasada compone una página horizontal girada un cuarto de vuelta en el sentido de las agujas del reloj. En esa página, la caja de texto, las columnas, los bloques, las líneas, los flotantes y las zonas de notas al pie están en **coordenadas de flujo**, y `VDTPage.flow` lleva el giro que las lleva al pliego: un punto (x, y) del flujo cae en (ancho de página − y, x). Nada de lo que viene después necesita saberlo: los cortes, los flotantes, las reglas de cohesión y el equilibrado funcionan en el marco del flujo como en cualquier página, una columna del flujo es un piso en el pliego, y cada backend aplica el giro al pintar (`flowToPage` y `pageToFlow` convierten puntos en los dos sentidos). Las cabeceras, los folios, las marcas de corte y el fondo siguen en coordenadas del pliego. Las figuras y las tablas se componen como bloques derechos girados de vuelta dentro del marco, y los caracteres que van de pie se giran de vuelta uno a uno al pintar.

### Etapa 4: Colocación de recursos

- **Entrada:** El plan de flotados de la Etapa 1 y las páginas tal como están colocadas hasta ese momento
- **Acción:** Flotar cada recurso referenciado al primer hueco libre tras su primera referencia — el final de la columna de la referencia, el principio / el final de la siguiente columna vacía, o una banda de la página siguiente (`packages/postext/src/pipeline/floatPlacement.ts` planifica los flotados, `pipeline/floatSlots.ts` enumera y mide los huecos; la pasada de colocación reserva las bandas a medida que avanza)
- **Se ejecuta:** en cuanto ha caído el bloque que contiene la primera referencia, dentro del mismo recorrido; el texto que se coloca después fluye junto a la banda
- **Resolución de la colocación:** Por recurso, el motor resuelve `resource.placement` → el `resourceType.defaultPlacement` de su tipo → el valor por defecto integrado `{ position: 'auto', span: 'column' }`

| Campo de colocación | Comportamiento |
| --- | --- |
| `position: 'auto'` | El recurso toma el primer hueco libre tras su referencia, arriba o abajo — el valor por defecto |
| `position: 'top'` | Solo huecos superiores: una banda al principio de la siguiente columna o página vacía, empujando el contenido de la columna por debajo de él |
| `position: 'bottom'` | Solo huecos inferiores: una banda al pie de una columna o de la página, acortando la columna por encima de él |
| `position: 'here'` | Renuncia al flotado: el recurso se embebe en línea en su directiva `::resource`, exactamente donde aparece en el flujo |
| `span: 'column'` | La banda ocupa una sola columna (el motor elige la columna con más espacio libre) |
| `span: 'page'` | La banda abarca todo el ancho de contenido a través de todas las columnas, interrumpiendo el flujo — las bandas a ancho completo se reservan primero, de modo que los flotados de columna anidan en el espacio restante |
| `span: 'side'` | El recurso se compone en la columna lateral de una disposición de columna y media que reserva esa columna a los flotados (`layout.sideColumnRole: 'floats'`), apilado junto al párrafo que lo cita por primera vez. En una página sin esa columna se comporta como `'column'` |

`ResourcePlacement` tiene más campos que estos dos: `rotate`, `width`, `align`, `columns`, `captionSide`, `shrink`, `minScale`, `captionMeasure`, `wrap`, `wrapGap` y `citingPage`. [Configuración → Tipos de recurso](https://postext.dev/es/docs/configuration-resources.md#tipos-de-recurso) y [Formato del documento › Colocación](https://postext.dev/es/docs/document-format-resources.md#colocación) explican qué hace cada uno.

- **Colocación diferida:** Un flotado que no cabe en ningún hueco de la página actual espera a la siguiente página que abra el flujo y, en un límite de capítulo, se vuelca en páginas abiertas por delante del límite. Una imagen conserva su tamaño salvo que `placement.shrink` (o `layout.floatShrink`, para todo el documento) permita reducirla al sitio que tiene un hueco. Una tabla demasiado alta para una página nueva se corta entre filas y continúa en las páginas siguientes, con sus filas de cabecera repetidas (`tableStyle.overflow: 'split'`, el valor por defecto)
- **Salida:** Recursos posicionados en bandas de página (`page.floats`), con las alturas de las columnas afectadas reducidas para que el texto fluya alrededor de las bandas

La colocación de recursos es donde las cosas se ponen interesantes, porque los flotados no solo ocupan espacio — remodelan el espacio a su alrededor. Cuando se abre una página, los flotados pendientes reclaman primero sus bandas, y las columnas se encogen para encajar entre ellas. El texto fluye después por las columnas estrechadas sin interrupción; el lector ve la figura en la parte superior o inferior de la página, cerca del punto donde se menciona en el texto (aunque no exactamente en él). Esto es práctica estándar en la composición tipográfica profesional; los libros lo hacen constantemente.

**Reglas de colocación.** Más allá del despacho de la colocación, los flotados siguen restricciones editoriales estrictas:

- **Regla de post-referencia.** Un flotado aterriza en el primer hueco libre *después* de su primera referencia en el texto, nunca antes. El lector encuentra la referencia primero y luego ve el recurso. Si un flotado no cabe, se difiere hacia adelante a un hueco o página posterior, nunca hacia atrás.
- **Orden de referencia dentro de cada secuencia.** Los flotados pendientes se ofrecen a cada hueco en orden de primera referencia, y uno que no cabe en ninguna parte retiene a los que vienen detrás en su secuencia de numeración: la tabla 3 nunca aterriza después de la tabla 4, la figura 12 nunca antes que la figura 11. Las secuencias no se retienen entre sí — una tabla en espera deja pasar a una figura posterior. Para que una tabla larga no tenga que esperar a una página nueva, la que recibe la cabecera de una columna vacía se corta a esa columna y continúa en el siguiente hueco (la columna de al lado, o las bandas de la página siguiente), con sus filas de cabecera repetidas.
- **Barrera de capítulo.** Los flotados nunca escapan de su capítulo. En una apertura de capítulo (un nivel de encabezado con `breakBefore` o `span: 'page'`), un `:::part`, un estilo de aviso con `floatBarrier: true` y el final del documento, todo flotado pendiente se coloca primero — en los huecos libres de la página, y después en páginas abiertas por delante del límite (cada una fuerza al menos un flotado) — antes del salto de página propio del límite. Antes de abrir esa página, a una figura o tabla aún pendiente se le ofrecen otra vez los huecos libres de la página actual sea cual sea su `position`: un flotado de cabeza de página citado en la página de cierre de un capítulo ocupa el pie de esa página, bajo las columnas equilibradas, en lugar de una página propia (los avisos flotantes conservan su colocación). Un `:::pagebreak` envía los flotados pendientes a la página que le sigue, tras el relleno de paridad.
- **Espacio mínimo de texto.** En una página recién abierta, una banda solo se reserva si todavía caben al menos 3 líneas de cuerpo de texto en las columnas afectadas — con una excepción: un flotado sobredimensionado puede colocarse a la fuerza en una banda que aún es todo texto, para que una figura dominante no pueda atascar la cola indefinidamente. Un hueco de la página actual debe caber en la altura restante de la columna; la regla de las 3 líneas solo se aplica ahí junto a otra banda de flotantes.
- **Espacio de respiración.** Un hueco de una altura de línea de cuerpo separa cada banda del texto que la acompaña.
- **Alineación con la rejilla base.** Las bandas superiores se redondean *hacia arriba* a un múltiplo de la rejilla base (agrandando el hueco bajo el flotado), de modo que cada línea desplazada sigue cayendo en la rejilla. Los flotados inferiores se anclan de forma que la última línea base del pie quede sobre la rejilla — el pie comparte su línea base con la última línea de texto de las columnas vecinas, y las páginas terminan a la misma altura entre columnas y entre páginas enfrentadas.

**Numeración tipada por primera referencia.** Los recursos *no* se numeran en el markdown. `pipeline/resourceNumbering.ts` asigna a cada recurso su número la primera vez que se referencia en orden de lectura, usando su `ResourceType`: la `numberingTemplate` combina el contador por tipo `{n}` con los contadores de encabezado `{h1}`..`{h6}` vigentes en la referencia (p. ej. `'{h1}.{n}'` → "1.7"), `resetOn` controla cuándo se reinicia el contador (`'never'` o en cualquier nivel de encabezado), y `counterFormat` elige numerales decimales, romanos o alfabéticos. Los tipos integrados *Figura* y *Tabla* provienen de `defaultResourceTypes(locale)`, localizados al idioma del documento. Como la numeración sigue el orden de primera referencia, insertar una figura nueva a mitad del documento renumera automáticamente todo lo posterior — sin editar el contenido original.

### Etapa 5: Cortes y reglas de cohesión

Esta es la etapa que separa un motor de composición de un volcador de texto. Aplica las reglas de calidad editorial que los tipógrafos profesionales han aplicado a mano durante siglos — y que el renderizado de texto ingenuo ignora por completo.

Tres mecanismos se reparten el trabajo, y cada uno actúa allí donde existe la información que necesita: una **penalización de línea corta** dentro del divisor de líneas, una **partición puntuada** donde un párrafo se encuentra con el final de una columna, y **reglas estructurales de cohesión** entre bloques.

#### Viudas, huérfanas y líneas cortas

Las **viudas y huérfanas** son los signos más visibles de una composición amateur:

- Una **viuda** es una sola línea de un párrafo que queda aislada al final de una columna. El párrafo
  continúa en la columna siguiente, pero esa línea solitaria parece abandonada (como si la columna
  terminara prematuramente).
- Una **huérfana** es una sola línea de un párrafo que queda varada al inicio de una columna. El
  grueso del párrafo está en la columna anterior, pero una línea se desbordó (parece desconectada
  de su contexto).
- Una **línea corta** (*runt*, en inglés) es la última línea de un párrafo cuando se queda en una sola palabra corta (o dos): visualmente demasiado corta para sentirse como una línea de texto en condiciones. Menos grave estructuralmente que una viuda, pero igual de molesta para un lector atento.

**Las líneas cortas se resuelven en el divisor de líneas.** Una línea corta es cosa del párrafo y de nada más: su última línea es igual de corta caiga donde caiga el párrafo. Por eso es la única de las tres reglas que ve el divisor de líneas de Knuth-Plass. Cuando un conjunto de cortes terminaría el párrafo en una línea cuyo contenido es más estrecho que `runtMinCharacters × normalSpaceWidth` (valor por defecto de `runtMinCharacters`: 20, es decir, un contenido tan ancho como veinte espacios entre palabras, de 8 a 12 letras más o menos), el divisor suma `runtPenalty` (valor por defecto 1000) al *badness* de esa línea dentro de la fórmula cuadrática de demerits, y así compite en la misma escala que el *badness* de línea (que satura en 10000) en vez de quedar aplastado por él. La penalización convive con los demerits habituales (*badness*, coste de separación silábica, desajuste de clase de *fitness*) en una única optimización global, y el divisor encuentra casi siempre un conjunto de cortes sin línea corta. Cuando no lo encuentra, `bodyText.tightenRunts` compone el párrafo con una línea menos si sus espacios entre palabras, y un poco de tracking negativo, pueden absorber la diferencia. El módulo de medida entrega la penalización al divisor (`measure/plain.ts`, `measure/rich.ts`) y `knuthPlass/breakpoints.ts` la aplica.

**Las viudas y las huérfanas se resuelven al final de la columna.** El divisor de líneas parte un párrafo para un ancho. No sabe en qué lugar de la página quedará el párrafo, así que no puede saber cuáles de sus líneas cerrarán una columna. La pasada de colocación sí lo sabe. Cuando las líneas de un párrafo no caben todas en lo que queda de columna, `chooseParagraphSplit` (en `pipeline/orphanWidow.ts`) prueba todas las particiones `k` de las líneas ya cortadas, de 0 al número de líneas que caben: `k` líneas se quedan y el resto abre la columna siguiente. Cada candidata recibe una puntuación:

- `slackWeight · slack²` por las líneas que deja vacías al pie de la columna, siendo `slack` las líneas que caben menos `k` (valor por defecto de `slackWeight`: 10);
- más `widowPenalty` (valor por defecto 1000) cuando deja menos de `widowMinLines` líneas al final de la columna;
- más `orphanPenalty` (valor por defecto 1000) cuando lleva menos de `orphanMinLines` líneas al inicio de la siguiente.

Gana la partición con la puntuación más baja, y `k = 0` pasa el párrafo entero. Las reglas son blandas, y los números dicen cuánto: una penalización de 1000 es el coste de *slack* de diez líneas vacías (10 · 10² = 1000), así que con los valores por defecto una partición que cumple las dos reglas gana siempre que deje vacías menos de diez líneas de la columna. Más allá, una viuda o una huérfana puede salir más barata. La partición elige entre las líneas que el divisor de líneas ya hizo; no vuelve a cortar el párrafo. Los ítems de lista pasan por la misma partición mediante `avoidOrphansInLists` y `avoidWidowsInLists`, y reciben la penalización de línea corta mediante `avoidRuntsInLists` (todos a `true` por defecto).

Todo esto se puede afinar en `BodyTextConfig` — véase [Configuración → Huérfanas, viudas, *runts* y cohesión](https://postext.dev/es/docs/configuration-page-flow.md#huérfanas-viudas-runts-y-reglas-de-cohesión). Ajustar cualquier `*Penalty` a `0` desactiva efectivamente esa regla.

#### Reglas estructurales de cohesión (keep-together)

Algunas agrupaciones son mayores que un solo párrafo — abarcan bloques adyacentes y no pueden resolverse solo con la división de líneas. La pasada de colocación las aplica a medida que cae cada bloque, moviendo grupos enteros hacia adelante cuando de otro modo se partirían en un salto de columna o de página:

- **Título con su primer párrafo.** Un título nunca debe aparecer al final de una columna si el párrafo que introduce empezaría en la columna siguiente. Lo gestiona `headings.keepWithNext` (por defecto `true`): si no hay sitio para el título *más* el mínimo de viudas del cuerpo (`bodyText.widowMinLines`, por defecto `2`) del bloque siguiente — o solo una línea cuando `avoidWidows` está desactivado —, el título se empuja hacia adelante para viajar con su texto.
- **Títulos consecutivos.** Cuando aparecen varios títulos en secuencia (por ejemplo, un h2 seguido de un h3 seguido de un párrafo), todo el grupo debe permanecer junto. Ninguno de los títulos puede quedar suelto al final de una columna sin el contenido que introducen.
- **Listas introducidas con dos puntos.** Cuando un párrafo termina con dos puntos que introducen directamente una lista, la línea que lleva los dos puntos debe quedarse con el inicio de la lista. Lo gestiona `bodyText.keepColonWithList` (por defecto `true`): si colocar el párrafo no dejaría sitio para que empiece el primer ítem de la lista (todo él cuando las reglas de huérfanas y viudas de las listas lo mantienen entero; una línea con `bodyText.colonListRoom: 'line'`, como hasta postext 1.4), la última línea con los dos puntos (o el párrafo entero, si es de una sola línea) se mueve adelante junto con la lista. Siempre que esta regla tenga que empujar el párrafo completo y justo antes haya una secuencia de títulos en la columna, esos títulos también se arrastran hacia adelante para no violar silenciosamente `keepWithNext`; la única excepción es cuando la columna contiene solo el título (o títulos) que un movimiento anterior ya adelantó, en cuyo caso el motor deja el párrafo junto al título y acepta la separación más leve entre dos puntos y lista para evitar un bucle.
- **Figura con su pie.** Una figura y su pie de figura son una unidad inseparable. Siempre se mueven juntos.

Cuando se detecta una violación de cohesión, el motor empuja todo el grupo a la siguiente columna o página. La columna que deja termina entonces corta, en las líneas que habría ocupado el grupo. Rellenarla es tarea del [Equilibrado de columnas](https://postext.dev/es/docs/architecture-layout-pipeline.md#equilibrado-de-columnas).

Estas correcciones son invisibles cuando se hacen bien (un lector nunca debería notarlas). Pero su *ausencia* salta a la vista de cualquiera que lea con atención: esa línea incómoda al inicio de una columna, esos huecos desiguales donde el motor renunció a intentar ajustar el texto. Las editoriales profesionales tienen guías de estilo enteras dedicadas a prevenir exactamente estos problemas. Postext los automatiza.

### El controlador de pasadas

> **Figura: La pasada de colocación y sus repeticiones**
> Una pasada de colocación lleva el contenido por cinco etapas: estructurar, medir, colocar, flotados, y cortes y reglas de cohesión. buildDocument repite la pasada entera con indicaciones de la anterior: un tope de banda, palancas de equilibrado, un corte de la banda de cierre, etiquetas de página o números de nota ya asentados.
>
> *Una pasada coloca el documento entero. buildDocument la repite con indicaciones hasta que cada ronda se asienta o llega a su límite.*

`buildDocument` es un controlador alrededor de la pasada de colocación (`buildDocumentGen` y `buildDocumentBalanced`, en `pipeline/build.ts`). Ejecuta la pasada una vez. Después lee lo que la pasada ha notificado y, donde una decisión necesita una segunda mirada, repite la pasada, entera, con indicaciones (`PassHints`): los topes, las líneas adicionales y los números que la pasada siguiente debe aplicar. Cuando no se da ninguna de las condiciones de la tabla de abajo, esa primera pasada es toda la composición.

Las repeticiones van por rondas, cada una para un tipo de decisión, en este orden:

| Ronda | Se ejecuta cuando | Qué se indica a la pasada siguiente | Límite |
| --- | --- | --- | --- |
| **1. Índice** | El documento imprime números de página que el anfitrión no ha proporcionado: un `:::toc` o un `:::index` sin `outline` en el contenido, o una referencia que imprime la página de un ancla | El esquema con las etiquetas de página de la composición anterior | `MAX_TOC_ROUNDS`: 3 composiciones además de la primera |
| **2. Números de nota** | `footnotes.numbering` es `'page'`, `'column'` o `'spread'` y las notas van al pie de la columna o del pliego, de modo que el número de una nota depende de dónde la ponga la composición | El número que recibió cada nota en la composición anterior (`footnoteNumbers`) | `MAX_FOOTNOTE_ROUNDS`: 3 composiciones además de la primera |
| **3. Pies laterales** | Una caja lateral o un flotado lateral se montó sobre el pie que una figura compuso en la columna lateral (`placement.captionSide`) | Esos pies van debajo de sus figuras (`captionUnder`) | 3 pasadas |
| **4. Flotados en la página de la cita** | Un flotado con `placement.citingPage` cayó en una página o columna posterior a la línea que lo cita | Esa página, o columna, se abre con el flotado en su cabeza (`citingPageFloats`) | `MAX_CITING_PAGE_ROUNDS`: 3 rondas de propuestas, y después una pasada por cada tanda de flotados que una pasada no pudo mantener ahí |
| **5. Topes de banda** | Un bloque a ancho de página llegó a una banda con las columnas desiguales (ver [Etapa 3](https://postext.dev/es/docs/architecture-layout-pipeline.md#etapa-3-colocación-en-páginas-y-columnas)) | La altura a la que cortar esa banda (`bandCaps`) | `MAX_BAND_PASSES`: 4 pasadas de propuestas y reintentos. Un tope crece una línea hasta 3 veces (`MAX_BAND_CAP_RETRIES`) y después se descarta |
| **6. Equilibrado de columnas** | `headings.balancing.enabled` está activado y una columna equilibrable termina corta | Las líneas de rejilla adicionales de cada bloque y los párrafos que hay que componer con una línea más (`balanceExtraPx`, `balanceLooseness`) | `MAX_BALANCING_PASSES`: 10 intentos por segmento. `MAX_BALANCING_PASSES_PER_DOCUMENT`: 30 pasadas por documento |
| **7. Bandas de cierre** | El equilibrado está activado con `headings.balancing.trailing`, y un capítulo o el documento termina en una banda con las columnas desiguales | Un tope de cierre para esa banda, con las indicaciones del equilibrado congeladas | `MAX_BAND_PASSES`: 4 pasadas de propuestas y reintentos, cada tope con sus 3 reintentos |
| **8. Pulido** | La ronda 7 conservó un tope de cierre | Equilibrado de columnas una vez más, sobre la composición con el tope, con segmentos nuevos | 10 intentos por segmento y 30 pasadas, contados desde cero |

Las rondas 3 a 8 se ejecutan una tras otra dentro de una misma composición (`buildDocumentBalanced`). Las rondas 1 y 2 quedan fuera: una ronda de índice o de números de nota es una composición entera, con sus propias rondas 3 a 8. No hay un límite para la composición en su conjunto, solo estos.

**Cada repetición es una colocación completa.** Una indicación no parchea el árbol de la pasada anterior. La pasada coloca el documento de nuevo con la indicación en vigor, de modo que un encabezado con dos líneas más encima, o una banda cortada seis líneas más arriba, pasa por las mismas reglas de corte y de cohesión que todo lo demás. El controlador comprueba después el resultado: un tope de banda cuya caja no cayó en el corte se agranda o se descarta, y una pasada de equilibrado que no mejoró un segmento se descarta para ese segmento. Lo que devuelve el controlador está hecho de páginas que colocó una pasada: una pasada entera o, tras el equilibrado de columnas, las páginas de cada segmento tomadas de la mejor pasada que tuvo ese segmento.

**Lo que dice el documento.** `doc.iterationCount` cuenta las pasadas de colocación que hay detrás de la composición devuelta: la primera y las de las rondas de topes de banda, de equilibrado y de bandas de cierre. Las pasadas de pies laterales y de página de la cita no entran en la cuenta, ni tampoco las composiciones anteriores de una ronda de índice o de números de nota; a `BuildDocumentOptions.onPass` se le llama en cada pasada, con lo que ha tardado. `doc.converged` habla solo del equilibrado de columnas. Es `true` cuando todos los segmentos terminaron sin ninguna línea vacía en sus columnas equilibrables o sin ninguna palanca que proponer, y `false` cuando un segmento se detuvo en una pasada que no lo mejoró, o agotó sus intentos, con un hueco todavía abierto. Con el equilibrado desactivado es `true`. Un documento con `converged: false` está completo: contiene la mejor composición que encontró el controlador, y alguna de sus columnas termina corta.

### Equilibrado de columnas

- **Se ejecuta:** como rondas 6 y 8 del controlador (activado por defecto; `headings.balancing.enabled: false` lo desactiva, y está desactivado por defecto en texto vertical y en una retícula de caracteres, ver [Configuración → Equilibrado de columnas](https://postext.dev/es/docs/configuration-page-flow.md#equilibrado-de-columnas))
- **Entrada:** La mejor composición hasta el momento y las líneas de rejilla vacías al pie de cada columna equilibrable (`collectColumnGaps`, en `pipeline/columnBalancing.ts`)
- **Acción:** Proponer palancas que rellenan cada columna corta en su sitio, ejecutar la pasada de colocación con ellas y conservar el resultado para un segmento cuando su puntuación de huecos ha bajado
- **Salida:** Columnas que terminan en la última línea de rejilla de la página, con cada palanca anotada en el bloque al que se aplicó (`VDTBlock.balancing`)

Las columnas desequilibradas se notan inmediatamente. Las reglas de corte las dejan constantemente: un encabezado que pasó a la columna siguiente para quedarse con su texto, un párrafo que pasó entero para evitar una viuda, una figura que no cabía. Una columna que se detiene dos líneas por encima de la de al lado parece inacabada — como si la maquetación se hubiera rendido a mitad de camino.

Ningún bloque cambia de columna para arreglarlo. Un salto de columna está ligado al elemento que no cabía: el bloque que abre la columna siguiente era demasiado alto para el sitio que quedaba, y lo sigue siendo para un sitio menor. Así que una columna corta se puede rellenar desde dentro, como mucho en lo que mide su hueco, y nada cruza a la columna siguiente. El ajuste es local, que es lo que hace a mano un maquetista: añadir una línea de espacio allí donde el ojo la acepta. Una columna es equilibrable cuando el contenido siguió más allá de ella hacia una columna posterior de la misma página o, en el caso de la última columna de una página, cuando la página fluye hacia la siguiente. La última página de un capítulo puede terminar corta; su banda de cierre la corta a nivel un tope de cierre (ver [Etapa 3](https://postext.dev/es/docs/architecture-layout-pipeline.md#etapa-3-colocación-en-páginas-y-columnas)).

Para cada hueco, las palancas se proponen en orden de prioridad editorial, el mismo orden en que las da [Configuración → Equilibrado de columnas](https://postext.dev/es/docs/configuration-page-flow.md#equilibrado-de-columnas):

1. **Una caja que cierra la columna** toma como espacio superior el sitio que queda bajo su pie, de modo que su pie cae en la última línea de rejilla de la página, a nivel con la última línea de la columna contigua. Se anota como `trailingCallout`.
2. **Una imagen con zona segura** (`Resource.safeArea`), colocada en línea en la columna o flotada en su cabeza o su pie y solo sobre ella, crece en líneas enteras de rejilla, recortada dentro de su zona segura. Una imagen más alta no deja ningún hueco en la página, así que actúa antes de añadir ningún espacio. Se anota como `flexFigure`.
3. **Los encabezados** toman líneas enteras de rejilla por encima, de una en una y por turnos, empezando por el encabezado más importante (el de nivel más bajo), que se lleva así la mayor parte. Un encabezado al principio de una columna no toma ninguna, de modo que las columnas siguen empezando en la parte superior de la página. Un encabezado solo toma así sus primeras líneas antes de probar las palancas siguientes, y las demás, hasta su máximo, cuando estas ya no pueden dar más. Se anota como `heading`.
4. **Los finales de lista** toman una línea de rejilla donde termina una lista. Se anota como `listEnd`.
5. **Bajo fórmulas, cajas y figuras** va una línea de rejilla: bajo una fórmula destacada o una caja tras la que sigue el texto (`afterDisplay`), y entre una figura o tabla que encabeza la columna y el texto de debajo (`afterFloat`).
6. **Los párrafos corridos** son el último recurso. Los párrafos de la columna, los más largos primero, se recomponen cada uno con una línea más (el `\looseness=+1` de TeX, a través de la opción `looseness` del divisor de Knuth-Plass), y la composición más larga solo se acepta mientras todas las líneas se mantienen dentro de `bodyText.maxWordSpacing`. Cuando el espaciado entre palabras no basta para ganar la línea, el párrafo puede tomar además el menor tracking positivo que lo consiga, hasta un máximo. Se anota como `looseParagraph`.

Los máximos, los interruptores y las excepciones (una retícula de caracteres, las páginas de cierre, un flotado sobre varias columnas) son claves de `headings.balancing`. La página de Configuración las enumera y se mantiene al día con ellas; esta página no las repite.

Una propuesta se comprueba colocándola. La pasada que aplica las palancas es una colocación completa, y de vez en cuando una palanca sí acaba moviendo contenido a través de un salto de columna: un párrafo partido cuya cabeza ya no cabe, un flotado que perdió su hueco. Las páginas posteriores a ese punto fluyen de otra manera y se abren huecos en otros sitios. El controlador lo ve en la puntuación, descarta la pasada para ese segmento y veta las palancas recién aplicadas en la primera columna que cambió, de modo que la propuesta siguiente conserva las palancas que funcionaron y deja fuera la que provocó la cascada.

**El equilibrado converge por segmentos.** Las páginas entre saltos explícitos — la apertura de un capítulo, un `:::pagebreak` — se maquetan con independencia unas de otras: nada fluye a través de un salto así, de modo que una palanca de equilibrado dentro de una tanda de páginas nunca puede mover una línea de otra. El bucle de equilibrado de columnas juzga por eso cada *segmento* por separado. Su puntuación de huecos es el número de líneas de rejilla vacías al pie de sus columnas equilibrables. Una pasada sigue colocando el documento entero, pero cada segmento conserva o rechaza su parte de las palancas según su propia puntuación de huecos, veta sus propias cascadas, se estanca por su cuenta y gasta su propio presupuesto de intentos; un segmento cuyo reintento empeoró recupera sus páginas de la mejor pasada que tenía mientras los demás siguen avanzando. Un libro de treinta capítulos se equilibra así exactamente igual que sus capítulos uno a uno — el PDF del libro completo y el PDF del capítulo suelto son idénticos — en lugar de que una cascada en cualquier parte cueste una pasada a todas las páginas del libro.

Los límites son una válvula de seguridad. Algunas columnas no se pueden cerrar: una que solo contiene párrafos cortos no tiene palanca de la que tirar, y una cuyas palancas empujan cada una un bloque al otro lado del salto de columna no tiene ninguna que se pueda conservar. El motor se queda con la mejor composición que encontró y deja corta esa columna. Como las palancas quedan anotadas en los bloques, una hoja de pruebas o un test puede enumerar las columnas que nada cerró (ver [Configuración → Qué palanca actuó](https://postext.dev/es/docs/configuration-page-flow.md#qué-palanca-actuó)).
