Saltar al contenido principal

Separación silábica y justificación

Actualizado: 2026-04-21|20 min|enes

La diferencia entre una composición amateur y una profesional vive en los espacios entre palabras.

Abre cualquier novela de bolsillo. El texto está justificado — los dos bordes de cada párrafo están perfectamente alineados. Pero mira más de cerca. Los espacios entre palabras son casi uniformes, línea tras línea. No hay ríos de espacio en blanco bajando por la página. No hay líneas donde dos palabras flotan con un océano enorme entre ellas. Conseguir esto 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 en la historia.

Postext lo resuelve con el mismo algoritmo que TeX lleva usando desde 1981: Knuth-Plass para ruptura óptima de líneas. Combinado con patrones de separación silábica de calidad TeX, límites configurables de espaciado entre palabras y un sistema visual de depuración para identificar líneas problemáticas, el motor produce texto justificado con estándares de publicación.

#El problema

Cuando el texto se configura con textAlign: 'justify', cada línea (excepto la última) debe estirarse o comprimirse para llenar exactamente el ancho de la columna. El motor distribuye la diferencia entre el ancho natural del contenido y el ancho de la columna a través de los espacios entre palabras de esa línea.

Si una línea tiene muchas palabras, cada espacio absorbe un ajuste mínimo — invisible para el lector. Pero si una línea tiene pocas palabras (porque una palabra larga forzó un corte prematuro), cada espacio debe estirarse dramáticamente. El resultado es una línea floja: una línea donde el espaciado entre palabras es tan amplio que rompe el ritmo de lectura y crea huecos visuales feos.

El problema opuesto también existe. Si el motor empaqueta demasiadas palabras en una línea, los espacios se comprimen por debajo de su ancho natural, produciendo una línea apretada donde las palabras se sienten amontonadas.

Un algoritmo de corte de líneas ingenuo — el tipo que usa CSS — toma decisiones línea por línea. Llena la línea actual con tantas palabras como sea posible, corta y sigue adelante. Este enfoque voraz de primer ajuste tiene una debilidad fundamental: no puede ver el futuro. Una decisión que parece óptima para la línea 5 puede forzar a la línea 6 a un corte terrible. Para cuando el algoritmo llega a la línea 6, es demasiado tarde — la línea 5 ya está confirmada.

#Knuth-Plass: ver el párrafo completo

El algoritmo Knuth-Plass, publicado por Donald Knuth y Michael Plass en 1981, adopta un enfoque radicalmente diferente. En lugar de cortar una línea a la vez, considera todas las formas posibles de cortar el párrafo entero y elige la combinación que minimiza la «fealdad» total en todas las líneas. Es el algoritmo que impulsa TeX, y es la razón por la que los documentos compuestos con TeX han sido el estándar de oro del texto justificado durante más de cuatro décadas.

#El modelo Caja-Cola-Penalización

Knuth-Plass no piensa en términos de palabras y espacios. Modela el texto como una secuencia de tres primitivas:

PrimitivaRepresentaComportamiento
Caja (Box)Una palabra o fragmento de textoTiene un ancho fijo. No se puede estirar ni comprimir. No se puede romper.
Cola (Glue)Espacio entre palabrasTiene un ancho natural, una capacidad de estiramiento y una capacidad de compresión. El motor puede ajustar la cola dentro de estos límites para llenar la línea.
Penalización (Penalty)Un punto de ruptura potencialTiene un coste. Penalización baja = ruptura barata. Penalización alta = ruptura cara. Una penalización marcada (flagged) indica un punto de separación silábica (añade un guión visible si se usa).

Un párrafo se convierte en una secuencia como:

[caja "El"] [cola] [caja "rápido"] [cola] [caja "zorro"] [cola] [caja "marrón"]
[penalización -∞]  ← ruptura forzada al final del párrafo

Cuando la separación silábica está activa, 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 es más barato que producir una línea floja.

Primitivas caja-cola-penalizaciónClave visual de las tres primitivas: caja para un fragmento de palabra, cola para el espacio entre palabras, penalización para un posible punto de ruptura. El párrafo de ejemplo muestra cómo un punto de separación silábica se convierte en una penalización marcada.Cajapalabra fijaColaespacio elásticoPenalizaciónposible rupturaEjemplo: “El arte de la tipografía es antiguo.”The·art·of·typog-raphy·is·old.breakpoints ⇄ glue and penalty positions
Cada párrafo se convierte en una secuencia de cajas, colas y penalizaciones.

