Capítulo 4 · Parte II · El oficio
Separación silábica y justificación
Cómo corta Postext las líneas, cómo justifica el texto y cómo controla el espacio entre palabras con el algoritmo de Knuth-Plass.
En pocas palabras
Esta página explica cómo reparte Postext las palabras a lo largo de cada línea. Cuando los dos bordes de un párrafo van rectos, los espacios entre palabras pueden abrirse demasiado y quedar feos. Postext mira el párrafo entero antes de decidir dónde acaba cada línea, y así los espacios quedan parejos. También sabe partir las palabras largas con un guion en el sitio correcto. Puedes fijar cuánto pueden abrirse los espacios y marcar las líneas que aún quedan flojas. La página enseña además cómo consigue Postext que las columnas acaben a la misma altura.
La diferencia entre una composición de aficionado y una profesional está en los espacios entre palabras.
Abre cualquier novela de bolsillo. El texto está justificado: los dos bordes de todos los párrafos están perfectamente alineados. Pero fíjate bien. Los espacios entre palabras son casi uniformes, línea tras línea. No hay ríos de blanco que bajen por la página. No hay líneas con dos palabras perdidas a ambos lados de un océano de blanco. Conseguirlo es mucho más difícil de lo que parece, y es el problema que más esfuerzo de ingeniería tipográfica ha consumido.
Postext lo resuelve con el mismo algoritmo que usa TeX desde 1981: el corte óptimo de líneas de Knuth-Plass. Con patrones de separación silábica de la calidad de TeX, límites configurables para el espacio entre palabras y un sistema visual de depuración para localizar las líneas problemáticas, el motor produce texto justificado con calidad de edición profesional.
#El problema
Cuando el texto se compone con textAlign: 'justify', todas las líneas salvo la última deben estirarse o comprimirse para llenar exactamente el ancho de la columna. El motor reparte entre los espacios de cada línea la diferencia entre el ancho natural de su contenido y el ancho de la columna. (La última línea se deja en bandera, con su ancho natural, salvo una excepción que se explica en La última línea).
Si una línea tiene muchas palabras, cada espacio absorbe un ajuste mínimo, invisible para el lector. Pero si tiene pocas (porque una palabra larga ha obligado a cortar antes de tiempo), cada espacio tiene que estirarse muchísimo. El resultado es una línea floja: una línea con un espacio entre palabras tan grande que rompe el ritmo de lectura y deja huecos feos.
También existe el problema contrario. Si el motor mete demasiadas palabras en una línea, los espacios se comprimen por debajo de su ancho natural y la línea queda apretada, con las palabras amontonadas.
Un algoritmo de corte de líneas ingenuo, como el que usa CSS, decide línea a línea: llena la línea en curso con todas las palabras que caben, corta y sigue. Este método voraz (cada línea se corta en el primer punto que sirve) tiene un punto débil fundamental: no ve el futuro. Una decisión que parece óptima para la línea 5 puede obligar a cortar la línea 6 de una forma pésima. Cuando el algoritmo llega a la línea 6, ya es tarde: la línea 5 está decidida.
#Knuth-Plass: ver el párrafo completo
El algoritmo de Knuth-Plass, publicado por Donald Knuth y Michael Plass en 1981, adopta un enfoque radicalmente distinto. En lugar de cortar una línea cada vez, considera todas las formas posibles de cortar el párrafo entero y elige la combinación que minimiza la «fealdad» total de todas las líneas. Es el algoritmo en el que se basa TeX, y la razón por la que los documentos compuestos con TeX son la referencia del texto justificado desde hace más de cuatro décadas.
#Cajas, pegamento y penalizaciones
Knuth-Plass no piensa en palabras y espacios: modela el texto como una secuencia de tres elementos básicos:
| Primitiva | Representa | Comportamiento |
|---|---|---|
| Caja (Box) | Una palabra o fragmento de texto | Tiene un ancho fijo. No se puede estirar, comprimir ni partir. |
| Pegamento (glue) | Espacio entre palabras | Tiene un ancho natural, una capacidad de estiramiento y una capacidad de compresión. El motor puede ajustar el pegamento dentro de esos límites para llenar la línea. |
| Penalización (Penalty) | Un posible punto de corte | Tiene un coste: una penalización baja es un corte barato, y una alta, un corte caro. Una penalización marcada (flagged) indica un punto de separación silábica (si se usa, añade un guion visible). |
Un párrafo se convierte en una secuencia como:
[caja "El"] [cola] [caja "rápido"] [cola] [caja "zorro"] [cola] [caja "marrón"]
[penalización -∞] ← corte forzado al final del párrafo
Cuando la separación silábica está activada, las palabras largas se dividen en fragmentos separados por penalizaciones:
[caja "ti"] [penalización 50, marcada] [caja "po"] [penalización 50, marcada] [caja "grafía"]
Cada penalización marcada tiene un coste de 50: no es gratis, pero sale más barato que una línea floja.
#Cómo encuentra el óptimo
El algoritmo usa programación dinámica. Mantiene un conjunto de nodos activos (posibles puntos de corte en los que podría empezar una línea nueva) y evalúa todos los cortes viables desde cada nodo activo. Para cada corte candidato, calcula:
-
Proporción de ajuste (r): cuánto tiene que estirarse o comprimirse el pegamento de la línea.
r = 0significa que la línea encaja perfectamente;r > 0, que se estira (floja), yr < 0, que se comprime (apretada). -
Fealdad (badness): una medida de lo desigual que es el espaciado, que se calcula como
100 × |r|³. Al crecer con el cubo, una línea un poco floja es tolerable, pero una muy floja se penaliza mucho. Una línea conr = 2tiene una fealdad de 800; una conr = 0.5, de 12. -
Clase de ajuste (fitness class): cada línea se clasifica como apretada (
r < -0.5), normal (-0.5 ≤ r < 0.5), floja (0.5 ≤ r < 1.0) o muy floja (r ≥ 1.0). Si una línea apretada está junto a una muy floja, el contraste resulta chocante, así que se penalizan las líneas contiguas cuyas clases distan más de un paso. -
Deméritos (demerits): el coste total de cortar en ese punto. Como en TeX, la fealdad y la penalización del punto de corte se combinan en una fórmula cuadrática:
deméritos = (1 + fealdad + penalización)²cuando la penalización no es negativa; una penalización negativa (un corte deseable) resta, en cambio, su cuadrado:(1 + fealdad)² − penalización². Después se suman dos deméritos fijos:- Demérito por guiones consecutivos (3000 por defecto): penaliza dos líneas seguidas terminadas en guion, porque los guiones apilados distraen la vista.
- Demérito por clase de ajuste (100 por defecto): se suma cuando la clase de ajuste de la línea dista más de un paso de la de la línea anterior.
La penalización por línea corta que se describe más abajo también entra en esta fórmula, como fealdad adicional; las penalizaciones de huérfanas y viudas actúan en una etapa posterior, cuando un párrafo se reparte entre columnas.
El algoritmo elige la secuencia de cortes con menos deméritos en total en todo el párrafo: esa es toda su ventaja sobre el método voraz.
El algoritmo recorre hacia atrás los nodos activos para encontrar el camino con menos deméritos en total: el conjunto de puntos de corte óptimo para todo el párrafo.
Penalizaciones de huérfanas, viudas y líneas cortas
Postext amplía el modelo de costes habitual con tres penalizaciones editoriales, que apartan al motor de las maquetaciones con finales de párrafo y de columna poco afortunados:
- La penalización por huérfana se aplica cuando una partición candidata dejaría menos de
orphanMinLineslíneas al principio de la columna siguiente. El valor por defecto deorphanPenaltyes1000. - La penalización por viuda se aplica cuando la partición dejaría menos de
widowMinLineslíneas al pie de la columna en curso. El valor por defecto dewidowPenaltyes1000. - La penalización por línea corta (el runt de la tipografía inglesa) se aplica cuando la última línea del párrafo mediría menos de unos
runtMinCharacters × normalSpaceWidthpíxeles, una cuenta en espacios entre palabras, no en letras: el valor por defecto, 20, equivale a una última línea de unas 8 a 12 letras. El valor por defecto deruntPenaltyes1000, el mismo para todas las líneas cortas; congradedRuntPenalty, se gradúa según lo que le falta a la línea,runtPenalty × (1 − ancho / umbral), de modo que un final de dos palabras cuesta menos que uno de una y gana cuando una línea de arriba puede ceder una palabra. A diferencia de las de huérfanas y viudas (que se suman linealmente al demérito de la partición), la penalización por línea corta se introduce como fealdad equivalente dentro de la fórmula cuadrática de Knuth–Plass, de modo que compite en la misma escala que la fealdad de las líneas (que se satura en 10000), en lugar de quedar eclipsada por ella.
Cuando la penalización pierde de todos modos, porque ningún corte alternativo es viable, el motor recurre a lo que haría un cajista a mano: compone el párrafo con una línea menos (tightenRunts). Los espacios de todas las líneas se estrechan, nunca por debajo de minWordSpacing, y si con eso no basta para subir las palabras sueltas, se reduce además un poco el espaciado entre letras, en el paso más pequeño que funcione y nunca más de maxRuntTracking milésimas de em. La composición más corta también tiene que respetar maxWordSpacing. Meter las palabras en una línea menos estrecha casi todas las líneas, pero puede desplazar un corte de modo que otra línea tenga que estirarse mucho. Si una composición estira una línea más allá de maxWordSpacing (o, si el párrafo ya tiene una línea más abierta, más que esa línea), no sirve como arreglo, y la línea corta se queda. Solo cuentan las líneas que siguen justificadas: una línea que el párrafo compone en bandera porque pasaría de 3 veces el espacio normal (véase Líneas que el algoritmo no puede llenar) no sube el listón, y la composición más corta no puede dejar en bandera más líneas de las que tenía el párrafo. Tampoco cambia una línea en bandera por líneas justificadas que pasen de ese listón: el párrafo conserva su línea en bandera y su línea corta. El equilibrado de columnas sigue la misma regla en el sentido contrario: si un párrafo que iba a alargar una línea para llenar una columna corta acabaría en una línea corta, se deja como estaba; llenar una columna no justifica dejar sola una sílaba.
Un bloque que se ha compuesto así, con una línea menos, lo indica: VDTBlock.runtFix contiene { tracking }, el espaciado negativo entre letras que se ha usado, en milésimas de em (0 si han bastado los espacios entre palabras). Ningún renderizador lo lee; está ahí para quien ajusta el texto, porque un párrafo en el límite se ve apretado junto a sus vecinos, y con unas pocas palabras menos se habría compuesto sin tocar el espaciado entre letras.
La penalización por línea corta se suma a los deméritos del nodo candidato dentro del algoritmo de corte de líneas, así que este puede sacrificar una línea algo más floja a cambio de una línea final más larga, pero solo dentro de maxWordSpacing: una línea estirada más allá de ese límite lleva una fealdad adicional mayor que cualquier penalización por línea corta o por guion, de modo que el algoritmo prefiere una última línea corta, o una última palabra partida, antes que un espacio entre palabras que supere el límite. Las penalizaciones de huérfanas y viudas entran en una optimización aparte, que se ejecuta cuando un párrafo pasa de una columna a otra: se puntúa cada partición candidata (sumando los deméritos de holgura, de huérfana y de viuda) y gana la más barata, así que el motor prefiere de forma natural las particiones que evitan las dos siempre que es posible. Para equilibrar unas reglas con otras, ajusta orphanPenalty, widowPenalty o runtPenalty; si pones cualquiera de ellas a 0, desactivas esa regla, y avoidOrphans, avoidWidows y avoidRunts (todas true por defecto) apagan una regla entera. Los elementos de lista se incluyen mediante avoidOrphansInLists, avoidWidowsInLists y avoidRuntsInLists (todos, true por defecto). Esas reglas mantienen juntas las líneas de un mismo elemento; shortListSplit mantiene juntos los elementos de una lista corta (desactivado por defecto; véase Texto base).
#Por qué importa
La diferencia se ve en la práctica. En una composición voraz, encontrarás párrafos en los que una línea está claramente más floja que sus vecinas, y si te fijas bien, verás que ocurre porque la línea anterior se quedó con una palabra de más. Knuth-Plass lo evita aceptando una línea en curso algo peor a cambio de una siguiente mucho mejor, porque puede ver las consecuencias.
#Implementación de Postext
Postext implementa el algoritmo de Knuth-Plass completo en el módulo knuthPlass/ del paquete principal: un núcleo de programación dinámica (nodos activos, deméritos, recorrido hacia atrás) y dos adaptadores que convierten el texto al modelo de cajas, pegamento y penalizaciones:
- Texto sin formato: usa
@chenglou/pretextpara medir el texto sin DOM. Pretext da los anchos de los segmentos y de los guiones opcionales, y Postext los convierte en elementos de Knuth-Plass. - Texto con formato: trata los tramos en negrita y en cursiva midiéndolos con Canvas. Cada fragmento con estilo se convierte en una o más cajas, y los puntos de separación silábica se insertan como penalizaciones.
El núcleo de programación dinámica existe dos veces y las dos versiones eligen los mismos cortes: en TypeScript y, desde postext 1.33, en Rust compilado a WebAssembly, que responde por defecto y es más rápido. El de TypeScript responde hasta que el módulo está compilado y allí donde WebAssembly no está disponible, así que cuál de los dos compuso un párrafo nunca cambia la maquetación (véase El corte de líneas (WebAssembly)).
Los dos adaptadores calculan el justifiedSpaceRatio de cada línea: el ancho con el que se pinta cada espacio de la línea, dividido entre el espacio de la redonda. Todos los espacios de una línea se pintan igual, sea cual sea su estilo. Los límites, en cambio, acotan algo un poco distinto: el cambio de cada espacio respecto a su propio ancho, contado en espacios de redonda. Un espacio en cursiva o en negrita es más estrecho o más ancho que el de la redonda, así que una línea que contiene alguno se pinta un poco por debajo de minWordSpacing o por encima de maxWordSpacing, aunque se haya cortado dentro de esos límites. Una línea así incluye además justifiedSpaceLimitRatio, el número que acotan los límites: 1 más la holgura de la línea dividida entre sus espacios, cada uno con su propio ancho. lineWordSpacing(line) devuelve ese número, o justifiedSpaceRatio en una línea cuyos espacios son todos de redonda, en la que los dos coinciden. Este valor alimenta el sistema de depuración de líneas flojas que se describe más abajo; la maquetación en sí (el paso a bandera, el arreglo de las líneas cortas, la palanca de los párrafos aflojados) sigue usando la proporción pintada. Las proporciones solo se calculan en las líneas que no son la última; cómo se compone la última línea se explica en La última línea.
En torno al algoritmo de corte de líneas, la medición del texto está en el módulo measure/ (la medición del texto sin formato y con formato, las métricas de los glifos con Canvas y el tratamiento de las cadenas de fuente), detrás de una caché de medición explícita. Cuando llegan o desaparecen estilos de una familia, el motor descarta lo que había medido en esa familia, tanto en sus cachés de anchos como en la caché de medición que mantenga la aplicación, de modo que las mediciones siguen siendo exactas cuando terminan de cargarse las fuentes web, sin que la aplicación tenga que vaciar nada (véase Cargar las fuentes antes de componer). El análisis, la etapa que convierte el texto de origen en bloques y tramos en línea, está en el módulo parse/. Consulta la página Arquitectura para ver cómo se organizan los archivos de estos módulos.
El corte de líneas también interactúa con los tipos de contenido más recientes. Los recursos flotantes ocupan la banda superior o inferior de una columna o de una página y acortan la altura de las columnas, a la que reaccionan las penalizaciones de huérfanas, viudas y holgura. Los pies y las celdas de las tablas se miden como tramos de texto con formato partidos en líneas, con las fuentes de los pies y de las tablas. Las líneas de las fórmulas en bloque se centran y son indivisibles: nunca se justifican ni se cortan. Para más detalles, consulta Formato de documento › Recursos y Configuración › Estilo de tablas / Estilo de pies de recurso.
Solución de reserva: si Knuth-Plass no encuentra ningún conjunto de cortes válido (algo que puede ocurrir con columnas extremadamente estrechas o con palabras muy largas), el motor recurre al corte voraz de Pretext, layoutNextLine(). Así, la maquetación siempre se completa.
#Separación silábica
La separación silábica y la justificación son inseparables. Sin separación silábica, la única forma que tiene el motor de evitar una línea floja es pasar una palabra a la línea siguiente, lo que muchas veces solo desplaza el problema. La separación silábica le da muchos más puntos de corte entre los que elegir, y mejora enormemente la calidad del texto justificado.
#Patrones de calidad TeX
Postext usa Hypher (hypher v0.2.5) para la separación silábica, con patrones de separación de TeX/Liang. Son los mismos patrones que usa TeX desde 1983: una representación compacta de las reglas de división en sílabas, obtenida con el algoritmo de generación de patrones de Frank Liang a partir de grandes corpus de palabras.
Los patrones codifican un conjunto de reglas numeradas que, superpuestas a una palabra, indican dónde se permite cortar (números impares) y dónde no (números pares). Los parámetros leftmin y rightmin del archivo de patrones de cada idioma garantizan un número mínimo de caracteres antes y después de cualquier punto de corte. En inglés (en-us), suelen ser 2 y 3, respectivamente: una palabra debe tener al menos 2 caracteres antes del guion y 3 después.
#Idiomas admitidos
| Código de idioma | Idioma |
|---|---|
'en-us' | Inglés (EE. UU.) |
'es' | Español |
'fr' | Francés |
'de' | Alemán |
'it' | Italiano |
'pt' | Portugués |
'ca' | Catalán |
'nl' | Neerlandés |
Cada idioma carga su propio conjunto de patrones. Las instancias de Hypher se crean cuando se necesitan y se guardan en caché: la primera llamada para un idioma paga el coste de inicialización, y las siguientes son instantáneas. Un idioma con etiqueta de región usa los patrones de su lengua ('es-ES' se divide con 'es', y 'en-GB', con 'en-us'). Un idioma sin patrones recurre a en-us, y el motor lo avisa una vez en la consola (véase Separación silábica en la referencia de la configuración). El chino, el japonés y el coreano son la excepción: se componen sin separación silábica y sin aviso, y sus líneas se cortan entre caracteres (véase Chino, japonés y coreano, más abajo).
El catalán sigue las normas del Institut d'Estudis Catalans (Llibre d'estil, capítulo VI). Se aplican desde postext 1.14:
- Cada corte deja al menos dos letras a cada lado (
ter-ra,cai-xa,plu-ja). - La ela geminada se corta entre sus dos l, y el guion ocupa el lugar del punto volado: il·lusió queda
il-|lusió. La línea se mide sin el punto, así que la justificación y Knuth–Plass ven su anchura real. - Dos vocales en hiato no se separan (
cièn-cia,ca-mions). Una i o una u entre vocales cuenta como consonante y abre su sílaba (fe-ia,ve-ient). - No se corta justo después de un apóstrofo (
s'ha-via, y nuncas'-havia). El inglés, el francés y el italiano tienen sus propias reglas para el apóstrofo (véase Separación silábica). - Las palabras más comunes que llevan prefijo se cortan por el prefijo (
vos-altres,cel-obert,ben-estar). - Para que las palabras con pronombres enclíticos solo se corten por su propio guion (
portar-|lo), como prefiere el IEC, fijahyphenation.compoundsenfalse.
#Por qué Hypher (y no un algoritmo propio)
El sistema de separación silábica original de Postext usaba una heurística propia basada en las vocales: detectaba los límites entre sílabas buscando grupos de vocales, prefijos comunes (over-, under-, inter-) y sufijos comunes (-tion, -ment, -sion). Era sencilla y rápida, pero tenía limitaciones de fondo:
| Aspecto | Heurística propia | Hypher (patrones TeX) |
|---|---|---|
| Precisión | Buena para palabras comunes, poco fiable para las inusuales. La detección por vocales omite muchos puntos de corte válidos y crea otros inválidos. | Casi perfecta. Los patrones se generan a partir de grandes corpus y se han refinado durante más de 40 años. |
| Cobertura idiomática | Había que definir manualmente conjuntos de vocales, prefijos y sufijos para cada idioma — tedioso y propenso a errores. | Existen archivos de patrones para más de 50 idiomas, mantenidos por la comunidad de TeX. Añadir un idioma es añadir una importación. |
| Estándar de la industria | No es un estándar reconocido. No hay herramientas ni una comunidad que la respalde. | Los mismos patrones usados por TeX, LibreOffice, Firefox, Chrome y prácticamente todos los sistemas de composición profesional. |
| Mantenimiento | Cada caso especial es un error que hay que corregir a mano. | Archivos de patrones mantenidos por la comunidad. Las correcciones llegan del proyecto original. |
| Tamaño del paquete | ~170 líneas, sin dependencias. | El núcleo de Hypher ocupa unos 3 KB. Cada archivo de patrones de un idioma añade de 20 a 80 KB (comprimido con gzip, de 5 a 20 KB). Los ocho idiomas incluidos suman unos 300 KB (comprimidos con gzip, unos 80 KB). |
| Rendimiento | Muy rápida (un simple recorrido de las cadenas). | Rápida (una búsqueda en un trie por carácter). Despreciable en la práctica: la separación silábica nunca es el cuello de botella. |
La elección es clara: un paquete más grande a cambio de una corrección muchísimo mayor y de ningún trabajo de mantenimiento. En un motor de maquetación orientado a la calidad editorial, gana la corrección. Una sola separación silábica incorrecta en un libro impreso sale más cara que unos cuantos kilobytes más de patrones.
#Cómo se integra la separación silábica con Knuth-Plass
Antes de que el texto entre en el algoritmo de Knuth-Plass, el motor lo procesa con Hypher, que inserta guiones blandos (\u00AD de Unicode) en todos los puntos de corte válidos. Después, estos caracteres invisibles se convierten en penalizaciones de Knuth-Plass con un coste de 50 y flagged: true.
El algoritmo trata los cortes por separación silábica como una opción más que evaluar, junto con los límites naturales entre palabras (donde el pegamento permite un corte). Si poner un guion da menos deméritos en total que dejar la línea floja, el algoritmo pone el guion. Si no, deja la palabra entera.
El demérito por guiones consecutivos (3000 por defecto) hace que el algoritmo evite a toda costa poner guiones en dos líneas seguidas, una convención tipográfica que imponen prácticamente todas las guías de estilo.
Un guion en la última línea de una columna o de una página obliga al lector a pasar a la columna siguiente a mitad de una palabra. bodyText.hyphenateAcrossColumns: false evita esos guiones: cuando la línea que cierra una columna termina en guion, el párrafo se vuelve a cortar con ese guion penalizado como una línea corta, de modo que el algoritmo termina esa línea en una palabra entera siempre que los espacios entre palabras de las líneas de arriba puedan absorber la diferencia sin salirse de maxWordSpacing y minWordSpacing. Si no pueden, el guion se queda. Se aplica al primer cambio de columna de cada párrafo y a los siguientes que caen donde termina una columna llena. Un cambio posterior que cae en otro punto (una columna acortada para evitar una viuda, una banda de cierre cortada a la misma altura) recibe un segundo corte a partir de esa columna: las líneas ya colocadas en las columnas anteriores conservan sus cortes, y solo se vuelve a cortar el resto del párrafo. Así, el guion solo se queda donde ningún corte dentro de los límites lo evita; en la práctica, en el primer cambio de columna de un párrafo. hyphenateAcrossColumns: 'withinPage' solo protege las líneas que cierran una página: quien lee y pasa la página, o salta a la de enfrente, encuentra una palabra entera, pero se permite partir una palabra entre dos columnas de la misma página. Con tres columnas, eso deja sin protección dos de cada tres finales de columna, así que se vuelven a cortar menos párrafos, y menos líneas tienen que ensanchar o estrechar sus espacios para compensarlo. El algoritmo solo distingue las formas de llegar a una línea hasta la última línea que protege, así que un reintento cuesta más o menos lo mismo que la primera medición: una medición más por cada párrafo que vuelve a cortar.
Por defecto, la separación silábica solo se aplica cuando bodyText.hyphenation.enabled es true y bodyText.textAlign es 'justify': un borde en bandera está hecho para ser desigual. El texto en bandera también puede dividirse, con bodyText.hyphenation.ragged: true. Una línea en bandera no tiene espacios entre palabras que igualar, así que decide una zona de separación: una palabra que no cabe solo se divide si, al pasarla entera a la línea siguiente, quedaría un hueco mayor que hyphenation.zone (3 em por defecto). Knuth-Plass también corta el texto corrido en bandera (bodyText.optimalRagged, activado por defecto): sus espacios entre palabras mantienen su ancho, cada línea paga según lo que le falte para llegar a la medida, y la zona decide qué sílabas puede usar; dos líneas seguidas terminadas en sílaba cuestan lo que el demérito por guiones consecutivos. Cuando el texto se compone línea a línea (optimalRagged: false), nunca terminan en sílaba más de dos líneas seguidas. Consulta Texto en bandera.
Hay dos oportunidades de corte que no dependen de ese ajuste. Un guion entre dos letras (enseñanza-aprendizaje, físico-química) es un punto en el que la línea puede terminar: la palabra ya lleva el guion, así que no se añade nada. Knuth-Plass lo tiene en cuenta en todos los párrafos con bodyText.breakAfterHyphens (activado por defecto), con el mismo coste que una sílaba; en un párrafo justificado sin formato en línea, solo si hay dos letras a cada lado del guion, para que ninguna línea termine en la e- de e-mail. Si se desactiva, como hasta postext 1.4, un párrafo justificado sin formato en línea no corta justo después de ese guion, mientras que el mismo párrafo con una sola palabra en cursiva en cualquier sitio sí lo hace; los libros guardados antes de postext 1.5 se leen con el ajuste desactivado. La línea termina en el guion del propio texto, que la línea registra como hardHyphen junto a hyphenated, de modo que las funciones que eliminan un guion añadido (los titulillos, el cursor del Sandbox) conservan este. Una raya o una semirraya pegadas a las palabras (Madrid–Barcelona, I.—Que trata o, en inglés, say—that’s) también son un punto en el que la línea puede terminar, con los dos algoritmos de corte, gracias a bodyText.breakAfterDashes (activado por defecto): la línea termina en la raya, sin añadir nada ni sumar ningún coste. Lo mismo ocurre con una raya que cierra un tramo de un estilo delante de una palabra en otro (calidad–*precio*). En ese caso, la línea queda marcada como hyphenated, sin hardHyphen. Ninguna línea termina después de una raya que abre un inciso o un diálogo (—dijo, dijo "—Hola; unas comillas que cierran una palabra sí pueden ir delante de la raya, como en «no»—y o en el alemán „nein“—und), después de una raya situada delante de un signo de puntuación, de unas comillas o de un paréntesis (pensaba—» y, dijo—«no»), ni dentro de una serie de rayas o de un intervalo de números (1914–1918). Y una palabra más ancha que toda su medida (en una celda de tabla estrecha, un compuesto largo en una columna estrecha) nunca se deja sobresalir: el motor la divide por la última sílaba que cabe, con guion, o por el último carácter que cabe si el diccionario no ofrece ninguna sílaba en ese punto; un corte junto a un guion que ya tiene la palabra se hace detrás de ese guion y no añade otro, así que la palabra nunca muestra dos. El resto de la palabra conserva sus propios puntos de corte: un compuesto sigue cortándose por sus guiones, y una dirección web, por sus junturas, mientras que hasta postext 1.4 el resto se dividía por las sílabas del diccionario. Un párrafo sin formato en línea se corta por el último carácter que cabe, sin guion, salvo que tenga activada la separación silábica. Para Knuth-Plass no hay ningún conjunto de cortes viable alrededor de una palabra así, de modo que un párrafo justificado que la contiene se compone línea a línea.
Ese corte se avisa (desde postext 1.52): una palabra cortada entre dos de sus letras como aviso wordDividedByCharacter, una dividida por una sílaba como nota wordDividedBySyllable, con el texto de la línea. La línea lo registra como wordDivided ('character' o 'syllable'). Vale también para los titulillos, las aperturas y los marcos de una página compuesta, donde la causa habitual es un marco estrecho: da más espacio al texto o reduce el cuerpo. Un corte en un espacio de no separación no divide ninguna palabra y no se avisa.
Una barra entre dos palabras (entrada/salida, ACC/AHA/AAPA) también es un punto en el que puede terminar una línea de texto con formato, con bodyText.breakAfterSlashes (activado por defecto desde postext 1.53): la línea termina en la barra y la siguiente empieza con la palabra que la sigue; no se añade nada ni se penaliza. Cada lado de la barra ha de tener al menos tres letras o cifras seguidas, y no pueden ser dos cifras, de modo que una unidad (km/h, kg/m³, mg/dL), un par corto (y/o), una fracción y una fecha (1/2, 10/03/2026) no se separan. Una palabra más ancha que toda su medida se divide tras una barra antes que en una sílaba o entre dos letras, tras cualquier barra salvo la // de una dirección web, y ese corte no se notifica. Un espacio de ancho cero (U+200B) escrito en el texto también es un corte: la línea puede terminar ahí, no se añade nada y el carácter no llega al texto de la línea. Un párrafo sin formato en línea corta tras cualquier barra, donde la marca el diccionario de separación silábica, sea cual sea el ajuste. Las configuraciones guardadas antes de configVersion 19 se leen con breakAfterSlashes: false, los cortes de postext 1.51, que solo terminaba una línea con formato tras una barra dentro de una dirección web.
#Palabras compuestas
Un compuesto unido por guion (teórico-práctico, físico-química) puede cortarse en dos tipos de punto: detrás de su propio guion y por las sílabas del diccionario de cualquiera de sus partes (teó-rico-práctico). TeX, y el Chicago Manual of Style, mantienen enteras las partes y solo cortan un compuesto por su guion. bodyText.hyphenation.compounds: false hace lo mismo: el diccionario no toca ninguna palabra que tenga un guion entre dos letras, que así solo se corta detrás de ese guion. Un guion blando escrito entre dos letras de la palabra sigue permitiendo el corte, y un compuesto más ancho que toda la línea se sigue dividiendo donde haga falta. El valor por defecto, true, sigue dividiendo las partes, lo que da a una columna justificada estrecha más formas de llenar sus líneas.
La ortografía portuguesa pide repetir al principio de la línea siguiente el guion de un compuesto cortado a final de línea (vencer- · -se), y también las normas de la Real Academia Española desde 2010 (léxico- · -semántico), para que el lector sepa que el guion forma parte de la palabra. bodyText.repeatHyphen: true lo hace. El guion repetido se mide con su línea, así que el algoritmo le deja sitio, y se pinta como el resto del texto; en el PDF lleva un texto de sustitución que lo omite, de modo que el texto copiado o extraído del PDF contiene la palabra una sola vez (vencer-se). La línea lo registra como repeatedHyphen, y su plainStart y su sourceStart apuntan detrás de él, de modo que el mapa de origen, los enlaces y los titulillos leen la palabra tal como está escrita. Una dirección web nunca lo lleva. Se aplica al texto corrido, los encabezados, las listas, las citas y los recuadros; un párrafo sin formato que contiene un compuesto se corta entonces con el algoritmo que se usa para el texto con formato.
#Palabras que no se dividen
Se le puede pedir al diccionario que no divida ciertas palabras. bodyText.hyphenation.exceptions las enumera una a una: 'Alice' no se divide nunca, y 'You-Tube', solo por el guion de su entrada, como en el \hyphenation{…} de TeX. bodyText.hyphenation.capitalized: false mantiene enteras todas las palabras que empiezan por mayúscula, y citations.hyphenateNames: false, los apellidos de los autores que cita el documento. El algoritmo tiene entonces menos puntos en los que terminar una línea: donde un nombre quedaba dividido, la línea anterior tiene a cambio más o menos espacio entre sus palabras, sin salirse de minWordSpacing y maxWordSpacing. Una palabra más ancha que toda la medida se sigue dividiendo. Consulta Palabras que no se dividen para saber cómo se compara una entrada con el texto.
#Dónde nunca se corta una línea
Hay puntos en los que nunca se puede cortar, con ninguno de los dos algoritmos:
- Un espacio de no separación. U+00A0, el espacio fino de no separación U+202F y el espacio de cifra U+2007 unen las palabras que tienen a cada lado: un número y su unidad, una referencia a una página, un grupo de millares. Lo mismo ocurre en los pies, las celdas de tabla, los recuadros y los textos de diseño (titulillos, aperturas). Cada uno conserva su propio ancho, tal como se mide; la justificación solo estira los espacios entre palabras. Una fuente sin glifo para U+202F o U+2007 les da el ancho que les dan los navegadores (la mitad de un espacio entre palabras y el ancho de una cifra) en todas las salidas, incluido el PDF; prácticamente todas las fuentes tienen U+00A0. La excepción es un grupo unido de este modo que sea más ancho que toda la línea, tenga o no formato en línea: se corta por su último espacio de no separación, en lugar de partirse dentro de una palabra. El unidor de palabras U+2060 une sin ocupar espacio, también en el texto chino: dos caracteres con uno entre ellos se quedan en la misma línea. (Un número en círculo como ① no lo necesita: desde postext 1.25, el texto chino lo mantiene junto al carácter siguiente). También une U+FEFF, el espacio de no separación de ancho cero, aunque el carácter pensado para eso es el unidor de palabras.
- La etiqueta de una referencia con número. Una referencia en línea que imprime un número o una página (
sección 3.2,pág. 112,Fig. 1.7,Teorema 2) es una sola palabra: ninguna línea se corta entre su palabra y su número, sea cual sea el espacio de la plantilla, y ese espacio no se estira. Una referencia que imprime palabras (el texto de un ancla, un título, untext="…"propio) funciona de otra manera desde postext 1.28: se corta por sus propios espacios, que se estiran con la línea (crossRefs.breakLabels), aunque sus palabras siguen sin dividirse nunca con guion. - Texto pegado. Una palabra en negrita o en cursiva y la puntuación que la sigue (
**ósmosis**.), los paréntesis que rodean una referencia en línea ((:ref{id="fig-3"})), una palabra compuesta en dos tramos: el texto sin espacios intermedios es una sola unidad, como si fuera una palabra. Si esa unidad no cabe en lo que queda de línea, pasa entera a la siguiente. Si empieza una línea y aun así no cabe, su última palabra se divide por una sílaba para que la puntuación pase a la línea siguiente con el final de la palabra; solo una unidad sin ninguna sílaba por la que dividirse deja que su puntuación empiece la línea siguiente. - En un guion blando sin letra a uno de sus lados. Un guion blando (U+00AD) escrito en el texto solo es un punto de corte entre dos letras o cifras: escrito entre
toygether, permite que una línea termine ento-. Escrito en el borde de una palabra, después de un paréntesis o de unas comillas de apertura, o delante de un signo de puntuación de cierre, no corta ni imprime nada, así que ninguna línea termina en un guion detrás de una palabra entera. Y como el diccionario no toca una palabra que ya lleva un guion blando, uno escrito delante de la primera letra mantiene entera esa aparición de la palabra, en cualquier idioma: con uno delante de suA,Alicenunca se divide. Así escribe InDesign su guion opcional, y el texto exportado desde InDesign conserva ese sentido. Hasta postext 1.27, un guion blando así era un punto de corte como cualquier otro (said Alice-), y en catalán el resto de la palabra se seguía dividiendo. - Dentro de una palabra de una escritura con letras enlazadas. Una palabra árabe (o siríaca, n'ko o mongola) nunca se divide con guion, ni siquiera por un guion blando escrito en ella, ni se corta entre sus letras: sus letras van enlazadas, y un fragmento compuesto por separado tomaría otras formas y otro ancho. En árabe y en los demás idiomas de derecha a izquierda, la separación silábica está desactivada por defecto; si se activa (para las palabras latinas de un libro árabe, o en un libro en español que cita en árabe), el diccionario se salta esas palabras. Una palabra con dos estilos (
كتا**ب**) sigue siendo una sola palabra, que se mide entera. Si es más ancha que toda la línea, sobresale de la medida, y la compilación genera el aviso de contenidounbreakableWordOverflow.
Hasta postext 1.4, un párrafo con formato en línea o con un :ref podía cortarse por un espacio de no separación, y una línea en bandera podía terminar en el ( que precede a una referencia o empezar con el punto que sigue a una palabra en negrita.
#Límites de espaciado entre palabras
El modelo del pegamento da al motor unos límites explícitos para lo que pueden estirarse o comprimirse los espacios entre palabras. Estos límites se controlan con dos propiedades de configuración de bodyText:
| Propiedad | Por defecto | Descripción |
|---|---|---|
maxWordSpacing | 2 | Límite superior del espacio entre palabras, como multiplicador del ancho normal del espacio. Con el valor por defecto, los espacios pueden estirarse hasta el 200 % de su ancho natural. |
minWordSpacing | 0.6 | Límite inferior del espacio entre palabras, como multiplicador del ancho normal del espacio. Con el valor por defecto, los espacios pueden comprimirse hasta el 60 % de su ancho natural. |
Estos multiplicadores se convierten directamente en los valores stretch y shrink del pegamento en el modelo de Knuth-Plass:
estiramientoPorEspacio = anchoNormalEspacio × (maxWordSpacing - 1)
compresiónPorEspacio = anchoNormalEspacio × (1 - minWordSpacing)
Con los valores por defecto (2 / 0.6), si el ancho normal del espacio es 4 px:
- Cada espacio puede estirarse 4 px (de 4 px a 8 px)
- Cada espacio puede comprimirse 1,6 px (de 4 px a 2,4 px)
Unos límites más estrictos (por ejemplo, maxWordSpacing: 1.2) dan un espaciado más uniforme, pero dejan menos margen de maniobra al algoritmo, lo que puede traducirse en más divisiones silábicas o, en casos extremos, en desbordamientos. Unos límites más amplios (por ejemplo, maxWordSpacing: 2.5) dan más flexibilidad al algoritmo, pero permiten un espaciado visiblemente desigual en algunas líneas.
Los valores por defecto, 2 y 0,6, dan prioridad a la flexibilidad del algoritmo: dejan a Knuth-Plass margen suficiente para evitar desbordamientos, divisiones silábicas y líneas cortas en columnas estrechas, y se mantienen holgadamente dentro de los intervalos que la bibliografía tipográfica considera aceptables.
#Líneas que el algoritmo no puede llenar
Knuth-Plass no estira una línea más allá de maxWordSpacing cuando tiene alternativa, y el arreglo de las líneas cortas tampoco (véase Penalizaciones de huérfanas, viudas y líneas cortas). Una línea que supera el límite cuesta más que cualquier otro defecto que se tiene en cuenta (un guion, una última línea corta, una línea algo más floja que sus vecinas) desde el momento en que lo supera, y a partir de ahí el coste crece con el cuadrado de su proporción de estiramiento, así que el algoritmo prefiere dividir una palabra, repartir la holgura entre las líneas de alrededor o dejar dos líneas un poco flojas antes que una demasiado floja.
A veces, ningún conjunto de cortes respeta los límites. La línea que no admite la palabra siguiente tiene demasiado pocos espacios entre los que repartir la holgura: la primera línea de un párrafo, con su sangría, seguida de una palabra demasiado larga para subir y demasiado corta para dividirse; una línea de términos largos que no se pueden partir; una URL larga que solo se corta por sus propias junturas; un elemento de lista cuya última palabra no sube, mientras que cualquier división silábica dejaría una línea corta. La frecuencia depende de la medida, la fuente y el idioma. En una columna de 83 mm de texto en inglés a 9,3 pt, entre el 2 % y el 8 % de las líneas de un capítulo superaron maxWordSpacing, según la fuente (más cuanto más ancha); el mismo capítulo en español, cuya separación silábica ofrece muchos más cortes, no tuvo ninguna. Knuth-Plass acepta entonces la línea menos mala, y sus espacios superan el límite. Si tuvieran que estirarse más de 3 veces el ancho normal del espacio, el motor compone esa línea en bandera: se dibuja con su espaciado natural y el borde derecho queda desigual, como en la última línea de un párrafo. El umbral es el mismo que usa el aviso de línea floja del Sandbox. La línea se compone en bandera y se avisa: un aviso de contenido raggedLine indica su texto, su página y la proporción que habría necesitado (en el Sandbox, Línea justificada en bandera, entre los avisos de composición; al hacer clic en él, se selecciona la línea), y VDTLine.raggedSpaceRatio guarda esa proporción en la línea. Cambia la redacción o dale a la línea un punto por el que cortarse. Las líneas que no superan el umbral se dejan tal como se midieron. Lo mismo ocurre dentro de los recuadros, cuya medida estrecha es donde más se da; hasta postext 1.4, un recuadro mantenía justificada esa línea por mucho que se estiraran sus espacios.
En las columnas estrechas, suele tratarse de la primera línea de un párrafo corto, a la que la sangría deja pocos espacios, seguida de un monosílabo o de unas comillas de apertura («„Beim Schwärmen stechen sie» + «nicht.»). El motor no suprime la sangría para salvar esa línea: una sangría que cambia de un párrafo a otro se ve como un defecto a lo largo de la columna, y una sola línea floja no tanto. En columnas de revista de 60 y 92 mm en alemán, maxJustifyTracking: 12 (véase más abajo) redujo las líneas que superaban 2× del 8,8 % al 4,0 %; lo que queda se arregla retocando el texto.
Para tener menos líneas así, prueba, por este orden: comprobar que la separación silábica está activada y en el idioma del texto; ensanchar la medida o reducir el cuerpo; dejar que las líneas usen un poco de espaciado entre letras (sección siguiente), o subir maxWordSpacing, con lo que esas mismas líneas pasan a contar como dentro del límite sin componerse de otra manera.
#Espaciado entre letras como último recurso
bodyText.maxJustifyTracking permite que una línea así use un poco de espaciado entre letras (tracking) en lugar de más espacio entre palabras, hasta ese número de milésimas de em por carácter (10 = 0,01 em, la unidad de InDesign). Funciona en los dos sentidos: una línea cuyos espacios se estirarían más allá de maxWordSpacing separa sus letras hasta que los espacios vuelven al límite, y una línea que solo cabría con los espacios por debajo de minWordSpacing junta sus letras en lugar de pasar una palabra a la línea siguiente. Knuth-Plass lo tiene en cuenta al elegir los cortes: una línea con espaciado entre letras cuesta algo más que una dentro de los límites y mucho menos que una que los supera, así que solo se recurre a él donde los espacios entre palabras no bastan. Las líneas dentro de los límites, la última línea de un párrafo (salvo que se desborde), una línea de una sola palabra y una línea con un chip nunca lo usan. La línea lo registra como VDTLine.letterSpacing, sumado al espaciado entre letras propio de su bloque, y el canvas, el HTML y el PDF lo pintan. Las palabras de una escritura con letras enlazadas (como el árabe) nunca reciben espaciado entre letras, ni este ni ningún otro (el letterSpacing de un estilo, el equilibrado de columnas, el arreglo de las líneas cortas): separar sus letras rompe los enlaces. La línea no cuenta ninguna de sus letras, y los renderizadores las pintan sin espaciado; un estilo que fija letterSpacing en ese texto genera el aviso de contenido joiningScriptLetterSpacing.
bodyText: {
maxWordSpacing: 2,
maxJustifyTracking: 10, // como mucho 0,01 em por letra, en uno u otro sentido
}Está desactivado por defecto, así que los documentos conservan sus líneas salvo que lo pidan. En el capítulo en inglés de antes, 10 ‰ redujo las líneas que superaban maxWordSpacing a una o ninguna por fuente, y 20 ‰, a ninguna en tres fuentes de cuatro. Mantén un valor bajo: a partir de unos 20 ‰, el espaciado entre letras empieza a notarse como una línea más clara o más oscura.
#Cachida en el texto árabe
Una línea árabe justificada no recibe espaciado entre letras: sus letras van enlazadas, y separarlas rompe los enlaces. En su lugar, se estira por dos sitios: los espacios entre palabras y las cachidas (kashida), los enlaces alargados que el calígrafo traza entre dos letras unidas (كتاب → كتـاب). Postext los inserta como tatweels (U+0640, ـ) enteros, que la fuente dibuja como un solo trazo; Amiri convierte una serie de uno a siete en una cachida curva.
bodyText.kashida vale 'auto' por defecto en un documento cuyo idioma se escribe en alfabeto árabe (ar, fa, ur…) y 'none' en cualquier otro; fíjalo en 'auto' para alargar las palabras árabes citadas en un libro en otra lengua. En cada línea, salvo la última de un párrafo, primero se ensanchan los espacios entre palabras, hasta un cuarto de su ancho; después, las cachidas absorben el resto de la holgura, un tatweel cada vez, empezando por los mejores puntos, ronda tras ronda, y cada palabra se vuelve a medir entera con sus tatweels. Lo que sobra, menos de un tatweel, vuelve a los espacios. Knuth-Plass cuenta el alargamiento posible de cada palabra como estiramiento, así que elige los cortes sabiendo que una línea de palabras árabes puede ensancharse por ahí.
Los puntos donde puede ir una cachida salen de las reglas de raqim-kashida (MIT): solo entre dos letras que se enlazan, nunca después de una letra que no se enlaza con la siguiente (ا د ذ ر ز و ة), nunca al final de una palabra, nunca dentro del lām-alif, y siempre detrás de los signos de la letra anterior. kashidaPatterns: 'naskh' sigue las reglas clásicas del naskh (la matriz de Benatia con las prohibiciones de Afifi: nada después de kāf o lām, nada antes de ṣād, ʿayn o wāw, y un hāʾ pronominal final se alarga justo antes); 'simple', las prioridades de Microsoft para los tipos de letra modernos sencillos, y 'nastaliq', las reglas del naskh adaptadas al nastaʿlīq. El valor por defecto, 'auto', se basa en la fuente del texto base: ninguna cachida en un tipo ruqʿa o dīwānī como Aref Ruqaa, reglas de nastaʿlīq en un tipo nastaʿlīq y de naskh en los demás. Una palabra admite como máximo kashidaPerWord alargamientos (1 por defecto), de kashidaMaxLength em como máximo (0,6, tres tatweels de Amiri). Una palabra con un tatweel escrito por el autor se alarga en ese punto. Las palabras latinas, las cifras, los encabezados, las líneas en bandera y la última línea de un párrafo nunca reciben ninguna.
bodyText: {
kashida: 'auto', // lo normal en un libro árabe
kashidaPatterns: 'naskh', // 'auto' lo elige por la fuente
kashidaMaxLength: 0.6, // em
}Los tatweels forman parte del texto pintado (cada segmento los enumera en VDTLineSegment.kashida, y la línea los cuenta en VDTLine.kashida); el texto sin formato, el mapa de origen, los enlaces y el texto copiado del PDF no los incluyen.
#La última línea
En el modelo de cajas, pegamento y penalizaciones, cada párrafo termina con un pegamento de ancho 0, que se puede estirar infinitamente, seguido de un corte forzado. Ese pegamento final absorbe sin coste todo el espacio que quede en la última línea, así que las últimas líneas quedan en bandera de forma natural: se dibujan con su ancho natural, y el justifiedSpaceRatio solo se calcula en las líneas que no son la última.
Hay una excepción. Knuth-Plass puede aceptar una última línea cuyo contenido natural es más ancho que la medida, contando con que su pegamento entre palabras se comprimirá: es la semántica habitual del ajuste del pegamento (glue-setting) de TeX. Los tres backends detectan este caso (el ancho natural del contenido de la línea supera la medida efectiva) y comprimen los espacios entre palabras de esa última línea para que encaje exactamente en la medida, en lugar de desbordarla. La comprobación es idéntica en los backends de canvas, de PDF y de HTML.
#Corte óptimo frente a corte voraz
La propiedad bodyText.optimalLineBreaking (por defecto: true) controla qué algoritmo de corte de líneas usa el motor:
true: el algoritmo de programación dinámica de Knuth-Plass. Evalúa todos los conjuntos de cortes posibles y elige el óptimo para todo el párrafo. Es el ajuste recomendado para cualquier texto justificado. El texto corrido en bandera también lo usa conbodyText.optimalRagged(activado por defecto): los espacios entre palabras mantienen su ancho, y el algoritmo pondera, en cambio, lo que le falta a cada línea para llegar a la medida, de modo que una línea a la que le faltan 3 em cuesta lo mismo que una línea justificada conmaxWordSpacing; así, el borde en bandera queda más regular y se aplican las reglas de las líneas cortas.false: el corte voraz, conlayoutNextLine()de Pretext. Es más rápido, pero da resultados de menor calidad. Úsalo solo cuando el rendimiento importe más que la calidad tipográfica (por ejemplo, en una vista previa en tiempo real con muchísimos caracteres).
Si Knuth-Plass está activado y no encuentra ningún conjunto de cortes válido (algo que puede ocurrir con columnas extremadamente estrechas o con palabras más largas que el ancho de la columna), el motor recurre automáticamente al corte voraz en ese párrafo.
#Líneas con tabuladores
Un párrafo con un tabulador (véase Formato del documento › Tabuladores y tabulaciones) se compone línea a línea, como componen el texto con tabuladores los procesadores de texto e InDesign, y nunca con Knuth-Plass, diga lo que diga optimalLineBreaking. El punto en el que cae un tabulador depende de lo que ya ocupe su línea, y la búsqueda óptima suma los anchos de cada línea candidata como si no fuera así. Lo mismo ocurre con el texto chino y japonés: un párrafo con un tabulador se corta entre caracteres según las reglas de corte de líneas del documento, sin compresión de la puntuación ni justificación entre caracteres.
En un párrafo justificado, una línea con un tabulador no se estira antes de su último tabulador, porque las tabulaciones fijan dónde empieza cada parte. Solo los espacios entre palabras que siguen al último tabulador absorben la holgura, y solo si la línea no es la última del párrafo; una línea sin ningún espacio entre palabras detrás de su último tabulador se compone tal como queda. Las líneas en bandera no cambian. La línea registra que contiene un tabulador (VDTLine.tabbed); cada tabulador es un segmento de espacio que indica su tabulación (VDTLineSegment.tab), y cada relleno, un segmento propio (VDTLineSegment.leader, 'text' o 'rule').
El equilibrado de columnas nunca vuelve a cortar un párrafo así con más holgura ni le añade espaciado entre letras: se queda tal como se midió, para que sus tabulaciones no se muevan.
#Chino, japonés y coreano
Un párrafo con más caracteres CJK que espacios entre palabras lo compone el compositor CJK, no Knuth-Plass. Todos los huecos entre dos caracteres son puntos en los que la línea puede cortarse (salvo donde lo prohíben las reglas de corte de cjk.lineBreak), así que una búsqueda óptima no tiene nada que ponderar; las líneas se llenan una tras otra. Si un carácter no cabe, la línea intenta primero hacerle sitio cediendo parte del blanco de la puntuación (la parte vacía de la celda de un signo) y de los espacios (push-in: los espacios entre palabras, hasta un cuarto de em; después, los puntos medios, los paréntesis, los signos de pausa y los espacios entre caracteres han y latinos, hasta un octavo de em, y los signos de final de oración, según lo permita cjk.punctuationWidth); solo si eso no basta, un carácter que no puede abrir línea se lleva consigo el anterior. Las reglas se explican en Tipografía de Asia oriental. Composición china trata el resto de la composición china: los anchos de la puntuación según la región, la retícula de caracteres y el texto vertical. Un documento japonés sigue, en cambio, JLReq (véase Composición japonesa): sus niveles de kinsoku, una línea que devuelve espacio en el orden de JLReq §3.8.3 (primero el medio cuadratín que sigue al signo con el que termina la línea, entero o nada, y nunca el blanco que sigue a 。 dentro de la línea), y ningún espacio añadido después de un paréntesis de apertura, delante de uno de cierre ni junto a 、。・:;?! cuando la línea se ensancha.
Una línea justificada de un párrafo así, salvo la última, se ensancha hasta la medida en el orden que fija clreq (§6.2.2.4):
- Los espacios entre palabras occidentales, por igual, hasta medio cuadratín cada uno.
- Los espacios entre caracteres han y latinos (
cjk.latinSpacing), por igual, hasta medio cuadratín cada uno. - Todos los huecos entre caracteres, por igual, incluidos los espacios entre caracteres han y latinos: entre caracteres han, entre un carácter han y un signo de puntuación, entre un carácter han y una palabra latina. Nunca dentro de una palabra latina, de un número o de una raya o unos puntos suspensivos de dos cuadratines, y nunca junto a un conector (~, –, una raya — sola) ni junto a una barra. Un signo colgado más allá del final de la línea (
cjk.hangingPunctuation) queda fuera de la medida.
La última línea de un párrafo se compone sin espaciado adicional. El espaciado se guarda en cada segmento de la línea (VDTLineSegment.tracking), de modo que el canvas, el HTML y el PDF pintan las mismas posiciones, y el Sandbox coloca el cursor en el carácter en el que se hace clic.
Una línea que necesitaría más de medio cuadratín entre sus caracteres (o más que bodyText.maxJustifyTracking, si está definido) se compone con ese máximo, sin llegar a la medida, y genera el aviso de contenido cjkLooseLine (en el Sandbox, Línea CJK corta). Suele deberse a una palabra latina larga o a una dirección web que no ha podido subir a esa línea. Una línea sin ningún carácter CJK, como el principio de una dirección web larga, se compone en bandera y sin aviso.
Para el resaltado de líneas flojas, una línea CJK se considera floja según su espaciado: su proporción es 1 más su espaciado en octavos de cuadratín, así que, con el umbral por defecto de 3, se marca una línea cuyos caracteres se separan más de un cuarto de cuadratín (lineLooseness(line, fontSizePx) da la proporción). El equilibrado de columnas puede alargar en una línea un párrafo CJK: corta las líneas un poco antes de llegar a la medida, un octavo de cuadratín cada vez, hasta dos cuadratines, y después las vuelve a ensanchar hasta la medida. En una retícula de caracteres, solo puede separar dos caracteres hasta headings.balancing.maxTracking, para que los caracteres conserven sus casillas (véase Justificación vertical: equilibrado de columnas).
#Depuración de líneas flojas
Incluso con Knuth-Plass y la separación silábica, algunas líneas quedarán más flojas de lo ideal, sobre todo en columnas estrechas con palabras largas o en idiomas con pocas posibilidades de separación silábica. La función de depuración de resaltado de líneas flojas te ayuda a localizar esas líneas problemáticas al instante.
#Cómo funciona
Cada línea justificada del árbol virtual del documento (VDT) incluye un justifiedSpaceRatio: el ancho con el que se pintan sus espacios, dividido entre el espacio de la redonda. Un valor de 1,0 corresponde al espacio de la redonda, y uno de 2,5, a espacios 2,5 veces más anchos. Una línea con espacios de otro estilo (una frase en cursiva, una palabra en negrita) incluye un segundo número, justifiedSpaceLimitRatio, su espacio entre palabras tal como lo acotan minWordSpacing y maxWordSpacing. lineWordSpacing(line) da el número que hay que comparar con los límites: el segundo, si la línea lo tiene, y si no, el primero.
Cuando debug.looseLineHighlight.enabled es true, el Sandbox pinta sobre su vista Canvas una capa semitransparente en todas las líneas cuyo espacio entre palabras (lineWordSpacing) supera el threshold configurado. Una línea compuesta justo en maxWordSpacing que contiene un espacio en negrita se pinta unas centésimas de espacio por encima y no cuenta: el algoritmo respetó el límite. El umbral por defecto es 3,0, lo que significa que solo se resaltan las líneas con espacios tres veces más anchos de lo normal. Es un listón alto a propósito; las líneas tan flojas son verdaderos problemas tipográficos. También es el ancho a partir del cual el motor compone en bandera una línea justificada en lugar de estirarla (véase Líneas que el algoritmo no puede llenar), así que, con el valor por defecto, el resaltado y el aviso looseLines apenas encuentran nada en el texto corrido: esas líneas se componen en bandera y generan, en su lugar, un aviso raggedLine. Baja el umbral a 1,5 o a 2 para encontrar las líneas flojas que siguen justificadas.
La capa no forma parte de la página, así que las páginas exportadas nunca la muestran. Para dibujarla en un canvas propio, llama a drawLooseLines(ctx, page, doc, { threshold }) justo después de renderPageToCanvas; findLooseLines(doc, { threshold }) devuelve esas mismas líneas como datos (véase Líneas flojas en tu propio canvas).
#Configuración
El resaltado de líneas flojas forma parte de la sección debug de PostextConfig:
| Propiedad | Tipo | Por defecto | Descripción |
|---|---|---|---|
looseLineHighlight. | boolean | false | Si se resaltan las líneas flojas. |
looseLineHighlight.color | ColorValue | #ff000040 | Color de la capa de resaltado. El valor por defecto es un rojo semitransparente. |
looseLineHighlight. | number | 3 | Multiplicador del ancho normal del espacio a partir del cual una línea se considera floja. Los valores más bajos detectan más líneas, y los más altos solo resaltan las peores. A partir de 3, el texto corrido casi no muestra ninguna, porque las líneas que superan 3× se componen en bandera. |
debug: {
looseLineHighlight: {
enabled: true,
threshold: 2.5,
color: { hex: '#ff660040', model: 'hex' },
},
}#Interpretar los resultados
Si activas el resaltado de líneas flojas y ves bandas rojas en algunas líneas, significa que el motor no ha encontrado ninguna forma de componerlas sin un espacio entre palabras excesivo. Empieza por arriba: las causas están ordenadas de la más probable a la menos probable.
| Causa | Solución |
|---|---|
| La columna es demasiado estrecha para el cuerpo del texto | Aumenta el ancho de la columna, reduce el cuerpo o pasa a una disposición de una sola columna. |
| Palabras largas con pocos puntos de separación | Comprueba que la separación silábica está activada y que el idioma configurado es el correcto. Algunos términos técnicos o nombres propios no tienen puntos de corte válidos. |
| La separación silábica está desactivada | Activa bodyText.. La justificación sin separación silábica es casi siempre peor. |
| Unas pocas líneas siguen flojas con cualquier fuente | Deja que esas líneas usen un poco de espaciado entre letras: bodyText.. Véase Espaciado entre letras como último recurso. |
| Los límites de espaciado son demasiado estrictos | Aumenta un poco maxWordSpacing (por ejemplo, de 2 a 2,4). Así, el algoritmo tiene más margen. |
| El idioma tiene palabras compuestas largas (por ejemplo, el alemán) | Comprueba que el idioma configurado es el correcto. Los patrones de separación silábica del alemán tratan bien las palabras compuestas, pero solo si el motor sabe que el texto está en alemán. |
#Justificación vertical: equilibrado de columnas
Knuth-Plass resuelve el problema horizontal: dónde se corta cada línea. El equilibrado de columnas resuelve su equivalente vertical: dónde termina cada columna. Las editoriales esperan que todas las columnas de una página empiecen arriba y terminen a ras del pie de la página, línea con línea en toda la doble página. Las propias reglas que protegen la calidad del texto juegan en contra: la protección contra huérfanas y viudas, los encabezados con keepWithNext y las figuras que no se pueden partir pasan contenido a la columna siguiente antes de que la actual esté completamente llena, y dejan al pie de esta una o más líneas vacías de la rejilla base.
Con headings.balancing activado (el valor por defecto, salvo en el texto vertical y en una retícula de caracteres), el motor cierra esos huecos como lo haría un maquetista, aplicando sus palancas por estricto orden de prioridad editorial; cada palanca solo actúa sobre lo que no ha podido absorber la anterior:
-
Un recuadro que cierra la columna. Un recuadro que termina una columna corta baja exactamente el espacio que queda bajo su pie, de modo que su borde inferior cae en la última línea de la rejilla, a la altura de la columna de al lado; ese espacio puede ser una fracción de línea. Actúa primero y se queda con todo el hueco, salvo que
closingBoxindique otra cosa:'last'deja que las palancas siguientes tomen antes las líneas enteras y solo da al recuadro lo que estas dejen, de modo que un recuadro que comenta el párrafo de encima queda cerca de él;'off'no mueve nunca el recuadro. -
Imágenes con zona segura. Una imagen cuyo recurso define una zona segura (
Resource.safeArea, la parte que siempre debe verse; véase Formato del documento › Zona segura) crece en líneas enteras de la rejilla si está colocada en línea en la columna corta, o como flotante en su cabeza o en su pie y solo sobre esa columna: el motor recorta lo que queda fuera de la zona segura para hacerla más alta, a todo su ancho. Una imagen más alta no deja ningún hueco en la página, así que esta palanca actúa antes de añadir ningún espacio. Un flotante que encabeza una columna en una página o una banda de cierre, cuyas columnas mantienen la cabeza a la misma altura, no crece. -
Espacio sobre los encabezados. Se añaden líneas enteras de la rejilla base al margen superior de los encabezados de la columna corta. Cuando hacen falta varias líneas y la columna contiene varios encabezados, las líneas se reparten por turnos, por orden de importancia, de modo que el encabezado más importante siempre recibe la mayor parte: 3 líneas sobre un
h2y unh3se convierten en +2 sobre elh2y +1 sobre elh3. Los encabezados situados justo al principio de una columna nunca reciben espacio adicional (las columnas siguen empezando en la parte superior de la página), y un encabezado al final nunca se desplaza hacia el pie de la columna. De este modo, un encabezado toma como muchomaxLinesBeforeLooselíneas (dos por defecto) antes de que se prueben las palancas siguientes, y las demás, hastamaxLinesPerHeading, solo cuando esas ya no tienen nada más que dar. -
Espacio tras el final de las listas. Se añade una línea de la rejilla donde termina una lista o una enumeración: el espacio tras una lista se lee con naturalidad. Hay un máximo por cada final de lista (
maxLinesAfterList, 1 por defecto). Después, se añade una línea de la rejilla bajo una fórmula en bloque o un recuadro tras los que continúa el texto (stretchAfterDisplays), y otra bajo una figura o una tabla que encabeza la columna (stretchAfterFloats,maxLinesAfterFloat). El texto que hay bajo la figura puede ser la continuación de un párrafo empezado en la página anterior; baja igualmente, tanto como la línea que las reglas de corte dejaron libre al pie de la columna (una línea que la regla de las viudas dejó vacía, un espacio entre párrafos sin sitio para el texto que sigue). Bajo una figura que ocupa varias columnas, la línea solo se añade si la reciben todas las columnas que quedan bajo la figura, de modo que sus cabezas siguen a la misma altura, o si deja su columna a ras (stretchAfterWideFloats). -
Párrafos aflojados. Como último recurso, se vuelven a cortar párrafos de la columna con una línea más (el
\looseness=+1de TeX), hastamaxLooseParagraphspárrafos (dos por defecto), con una línea más cada uno. El algoritmo vuelve a ejecutar Knuth-Plass pidiendo exactamente una línea adicional, y solo acepta el resultado si todas las líneas de la nueva composición se mantienen por debajo demaxWordSpacing: el gris tipográfico nunca supera el límite que ya hayas configurado. El motor prefiere los párrafos más largos de la columna, en los que el espacio adicional se reparte entre más espacios entre palabras y no se nota. RequiereoptimalLineBreaking(el valor por defecto); los párrafos partidos entre columnas quedan excluidos. Un párrafo que el arreglo de las líneas cortas compuso con una línea menos cuenta su línea adicional a partir de esa composición: puede recuperar la línea que le quitó el arreglo, pero solo con una composición sin línea corta y sin ninguna línea por encima demaxWordSpacing(hasta postext 1.4, podía recuperarla con una línea muy por encima del límite).Si el espacio entre palabras no basta para ganar la línea, el párrafo también puede recibir un poco de espaciado entre letras positivo, el arreglo clásico del cajista. El motor prueba primero la cantidad más pequeña (la mitad de
maxTrackingy despuésmaxTracking, en milésimas de em por carácter; 10 = 0,01 em por defecto) y se queda con la primera que gana la línea, siempre con el mismo límite para el espacio entre palabras. El espaciado entre letras se tiene en cuenta al medir las líneas del párrafo, y lo pintan todos los backends (letterSpacingen canvas,letter-spacingen CSS y espaciado entre caracteres en PDF). Se desactiva contrackParagraphs: false.
#Por qué el ajuste es local
Los saltos de columna están ligados a elementos: el elemento que abre la columna siguiente está ahí porque no cabía en el hueco. Por eso, desplazar el final de una columna hacia abajo, como mucho lo que mide su propio hueco, nunca pasa contenido a la columna o la página siguiente: cada columna se arregla en su sitio, sin efectos en cadena. Aun así, el motor lo comprueba empíricamente: vuelve a colocar el contenido con los ajustes propuestos (hasta 8 pasadas), mide el hueco total que queda y conserva siempre la mejor maquetación encontrada. Un párrafo que no consigue ganar su línea dentro del límite de espaciado, con ningún espaciado entre letras, se descarta, y se prueba con el siguiente candidato.
#Cuándo se respeta una columna corta
Una columna corta es a veces el resultado correcto, y el equilibrado sabe apartarse:
- La última columna de una página solo se equilibra si la página continúa de forma natural en la siguiente. Las páginas que terminan con un
:::pagebreak, con elbreakBeforede un encabezado o con una apertura de capítulo conservan su última columna corta: es normal que un capítulo termine a mitad de página. - La última página del documento nunca se equilibra.
- Una columna sin ningún punto que se pueda estirar (sin un encabezado que lo admita, un final de lista ni un párrafo que se pueda alargar) conserva su hueco antes que empeorar la tipografía.
- Un párrafo con un tabulador nunca se alarga ni recibe espaciado entre letras (véase Líneas con tabuladores).
const doc = buildDocument(content, {
headings: {
balancing: {
enabled: true, // por defecto
maxLinesPerHeading: 4, // máximo por encabezado
maxLinesBeforeLoose: 2, // líneas antes de las demás palancas
stretchAfterLists: true, // palanca 4
maxLinesAfterList: 1, // máximo por final de lista
stretchAfterDisplays: true, // bajo una fórmula o un recuadro
looseParagraphs: true, // palanca 5 — acotada por bodyText.maxWordSpacing
maxLooseParagraphs: 2, // párrafos aflojados por columna corta
trackParagraphs: true, // permite algo de tracking en un párrafo corrido
maxTracking: 10, // ‰ de eme por carácter (0,01 em)
closingBox: 'first', // palanca 1: 'first' | 'last' | 'off'
},
},
});En el Sandbox, estos ajustes están en la sección Títulos de Diseño → Títulos e índice (Equilibrar columnas, con Estirar tras listas, Estirar bajo figuras, Recuadro que cierra una columna y Párrafos aflojados dentro de ella). Activa la rejilla base (Diseño → Avanzado → Ayudas en pantalla) y mira la vista Canvas para ver el efecto: con el equilibrado desactivado, las columnas cortas terminan por encima de la última línea de la rejilla; con él activado, todas las columnas equilibrables terminan en la misma línea. Consulta la página de Configuración para ver la referencia completa de las opciones.
#Ejemplo completo
Una configuración completa con todos los ajustes de separación silábica y justificación:
import { buildDocument } from 'postext';
const vdt = buildDocument(content, {
bodyText: {
fontFamily: 'EB Garamond',
fontSize: { value: 9, unit: 'pt' },
textAlign: 'justify',
// Corte óptimo de líneas Knuth-Plass (por defecto: true)
optimalLineBreaking: true,
// Separación silábica
hyphenation: {
enabled: true,
locale: 'es',
},
// Límites de espaciado entre palabras (multiplicadores del ancho normal)
maxWordSpacing: 2, // los espacios se estiran hasta el 200%
minWordSpacing: 0.6, // los espacios se comprimen hasta el 60%
},
// Depuración: resaltar líneas con espaciado excesivo
debug: {
looseLineHighlight: {
enabled: true,
threshold: 2.5,
color: { hex: '#ff000040', model: 'hex' },
},
},
});Para ver la lista completa de opciones de configuración del texto base, consulta la página de Configuración. Para saber cómo usa estos ajustes el proceso de composición durante la medición del texto, consulta la página Arquitectura.