#Cómo encuentra el óptimo

El algoritmo utiliza programación dinámica. Mantiene un conjunto de nodos activos — puntos de ruptura potenciales que podrían iniciar nuevas líneas — y evalúa cada ruptura factible desde cada nodo activo. Para cada ruptura candidata, calcula:

  1. Ratio de ajuste (r): cuánto necesita estirarse o comprimirse la cola en esta línea. r = 0 significa que la línea encaja perfectamente. r > 0 significa estiramiento (floja). r < 0 significa compresión (apretada).

  2. Fealdad (badness): una medida de lo desigual que es el espaciado, calculada como 100 × |r|³. El crecimiento cúbico significa que una línea ligeramente floja es tolerable, pero una muy floja se penaliza severamente. Una línea con r = 2 tiene fealdad 800; una con r = 0.5 tiene fealdad 12.

  3. Deméritos (demerits): el coste total de romper aquí, combinando fealdad, cualquier penalización en el punto de ruptura, y dos penalizaciones adicionales:

    • Demérito por guiones consecutivos (por defecto 3000): penaliza dos líneas con guión seguidas, porque los guiones apilados son visualmente molestos.
    • Demérito por clase de ajuste (por defecto 100): penaliza líneas adyacentes de tensión muy diferente. Si una línea apretada está junto a una muy floja, el contraste resulta chocante.
  4. 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). Las líneas adyacentes con clases de ajuste separadas por más de un paso incurren en el demérito de clase.

Los cuatro costes se suman en un único número por corte candidato, y el algoritmo elige la secuencia con el coste total más bajo en todo el párrafo — ahí está toda su ventaja sobre el voraz.

Fealdad en función del ratio de ajusteLa fealdad crece como 100 por el valor absoluto de r al cubo. Estiramientos o compresiones pequeños son baratos, pero los extremos se vuelven extremadamente costosos, y por eso el algoritmo los evita.r = 0apretada (r < 0)floja (r > 0)rfealdad0badness(r) = 100 · |r|³
La fealdad crece cúbicamente: algo desigual se tolera, muy desigual se penaliza duro.
Clases de ajusteCada línea se clasifica como apretada, normal, floja o muy floja según su ratio de ajuste. Las líneas adyacentes cuyas clases difieren en más de un paso se penalizan.Apretadar < -0.5comprimidaNormal-0.5 ≤ r < 0.5justoFloja0.5 ≤ r < 1.0algo aireadaMuy flojar ≥ 1.0muy aireadaEl demérito de clase se aplica cuando líneas vecinas distan en más de una clase.
Las líneas contiguas separadas por más de una clase incurren en el demérito de clase.

El algoritmo traza hacia atrás a través de los nodos activos para encontrar la ruta con los deméritos totales más bajos — el conjunto de puntos de ruptura globalmente óptimo para todo el párrafo.

Penalizaciones de huérfanas, viudas y runts

Postext amplía el conjunto estándar de deméritos con tres penalizaciones editoriales que alejan al divisor de líneas de conjuntos de cortes que producirían finales de párrafo visualmente pobres:

  • La penalización de huérfana se aplica cuando elegir este corte dejaría menos de orphanMinLines líneas en la parte superior de la siguiente columna. El valor por defecto de orphanPenalty es 1000.
  • La penalización de viuda se aplica cuando el corte dejaría menos de widowMinLines líneas al final de la columna actual. El valor por defecto de widowPenalty es 1000.
  • La penalización de runt se aplica cuando la última línea del párrafo sería más corta que aproximadamente runtMinCharacters × normalSpaceWidth píxeles. El valor por defecto de runtPenalty es 1000. A diferencia de huérfana/viuda (que suman linealmente al demerit del corte), el runt se inyecta como badness equivalente dentro de la fórmula cuadrática de Knuth–Plass, de modo que compite en la misma escala que el badness de línea (que satura en 10000) en lugar de quedar aplastado por él.

Las tres se suman a los deméritos del nodo candidato antes de que el algoritmo seleccione la ruta globalmente óptima. Como viven dentro de la misma optimización, el solver puede sacrificar una línea ligeramente más holgada a cambio de eliminar una viuda, y preferirá de forma natural conjuntos de cortes que eviten las tres siempre que sea posible. Ajusta el trade-off mediante orphanPenalty, widowPenalty o runtPenalty; ajustar cualquiera a 0 desactiva esa regla. Los ítems de lista se apuntan mediante avoidOrphansInLists, avoidWidowsInLists y avoidRuntsInLists (todos a true por defecto).

#Por qué importa

Primer ajuste voraz frente a Knuth-Plass óptimoIlustración lado a lado: el algoritmo voraz toma la primera línea que cabe y envenena las líneas siguientes, produciendo párrafos desiguales con espaciado irregular. Knuth-Plass evalúa el párrafo completo globalmente y produce líneas parejas.Primer ajuste vorazDecide línea a línea, sin mirar adelante✗ Un mal corte envenena la siguiente línea✗ Espaciado desigual en el párrafo✗ Ríos de espacio en blanco✗ Sin modelo de coste entre líneas adyacentes= Lo que hace CSSKnuth-Plass óptimoEvalúa cada conjunto de cortes posible✓ Deméritos globalmente mínimos✓ Espaciado parejo en todo el párrafo✓ Penalización de guiones consecutivos✓ Suavidad de clases de aptitud= Lo que hace Postext
Una decisión voraz local pierde donde un plan globalmente óptimo gana.

La diferencia práctica es visible. En una composición voraz, encontrarás párrafos donde una línea es notablemente más floja que sus vecinas — y si miras con cuidado, verás que ocurrió porque la línea anterior tomó una palabra de más. Knuth-Plass evita esto intercambiando una línea actual ligeramente peor por una siguiente mucho mejor, porque puede ver las consecuencias.

#Implementación de Postext

Postext implementa el algoritmo Knuth-Plass completo en knuthPlass.ts (aproximadamente 860 líneas). Dos caminos adaptadores convierten el texto al modelo caja-cola-penalización:

  • Camino de texto plano: usa @chenglou/pretext para medición de texto sin DOM. Pretext proporciona anchos de segmento y anchos de guiones discrecionales; Postext los convierte en ítems KP.
  • Camino de texto enriquecido: maneja tramos en negrita y cursiva usando medición basada en Canvas. Cada token con estilo se convierte en una o más cajas, con puntos de separación silábica insertados como penalizaciones.

Ambos caminos calculan el justifiedSpaceRatio por línea — el ancho real del espacio dividido entre el ancho natural — que alimenta el sistema de depuración de líneas flojas descrito más adelante.

Comportamiento de respaldo: si Knuth-Plass no produce rupturas válidas (lo que puede ocurrir con columnas extremadamente estrechas o palabras más largas que el ancho de columna), el motor recurre al corte voraz de Pretext (layoutNextLine()). Esto garantiza que la composición siempre se complete.

#Separación silábica

Texto justificado con y sin separación silábicaLa columna izquierda no usa separación silábica: los anchos de línea varían mucho porque el motor solo puede mover palabras completas. La columna derecha usa separación: los anchos son casi uniformes y una palabra se parte con guion.Sin separación silábicaLíneas desiguales, huecos largos, ríos visibles.Con separación silábica-Líneas parejas, un guion absorbe la variación.
La separación silábica reduce drásticamente la variación de espaciado en texto justificado.

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 mover una palabra a la línea siguiente — lo que muchas veces solo desplaza el problema. La separación silábica le da al motor un conjunto mucho mayor de puntos de ruptura, mejorando drásticamente la calidad del texto justificado.

#Patrones de calidad TeX

Postext usa Hypher (hypher v0.2.5) para la separación silábica, alimentado por patrones de separación TeX/Liang. Son los mismos patrones que TeX usa desde 1983 — una representación compacta de reglas de límites silábicos derivada por 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, al superponerse sobre una palabra, indican dónde se permiten las rupturas (números impares) y dónde se prohíben (números pares). Los parámetros leftmin y rightmin en el archivo de patrones de cada idioma aseguran un número mínimo de caracteres antes y después de cualquier punto de ruptura. Para el inglés (en-us), típicamente son 2 y 3 respectivamente — una palabra debe tener al menos 2 caracteres antes del guión y 3 después.

#Idiomas soportados

Código de idiomaIdioma
'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 de forma perezosa y se cachean — la primera llamada para un idioma paga el coste de inicialización; las llamadas siguientes son instantáneas. Si se pasa un idioma desconocido, el motor recurre a en-us.

#Por qué Hypher (y no un algoritmo propio)

El sistema original de separación silábica de Postext usaba una heurística propia basada en vocales: detectaba límites silábicos encontrando grupos de vocales, prefijos comunes (over-, under-, inter-) y sufijos comunes (-tion, -ment, -sion). Era simple y rápida, pero fundamentalmente limitada:

AspectoHeurística propiaHypher (patrones TeX)
PrecisiónBuena para palabras comunes, poco fiable para las inusuales. La detección por vocales omite muchos puntos de ruptura 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áticaHabí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 TeX. Añadir un idioma es añadir un import.
Estándar de la industriaNo es un estándar reconocido. Sin herramientas ni soporte comunitario.Los mismos patrones usados por TeX, LibreOffice, Firefox, Chrome y prácticamente todos los sistemas de composición profesional.
MantenimientoCada caso especial es un bug que hay que arreglar manualmente.Archivos de patrones mantenidos por la comunidad. Las correcciones vienen del upstream.
Tamaño del bundle~170 líneas, sin dependencias.El núcleo de Hypher son ~3 KB. Cada archivo de patrones de idioma añade 20–80 KB (gzipped: 5–20 KB). Los ocho idiomas incluidos suman aproximadamente 300 KB (gzipped: ~80 KB).
RendimientoMuy rápida (escaneo simple de cadenas).Rápida (búsqueda en trie por carácter). Despreciable en la práctica — la separación silábica nunca es el cuello de botella.

El compromiso es claro: un bundle más grande a cambio de una corrección drásticamente mejor y cero carga de mantenimiento. Para un motor de composición orientado a calidad de publicación, la corrección gana. Una sola separación silábica incorrecta en un libro impreso es más cara que unos kilobytes extra de patrones.

#Cómo se integra la separación silábica con Knuth-Plass

Antes de que el texto entre en el algoritmo Knuth-Plass, el motor lo preprocesa con Hypher, insertando guiones blandos (Unicode \u00AD) en cada punto de ruptura legal. Estos caracteres invisibles se mapean luego a penalizaciones KP con un coste de 50 y flagged: true.

El algoritmo trata las rupturas por separación silábica como una opción más a evaluar junto con los límites naturales de palabras (donde la cola permite una ruptura). Si usar un guión produce unos deméritos totales menores que dejar la línea floja, el algoritmo toma el guión. Si no, deja la palabra intacta.

El demérito por guiones consecutivos (por defecto 3000) asegura que el algoritmo evite firmemente colocar guiones en dos líneas adyacentes — una convención tipográfica que prácticamente todas las guías de estilo imponen.

La separación silábica solo se aplica cuando bodyText.hyphenation.enabled es true y bodyText.textAlign es 'justify'. El texto alineado a la izquierda no se beneficia de la separación silábica porque el borde derecho es intencionadamente irregular.

#Límites de espaciado entre palabras

El modelo de cola le da al motor límites explícitos sobre cuánto pueden estirarse o comprimirse los espacios entre palabras. Estos límites se controlan con dos propiedades de configuración en bodyText:

PropiedadPor defectoDescripción
maxWordSpacing2Límite superior del espaciado 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.
minWordSpacing0.6Límite inferior del espaciado 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 traducen directamente a los valores de stretch y shrink de la cola en el modelo 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)

Límites más estrictos (por ej., maxWordSpacing: 1.2) producen un espaciado más uniforme pero dan menos margen al algoritmo, lo que puede resultar en más separación silábica o, en casos extremos, desbordamiento. Límites más amplios (por ej., maxWordSpacing: 2.5) dan más flexibilidad al algoritmo pero permiten un espaciado visiblemente desigual en algunas líneas.

Los valores por defecto de 2 y 0.6 priorizan la flexibilidad del algoritmo — dan a Knuth-Plass margen suficiente para evitar desbordamientos, separaciones silábicas y runts en columnas estrechas, manteniéndose dentro del rango que la literatura tipográfica considera aceptable.

#Ruptura óptima vs. voraz

La propiedad bodyText.optimalLineBreaking (por defecto: true) controla qué algoritmo de ruptura de líneas usa el motor:

  • true: Algoritmo de programación dinámica Knuth-Plass. Evalúa todos los conjuntos de ruptura posibles y elige el globalmente óptimo. Es la configuración recomendada para cualquier texto justificado.
  • false: Voraz de primer ajuste, alimentado por layoutNextLine() de Pretext. Más rápido pero produce resultados de menor calidad. Úsalo solo cuando el rendimiento importa más que la calidad tipográfica (por ej., vista previa en tiempo real con conteos de caracteres muy altos).

Cuando Knuth-Plass está activo y no produce rupturas válidas (algo que puede ocurrir con columnas extremadamente estrechas o palabras más largas que el ancho de columna), el motor recurre automáticamente a la ruptura voraz para ese párrafo.

#Depuración de líneas flojas

Incluso con Knuth-Plass y separación silábica, algunas líneas serán más flojas de lo ideal — especialmente en columnas estrechas con palabras largas, o en idiomas con pocas oportunidades de separación silábica. La función de depuración resaltado de líneas flojas te ayuda a encontrar estas líneas problemáticas al instante.

#Cómo funciona

Cada línea en el Árbol de Documento Virtual lleva un justifiedSpaceRatio — la proporción entre el ancho real del espacio justificado y el ancho natural del espacio de la fuente. Un valor de 1.0 significa que los espacios están en su ancho natural. Un valor de 2.5 significa que los espacios son 2.5 veces más anchos de lo normal.

Cuando debug.looseLineHighlight.enabled es true, el renderizador pinta una capa semitransparente sobre cada línea cuyo justifiedSpaceRatio supera el threshold configurado. 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. Este es un listón deliberadamente alto; las líneas así de flojas son problemas tipográficos genuinos.

#Configuración

El resaltado de líneas flojas forma parte de la sección debug de PostextConfig:

PropiedadTipoPor defectoDescripción
looseLineHighlight.enabledbooleanfalseSi se resaltan las líneas flojas.
looseLineHighlight.colorColorValue#ff000040Color de la capa de resaltado. El valor por defecto es un rojo semitransparente.
looseLineHighlight.thresholdnumber3Multiplicador del ancho normal del espacio por encima del cual una línea se considera floja. Valores más bajos capturan más líneas; valores más altos resaltan solo las peores.
debug: {
  looseLineHighlight: {
    enabled: true,
    threshold: 2.5,
    color: { hex: '#ff660040', model: 'hex' },
  },
}

#Interpretar los resultados

Cuando activas el resaltado de líneas flojas y ves bandas rojas en ciertas líneas, significa que el motor no pudo encontrar una forma de componer esas líneas sin un espaciado excesivo. Empieza por arriba: las causas están ordenadas de más probable a menos.

CausaSolución
La columna es demasiado estrecha para el tamaño de fuenteAumenta el ancho de columna, reduce el tamaño de fuente o cambia a una composición de columna única.
Palabras largas con pocos puntos de separaciónVerifica que la separación silábica esté activada y que el idioma correcto esté configurado. Algunos términos técnicos o nombres propios no tienen puntos de ruptura válidos.
La separación silábica está desactivadaActiva bodyText.hyphenation.enabled. La justificación sin separación silábica es casi siempre peor.
Los límites de espaciado son demasiado estrictosAumenta maxWordSpacing ligeramente (por ej., de 2 a 2.5). Esto le da más espacio al algoritmo.
El idioma tiene palabras compuestas largas (por ej., alemán)Asegúrate de que el idioma correcto esté configurado. Los patrones de separación silábica del alemán manejan bien las palabras compuestas, pero solo si el motor sabe que es alemán.

#Ejemplo completo

Una configuración completa mostrando 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',
 
    // Ruptura óptima 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 la lista completa de opciones de configuración del cuerpo de texto, consulta la página de Configuración. Para ver cómo el pipeline de composición usa estos ajustes durante la medición de texto, consulta la página de Arquitectura.