Si has trabajado con React, ya conoces el truco fundamental. React construye un DOM virtual en memoria, lo compara con el anterior y solo entonces toca el DOM real del navegador. Postext hace exactamente lo mismo, pero en lugar de componentes de interfaz, construye un árbol de páginas, columnas, bloques de texto y cajas delimitadoras. Toda la geometría de un documento multipágina y multicolumna, calculada antes de renderizar un solo píxel. Cada párrafo, encabezado, imagen, nota al pie y cita destacada colocados en coordenadas exactas, respetando reglas tipográficas centenarias que CSS simplemente no puede expresar.
Todo esto es posible gracias a @chenglou/pretext, una librería de medición de texto sin DOM entre 300 y 600 veces más rápida que el reflow del navegador. Si quieres conocer la historia detrás del proyecto (una década de intentos fallidos, el cuello de botella que los frenaba a todos y la librería que finalmente lo eliminó), consulta la Introducción.
#La idea central
Imagina un documento de 74 páginas a dos columnas. Un informe anual, quizá, o un libro de texto densamente ilustrado. Se lo entregas a Postext y el motor construye toda la composición en memoria: cada página, cada columna, la posición exacta y las dimensiones en píxeles de cada párrafo. ¿Quieres saber qué hay en la página 72, columna 2? La respuesta ya está ahí. Sin renderizar nada. El motor ya ha decidido dónde partir cada párrafo, dónde colocar cada imagen, cómo evitar viudas y huérfanas, y cómo alinear las líneas base entre columnas adyacentes.
Y aquí viene lo importante.
Las reglas tipográficas son profunda y desesperantemente interdependientes. Corriges una viuda en la página 5 -- esa línea solitaria varada al final de una columna -- retrayéndola a la columna anterior. Perfecto. Pero ese cambio acorta la columna de la página 5, lo que desplaza contenido hacia adelante, lo que podría crear una huérfana en la página 6. Una primera línea empujada a una nueva columna, desconectada de su párrafo. Para siquiera detectar que has creado un nuevo problema, necesitas la composición completa del documento disponible para inspección. Y para corregirlo sin crear otro problema en otro sitio, necesitas poder ajustar, re-medir y re-verificar todo.
Esa es la filosofía de "calcular todo primero, renderizar después". No es un truco de rendimiento. Es la única forma de aplicar las decenas de reglas tipográficas interconectadas que los tipógrafos profesionales llevan siglos usando.
#Conceptos fundamentales
Glosario rápido. El resto del documento asume estos términos — vuelve aquí cuando alguno se haya desdibujado.
| Término | Definición |
|---|---|
| VDT | Virtual Document Tree (Árbol Virtual del Documento). Estructura de datos mutable que representa el documento completo en memoria: páginas, columnas, bloques, segmentos en línea y cajas delimitadoras. Análogo al DOM virtual, pero aplicado a la geometría de composición del documento. |
| Page | Área rectangular de tamaño fijo. El motor trabaja con páginas desde el inicio. Un documento es una secuencia ordenada de páginas. |
| Column | Subdivisión vertical de una página. Las columnas tienen un ancho fijo y una altura máxima. El texto fluye de una columna a la siguiente, y luego a la página siguiente. |
| Block | Unidad de contenido que ocupa espacio vertical en una columna: párrafo, encabezado, imagen, tabla, cita, cita destacada o área de notas al pie. |
| Line | Línea de texto medida dentro de un bloque, generada por Pretext. Cada línea tiene una caja delimitadora y una posición de línea base. |
| Bounding Box | x, y, width, height en píxeles, relativo al origen de la página. Cada nodo del VDT contiene una. |
| Resource | Elemento no textual (imagen, tabla, figura, cita destacada) referenciado desde el markdown. Definido por PostextResource. |
| Note | Nota al pie, nota final o nota al margen. Definida por PostextNote. |
| Backend | Implementación unificada de medición de texto y renderizado de salida para un destino específico. Hoy se distribuyen tres: canvas (previsualización bitmap), HTML (lectura en pantalla basada en DOM) y PDF (salida lista para imprimir a través de postext-pdf). |
| Pass | Una etapa del pipeline de composición. Cada pasada lee y modifica el VDT con una única responsabilidad. |
| Convergence Loop | El bucle externo que re-ejecuta las pasadas de composición cuando pasadas posteriores invalidan decisiones anteriores. Limitado a un máximo de 5 iteraciones. |
#Arquitectura del sistema
Este es el recorrido de tu contenido a través del motor:
- El Parser lee el markdown enriquecido y la configuración, construyendo el VDT inicial -- un árbol de bloques tipados sin posiciones todavía, solo contenido y estructura
- Las pasadas de composición toman el control, mutando el VDT en secuencia: midiendo texto vía Pretext, fluyendo bloques hacia páginas y columnas, refinando la tipografía hasta alcanzar estándares profesionales
- El bucle de convergencia vigila los problemas -- cuando una pasada posterior (digamos, corregir una viuda) invalida una decisión anterior (digamos, las alturas de columna), el motor vuelve atrás y re-ejecuta desde el punto afectado. Hasta 5 iteraciones, hasta que todo se estabiliza
- El VDT final es la geometría completa de la composición: cada elemento conoce su número de página, su columna asignada, su posición y su caja delimitadora. El documento está completamente "compuesto" antes de que ocurra ningún renderizado
- Un backend recorre el VDT terminado y lo renderiza al formato destino -- un bitmap rasterizado sobre canvas, un árbol DOM de elementos HTML posicionados, o un documento PDF con fuentes incrustadas. El mismo VDT alimenta a los tres; elegir backend es puramente una decisión de salida
#Capa de entrada
#Modelo de contenido
El modelo de contenido es tanto una filosofía como una estructura de datos. Tú describes qué decir, no cómo componerlo. Las decisiones de composición las toma el motor.
// packages/postext/src/types.ts
interface PostextContent {
markdown: string; // enriched markdown with reference markers
resources?: PostextResource[]; // images, tables, figures, pull quotes
notes?: PostextNote[]; // footnotes, endnotes, margin notes
}Los recursos llevan metadatos visuales (dimensiones, pies de foto, texto alternativo) y se referencian desde el markdown por ID. Las notas llevan contenido y un estilo de marcador, referenciados desde posiciones en línea dentro del markdown.
Esta separación es una decisión de diseño deliberada, y tiene más importancia de lo que parece. El markdown es dueño del orden de lectura y la estructura semántica -- qué va primero, qué es un encabezado, dónde se referencia una nota al pie. Los arrays de recursos y notas son dueños de los datos visuales -- dimensiones de imagen, texto de pies de foto, contenido de notas. Al mantenerlos separados, el mismo markdown puede componerse de formas completamente distintas simplemente cambiando la configuración. Una maquetación académica a dos columnas y un artículo de blog a columna única pueden compartir el mismo contenido fuente. Y el motor puede tomar decisiones de colocación -- como diferir una imagen a la siguiente columna porque aquí no cabe -- sin tocar jamás tu contenido original.
// Example: a simple article with an image and a footnote
const content: PostextContent = {
markdown: `
# The Art of Typography
The history of typography begins with Gutenberg's
movable type[^1]. His invention transformed the
production of books.
![fig:printing-press]
The technique spread rapidly across Europe, reaching
Italy by 1465 and France by 1470.
`,
resources: [
{
id: 'fig:printing-press',
type: 'figure',
src: '/images/gutenberg-press.jpg',
alt: 'Reconstruction of Gutenberg\'s printing press',
caption: 'Reconstrucción de la imprenta original.',
width: 600,
height: 400,
},
],
notes: [
{
id: '1',
type: 'footnote',
content: 'Johannes Gutenberg, c. 1400–1468, Mainz, Germany.',
},
],
};Fíjate en que ![fig:printing-press] en el markdown es solo un marcador de referencia -- un nombre, nada más. El motor lo resuelve contra el array resources por ID, obtiene las dimensiones de la imagen y decide dónde colocarla según la PlacementStrategy configurada. Quizá aterriza justo ahí. Quizá el motor la difiere a la parte superior de la siguiente columna porque la actual está casi llena. Lo mismo ocurre con [^1] -- el motor lo resuelve contra el array notes y coloca la nota al pie en la parte inferior de la columna, o de la página, o al final de la sección, dependiendo de ReferenceConfig. El autor nunca tiene que pensar en la colocación. De eso se encarga el motor.
#Configuración
Cada aspecto del pipeline de composición está controlado por PostextConfig:
| Config | Controla | Usado en |
|---|---|---|
ColumnConfig | Número de columnas, ancho del medianil, filetes de columna, indicador de equilibrado | Pasada 3, Pasada 6 |
TypographyConfig | Controles tipográficos heredados (espaciado alrededor de figuras, optimización de bandera). Los controles por campo de viudas/huérfanas/runts/cohesión viven ahora en BodyTextConfig y HeadingsConfig — véase la página de Configuración. | Pasada 5, Pasada 7 |
ResourcePlacementConfig | Estrategia de colocación por defecto, colocación diferida, preservación de la relación de aspecto | Pasada 4 |
ReferenceConfig | Ubicación de notas al pie, estilo de marcador, numeración de figuras/tablas, notas al margen | Pasada 1, Pasada 3 |
PostextSectionOverride | Sobrecargas de reglas por sección mediante selectores | Pasada 1 |
#Estrategia de análisis
El análisis es deliberadamente el paso más simple de todo el pipeline. El markdown entra, se parsea en un AST, y cada nodo se convierte en un VDTBlock. Las referencias a recursos y notas se resuelven contra los arrays resources[] y notes[] por ID. La salida es una lista plana de bloques tipados y con contenido, pero sin asignación de página, ni columna, ni posición.
Piensa en ello como un manifiesto: "hay un encabezado, luego un párrafo de 200 palabras, luego una referencia a una figura, luego otro párrafo." Sin mediciones. Sin posicionamiento. Sin ninguna decisión de composición. El trabajo pesado empieza en la Pasada 2.
#Árbol Virtual del Documento (VDT)
Imagina que le pides a un tipógrafo profesional que componga un libro entero, pero en lugar de entregarte páginas impresas, te entrega una hoja de cálculo. Cada fila es un elemento. Cada celda es una medida precisa: "el encabezado está en (40, 30), el primer párrafo empieza en (40, 78) y mide 144px de alto, la imagen va en la parte superior de la columna 2 en la página 3..." Esa hoja de cálculo es el VDT.
El Árbol Virtual del Documento es la estructura de datos central de Postext -- un árbol mutable, modificado in-place, que representa cada página, columna, bloque y línea, cada uno con una caja delimitadora precisa. Una vez que el pipeline de composición converge, el VDT es la respuesta. Puedes consultar "¿qué hay en la página 72, columna 2?" sin renderizar un solo píxel.
#Por qué mutable
Es el mismo enfoque que usan los pipelines de renderizado de motores de juegos, donde un estado mutable compartido del mundo es actualizado por sistemas sucesivos en un bucle cerrado. Y por la misma razón.
Los árboles inmutables (como el DOM virtual de React) crean objetos nuevos con cada cambio. Eso está bien para una interfaz con unos cientos de componentes. Pero en un bucle de convergencia que puede ejecutarse hasta 5 iteraciones a lo largo de 7 pasadas, tocando potencialmente miles de bloques, la presión de asignación de memoria y las pausas del recolector de basura se convierten en un problema muy real. El VDT usa mutación in-place con un patrón de dirty flags: las pasadas marcan nodos como sucios, y las pasadas posteriores saben exactamente qué nodos re-examinar. El motor recuerda qué cambió para no rehacer trabajo válido.
#Estructura
#Definiciones de tipos
// The root of the Virtual Document Tree
interface VDTDocument {
pages: VDTPage[];
config: PostextConfig;
baselineGrid: number; // baseline increment in px (e.g. 24 for 16px/1.5)
converged: boolean;
iterationCount: number;
}
// A physical page
interface VDTPage {
index: number;
width: number;
height: number;
columns: VDTColumn[];
header?: VDTBlock; // running header
footer?: VDTBlock; // running footer / page number
marginNotes: VDTBlock[];
footnoteArea?: VDTFootnoteArea;
}
// A column within a page
interface VDTColumn {
index: number;
bbox: BoundingBox; // position within the page
blocks: VDTBlock[];
availableHeight: number; // remaining vertical space
baselineOffset: number; // current baseline y-position
}
// A content block (paragraph, heading, image, etc.)
interface VDTBlock {
id: string;
type: 'paragraph' | 'heading' | 'resource' | 'blockquote'
| 'listItem' | 'footnoteRef';
bbox: BoundingBox;
lines?: VDTLine[]; // for text blocks (populated by Pass 2)
resource?: PostextResource; // for resource blocks
pageIndex: number;
columnIndex: number;
dirty: boolean; // needs re-layout
snappedToGrid: boolean; // baseline aligned to grid
}
// A measured line of text
interface VDTLine {
text: string;
bbox: BoundingBox;
baseline: number; // y-position of the text baseline
hyphenated: boolean; // line ends with a hyphen
}
// Bounding box — all values in px, relative to page origin
interface BoundingBox {
x: number;
y: number;
width: number;
height: number;
}
// Footnote area at the bottom of a page
interface VDTFootnoteArea {
bbox: BoundingBox;
notes: VDTBlock[];
separator: boolean; // draw a rule above footnotes
}#Seguimiento de cambios (dirty flags)
El seguimiento de cambios es la forma en que el motor evita rehacer trabajo que ya hizo correctamente. Cuando una pasada mueve o redimensiona un bloque, establece dirty = true en ese bloque y en todos los bloques posteriores de la misma columna -- porque las posiciones de todos dependen del bloque que cambió. El bucle de convergencia puede entonces saltarse los subárboles sin cambios por completo.
Un ejemplo concreto. La Pasada 5 inserta un guión en un párrafo de la página 12, provocando que pierda una línea de altura. Ese párrafo se marca como sucio. También todos los bloques por debajo de él en la misma columna -- todos necesitan desplazarse hacia arriba una línea. ¿Pero los bloques de la página 11 y anteriores? Intactos. Las pasadas se los saltan completamente en la siguiente iteración.
El dirty flag sirve también como señal de convergencia: si no hay bloques sucios después de las pasadas 5--7, la composición ha convergido y el motor deja de iterar. Listo.
#Pipeline de composición
Siete pasadas, cada una con un solo trabajo. Ese es todo el pipeline de composición.
El diseño está tomado de los pipelines de renderizado de motores de juegos -- pasada de sombras, pasada de iluminación, pasada de post-procesado -- donde cada sistema lee y muta un estado del mundo compartido y confía en que los sistemas anteriores hicieron su parte. Esto hace que cada pasada individual sea fácil de entender, probar y optimizar de forma aislada. Puedes medir el rendimiento de la Pasada 5 sin pensar en la Pasada 3.
La diferencia clave respecto a un motor de juegos es que un juego renderiza cada fotograma una vez y pasa al siguiente. Postext no puede permitirse eso. Las decisiones tipográficas son profundamente interdependientes -- corregir una viuda puede cambiar las alturas de las columnas, lo que afecta al equilibrado, lo que puede crear una nueva huérfana -- así que el pipeline puede necesitar iterar. Las pasadas 3--7 se ejecutan dentro de un bucle de convergencia, iterando hasta 5 veces hasta que la composición se estabiliza en un resultado final.
#Pasada 1: Estructuración del contenido
- Entrada:
PostextContentsin procesar - Acción: Analizar el markdown en un AST, resolver las referencias a recursos y notas contra
resources[]ynotes[]por ID, crear los nodosVDTBlockiniciales - Salida:
VDTBlock[]plano (tipado y con contenido, pero sin asignación de página ni columna) - Se ejecuta una sola vez (no forma parte del bucle de convergencia)
#Pasada 2: Medición de texto
- Entrada:
VDTBlock[]con contenido de texto - Acción: Para cada bloque de texto, llamar a
prepare()de Pretext para analizar el texto, y luego alayout()para calcular la altura al ancho de columna objetivo. Almacenar lasVDTLine[]medidas y la altura total en cada bloque - Detalle clave: Usa
layoutNextLine()de Pretext para el flujo de texto alrededor de obstáculos (cada línea puede tener un ancho disponible diferente cuando un recurso está flotado junto a ella) - Salida: Cada bloque de texto tiene dimensiones precisas en píxeles
- Se re-ejecuta cuando: Cambian los anchos de columna o el contenido del texto (por ejemplo, al insertar separación silábica)
Aquí es donde Pretext demuestra su valor. La llamada a prepare() es la parte costosa -- analiza el texto usando el motor de fuentes del canvas y cachea el resultado. ¿Pero la llamada a layout()? Aritmética pura, prácticamente gratis. Esa división lo cambia todo. Una vez que el texto está preparado, el motor puede re-componer a diferentes anchos -- probando configuraciones de columnas, fluyendo texto alrededor de un obstáculo, comprobando qué pasa si un párrafo gana un guión -- todo con un coste insignificante. Preparar una vez, componer tantas veces como haga falta.
// Simplified: how Pass 2 uses pretext internally
const prepared = prepare(paragraphText, '16px/1.5 Inter');
const { height } = layout(prepared, columnWidth, 24); // 24px line-height
// => "This paragraph is 168px tall at 320px column width — that's 7 lines."#Pasada 3: Colocación en páginas y columnas
- Entrada: Bloques medidos
- Acción: Fluir bloques en páginas y columnas secuencialmente. Crear nodos
VDTPageyVDTColumn. RegistraravailableHeightpor columna. Cuando un bloque no cabe, avanzar a la siguiente columna o página - Estrategia: Colocación greedy de primer ajuste. Los saltos de columna y página siguen la asignación válida más simple
- Salida: Cada bloque tiene
pageIndex,columnIndexybboxasignados
Este es el momento en que el VDT se convierte en un documento de verdad. Antes de esta pasada, los bloques son solo una lista plana con dimensiones pero sin dirección. La Pasada 3 los recorre y asigna cada uno a una página y columna, como verter agua en una cuadrícula de recipientes: llenar la columna 1 hasta que se desborde, pasar a la columna 2, y cuando la página esté llena empezar una nueva.
Antes de que aterrice ningún bloque de contenido, la pasada reserva espacio para elementos estructurales -- áreas de notas al pie en la parte inferior de las páginas (según ReferenceConfig.footnotes.placement), cabeceras, pies de página y columnas de margen. Estas reservas reducen el availableHeight de cada columna, así que cuando los bloques de contenido empiezan a fluir, el motor ya sabe exactamente cuánto espacio hay disponible.
#Pasada 4: Colocación de recursos
- Entrada: VDT con bloques colocados en columnas
- Acción: Colocar recursos según su
PlacementStrategy:
| Estrategia | Comportamiento |
|---|---|
topOfColumn | El recurso se coloca en la parte superior de la columna actual o siguiente |
inline | El recurso aparece en el flujo de texto en el punto de referencia |
floatLeft | El recurso flota a la izquierda; el texto lo envuelve usando layoutNextLine() |
floatRight | El recurso flota a la derecha; el texto lo envuelve usando layoutNextLine() |
fullWidthBreak | El recurso ocupa todo el ancho de la página, interrumpiendo el flujo de columnas |
margin | El recurso se coloca en una columna de margen junto al párrafo que lo referencia |
- Colocación diferida: Si un recurso no cabe en su punto de referencia, el motor busca la siguiente posición viable (controlado por
ResourcePlacementConfig.deferPlacement) - Complejidad clave: Los recursos pueden desplazar bloques de texto, lo que puede requerir una nueva medición a anchos efectivos diferentes
- Salida: Recursos posicionados, bloques de texto circundantes ajustados
La colocación de recursos es donde las cosas se ponen interesantes, porque los recursos no solo ocupan espacio -- remodelan el espacio a su alrededor. Veamos la historia de una imagen con floatRight en una composición a dos columnas. El párrafo que referencia la imagen tiene ahora que envolverla. Algunas líneas son más cortas -- comparten espacio horizontal con la imagen. Otras son de ancho completo -- están por debajo de la imagen. La API layoutNextLine() de Pretext gestiona esto con elegancia, aceptando un ancho disponible diferente para cada línea. Pero la consecuencia se propaga hacia fuera: la altura total del párrafo cambia, lo que empuja los bloques posteriores hacia abajo, potencialmente derramándolos hacia la siguiente columna o incluso la siguiente página.
Y luego está la colocación diferida. Imagina que una imagen se referencia en un punto donde solo quedan 50px de espacio en la columna, pero la imagen mide 300px de alto. No cabe ahí. Así que el motor la difiere a la parte superior de la siguiente columna (o la siguiente página), continúa colocando texto e inserta la imagen en la posición diferida. El lector ve la imagen cerca (pero no exactamente en) el punto donde se menciona en el texto. Esto es práctica estándar en la composición tipográfica profesional; los libros lo hacen constantemente.
Reglas de colocación. Más allá del despacho de estrategias, la colocación de recursos sigue restricciones editoriales estrictas:
- Regla de post-referencia. Una figura, imagen o tabla siempre debe aparecer después de su referencia en el texto, nunca antes. El lector encuentra la referencia primero y luego ve el recurso. Si no hay espacio suficiente en la columna actual, el recurso se difiere hacia adelante, nunca se trae hacia atrás.
- Regla de proximidad. El recurso debe aparecer lo más cerca posible de su referencia. El motor minimiza la distancia entre el punto de referencia y la colocación real, dentro de las restricciones del espacio disponible y las demás reglas.
- Colocación superior e inferior. Tanto en composiciones a una columna como a varias, las figuras, imágenes, ilustraciones y tablas se colocan en la parte superior o inferior de la página (nunca flotando en medio de un bloque de texto). Cada recurso aparece con su pie de figura y se numera dinámicamente durante la composición, no en el contenido fuente.
- Numeración dinámica. Los recursos no se numeran en el markdown. La numeración (Figura 1, Figura 2, Tabla 1...) se asigna durante la composición, después de la colocación. Esto significa que insertar una nueva figura en medio del documento no requiere renumerar todas las referencias posteriores en el código fuente.
- Ancho completo en composiciones multicolumna. En maquetaciones multicolumna, los recursos que requieren colocación a ancho completo (abarcando todas las columnas) se colocan en la parte superior o inferior de la página, empezando desde la primera columna. Interrumpen el flujo de columnas, ocupan todo el ancho de la página, y el texto se reanuda en la primera columna debajo (o encima) del recurso.
#Pasada 5: Refinamiento tipográfico
Esta es la pasada que separa un motor de composición de un volcador de texto. Aplica las reglas de calidad editorial que los tipógrafos profesionales han aplicado a mano durante siglos -- y que el renderizado de texto ingenuo ignora por completo.
La Pasada 5 opera en dos niveles: división de líneas basada en penalizaciones dentro de cada párrafo, y aplicación estructural de reglas de cohesión (keep-together) entre bloques. Trabajan juntos, pero son mecanismos distintos.
Prevención de huérfanas, viudas y runts basada en penalizaciones
Las viudas y huérfanas son los signos más visibles de una composición amateur:
- Una viuda es una sola línea de un párrafo que queda aislada al final de una columna. El párrafo continúa en la columna siguiente, pero esa línea solitaria parece abandonada (como si la columna terminara prematuramente).
- Una huérfana es una sola línea de un párrafo que queda varada al inicio de una columna. El grueso del párrafo está en la columna anterior, pero una línea se desbordó (parece desconectada de su contexto).
- Un runt es un párrafo cuya última línea es una sola palabra corta (o dos) -- visualmente demasiado corta para sentirse como una línea de texto en condiciones. Menos grave estructuralmente que una viuda, pero igual de molesto para un lector atento.
Los tres se tratan inyectando demerits (penalizaciones) en el algoritmo de división de líneas de Knuth-Plass. En lugar de maquetar el párrafo y después tratar de reparar un corte malo a posteriori, el motor enseña al algoritmo que ciertos conjuntos de cortes son más caros que otros. El algoritmo elige entonces el conjunto de cortes globalmente óptimo, que de forma natural evita viudas, huérfanas y runts siempre que es posible.
Concretamente, para cada nodo de corte candidato en un párrafo:
- Si elegir este corte dejaría menos de
orphanMinLineslíneas en la parte superior de la siguiente columna, se sumanorphanPenalty(valor por defecto 1000) a los demerits del nodo. - Si elegir este corte dejaría menos de
widowMinLineslíneas al final de la columna actual, se sumanwidowPenalty(valor por defecto 1000). - Si la última línea resultante de este corte fuera más corta que
runtMinCharacters × normalSpaceWidth, se inyectaruntPenalty(valor por defecto 1000) como badness equivalente dentro de la fórmula cuadrática de demerits -- así compite en la misma escala que el badness de línea (que satura en 10000) en vez de quedar aplastado por él.
Estas penalizaciones conviven con los demerits habituales -- badness (razón de ajuste al cuadrado), coste de separación silábica y desajuste de clase de fitness -- en una única optimización global. El algoritmo puede aceptar alguna de ellas si la alternativa es peor (un párrafo sin un corte legal que cumpla todas las reglas), pero casi siempre encontrará un conjunto de cortes que las evite. Los ítems de lista se apuntan a la misma protección mediante avoidOrphansInLists, avoidWidowsInLists, avoidRuntsInLists (todos a true por defecto).
Una cuarta presión blanda, slackWeight, pondera un coste cuadrático de "espacio de columna no utilizado", de modo que el algoritmo prefiere conjuntos de cortes que llenen las columnas de forma ajustada. Juntos, estos demerits convierten la Pasada 5 en un refinamiento de división de líneas: la mayoría de casos de viudas, huérfanas y runts se resuelven dentro del solver de Knuth-Plass, no mediante ajustes de tracking a posteriori.
Todo esto se puede afinar en BodyTextConfig -- véase Configuración → Huérfanas, viudas, runts y cohesión. Ajustar cualquier *Penalty a 0 desactiva efectivamente esa regla.
Reglas estructurales de cohesión (keep-together)
Algunas agrupaciones son mayores que un solo párrafo -- abarcan bloques adyacentes y no pueden resolverse solo con la división de líneas. La Pasada 5 las aplica a nivel de colocación de bloques, moviendo grupos enteros hacia adelante cuando de otro modo se partirían en un salto de columna o de página:
- Título con su primer párrafo. Un título nunca debe aparecer al final de una columna si el párrafo que introduce empezaría en la columna siguiente. Lo gestiona
headings.keepWithNext(por defectotrue): si no hay sitio para el título más el mínimo de viudas del cuerpo (bodyText.widowMinLines, por defecto2) del bloque siguiente -- o solo una línea cuandoavoidWidowsestá desactivado --, el título se empuja hacia adelante para viajar con su texto. - Títulos consecutivos. Cuando aparecen varios títulos en secuencia (por ejemplo, un h2 seguido de un h3 seguido de un párrafo), todo el grupo debe permanecer junto. Ninguno de los títulos puede quedar suelto al final de una columna sin el contenido que introducen.
- Listas introducidas con dos puntos. Cuando un párrafo termina con dos puntos que introducen directamente una lista, la línea que lleva los dos puntos debe quedarse con el inicio de la lista. Lo gestiona
bodyText.keepColonWithList(por defectotrue): si colocar el párrafo no dejaría sitio para el primer ítem de la lista, la última línea con los dos puntos (o el párrafo entero, si es de una sola línea) se mueve adelante junto con la lista. Siempre que esta regla tenga que empujar el párrafo completo y justo antes haya una secuencia de títulos en la columna, esos títulos también se arrastran hacia adelante para no violar silenciosamentekeepWithNext; la única excepción es cuando la columna contiene solo el título (o títulos) que una iteración anterior ya movió hacia adelante, en cuyo caso el motor deja el párrafo junto al título y acepta la separación más leve entre dos puntos y lista para evitar un bucle. - Figura con su pie. Una figura y su pie de figura son una unidad inseparable. Siempre se mueven juntos.
Cuando se detecta una violación de cohesión, el motor empuja todo el grupo a la siguiente columna o página. El espacio vacante se gestiona mediante el mecanismo normal de llenado de columnas (el divisor de líneas ya ha elegido un conjunto de cortes que encaja; si la columna resultante queda algo corta, la Pasada 7 redistribuye el espacio vertical alrededor de elementos que rompen la rejilla para mantenerla honesta).
Salida
Los bloques cuyas mediciones o colocaciones hayan cambiado se marcan como dirty para la siguiente iteración del bucle de convergencia. En la práctica, como el grueso del trabajo lo hace Knuth-Plass en lugar de ajustes a posteriori, la mayoría de documentos se estabilizan rápido -- el divisor de líneas elige un buen conjunto de cortes a la primera y las iteraciones siguientes solo tienen que absorber efectos secundarios del movimiento de bloques y del equilibrado de columnas.
Estas correcciones son invisibles cuando se hacen bien (un lector nunca debería notarlas). Pero su ausencia salta a la vista de cualquiera que lea con atención: esa línea incómoda al inicio de una columna, esos huecos desiguales donde el motor renunció a intentar ajustar el texto. Las editoriales profesionales tienen guías de estilo enteras dedicadas a prevenir exactamente estos problemas. Postext los automatiza.
#Pasada 6: Equilibrado de columnas
- Entrada: VDT con tipografía refinada
- Acción: Si
ColumnConfig.balancingestrue, igualar las alturas de las columnas en cada página moviendo bloques entre columnas para minimizar la diferencia de altura - Restricción: No debe violar las reglas de viudas/huérfanas establecidas en la Pasada 5
- Salida: Los bloques pueden haberse movido entre columnas, marcados como
dirty
Las columnas desequilibradas se notan inmediatamente, sobre todo en la última página de un capítulo. Una columna izquierda llena y una derecha casi vacía parece inacabada -- como si la maquetación se hubiera rendido a mitad de camino. El equilibrado redistribuye el contenido para que ambas columnas queden aproximadamente a la misma altura, dando a la doble página un aspecto pulido e intencionado.
El algoritmo calcula la altura total de contenido de todos los bloques en una página, divide por el número de columnas para encontrar la altura objetivo, y busca el mejor punto de corte de columna que acerque cada columna lo más posible a ese objetivo. Pero no es un simple corte por la mitad. Es un problema de satisfacción de restricciones: el algoritmo debe respetar las reglas keepTogether (un encabezado debe permanecer con su primer párrafo), honrar los conteos mínimos de líneas, y -- esto es crucial -- no deshacer las correcciones de viudas y huérfanas que la Pasada 5 tanto trabajó en establecer.
#Pasada 7: Alineación del ritmo vertical
- Entrada: VDT con columnas equilibradas
- Acción: Ajustar las líneas base a la rejilla distribuyendo ajustes de espaciado alrededor de encabezados, imágenes y otros elementos que rompen la rejilla
- Salida: Valores de espaciado ajustados; líneas base alineadas entre columnas
- Ver: Sistema de ritmo vertical para el algoritmo completo
#Bucle de convergencia
Piensa en el bucle de convergencia como el motor discutiendo consigo mismo. La Pasada 5 elige un conjunto de cortes que evita una viuda en el párrafo A -- pero al hacerlo, el párrafo A queda una línea más corto, lo que deja un hueco al final de la columna 2. La Pasada 6 re-equilibra las columnas para compensar, lo que empuja un título a una nueva columna, lo que activa keepWithNext y fuerza al título a saltar entero a la siguiente columna. La Pasada 7 ajusta el ritmo vertical, lo que podría crear un nuevo runt donde antes estaba el título. Así que el motor vuelve a la Pasada 3, re-coloca los bloques con las medidas actualizadas, y recorre toda la secuencia otra vez. Cada iteración resuelve más problemas de los que crea -- hasta que, finalmente, nada queda sucio.
Como la mayoría de los casos de viudas, huérfanas y runts se resuelven dentro del solver de Knuth-Plass en una sola pasada de división de líneas, los documentos típicos ahora convergen en 1--2 iteraciones. El bucle sigue siendo necesario cuando eventos a nivel de bloque (un título empujado por keepWithNext, una figura diferida por la colocación, o el equilibrado de columnas igualando alturas) desplazan los límites de columna sobre los que midió la Pasada 5. Cuando eso ocurre, la Pasada 3 re-coloca, la Pasada 5 re-divide con las nuevas restricciones, y el bucle se asienta.
Tras completarse las pasadas 5--7, el motor comprueba si hay bloques marcados como dirty. Si existen bloques sucios y el contador de iteraciones está por debajo de 5, el pipeline se re-ejecuta desde la Pasada 3.
Criterios de convergencia:
- No hay bloques sucios después de las pasadas 5--7, o
- Se ha alcanzado el máximo de 5 iteraciones (se acepta el mejor resultado obtenido hasta ese momento)
El motor registra una puntuación de violaciones tipográficas en cada iteración -- una suma ponderada de los problemas restantes: viudas, huérfanas, columnas desequilibradas, desalineación de la rejilla de líneas base. Cada tipo de violación tiene un peso que refleja su gravedad visual (una viuda es mucho más perceptible que 2px de desalineación en la rejilla). Si se alcanza el límite de 5 iteraciones sin convergencia completa, el motor elige la iteración que produjo la puntuación de violación más baja. No necesariamente la última -- las iteraciones posteriores a veces sobrecorrigen, arreglando un problema mientras crean otro.
En la práctica, la mayoría de los documentos convergen en 1--2 iteraciones gracias a la división de líneas basada en penalizaciones -- el solver de Knuth-Plass resuelve viudas, huérfanas y runts en una sola pasada de la Pasada 5. Solo se necesitan iteraciones adicionales cuando eventos a nivel de bloque (keep-with-next, dos-puntos-con-lista, diferimiento de figuras, equilibrado de columnas) desplazan los límites de columna sobre los que midió el divisor de líneas. El límite de 5 es una válvula de seguridad pragmática: lo perfecto es enemigo de lo terminado. Algunos casos patológicos -- una página donde cada párrafo tiene exactamente la longitud incorrecta para crear viudas independientemente de cómo equilibres las columnas -- nunca convergerán del todo. El motor acepta el "mejor esfuerzo" y sigue adelante.
#Sistema de ritmo vertical
Coge un libro bien compuesto y ponlo a contraluz. Las líneas de la página izquierda se alinean con las de la derecha. La línea base de la línea 5 en la columna 1 está exactamente en la misma posición vertical que la línea base de la línea 5 en la columna 2. Eso es el ritmo vertical, y es una de las primeras cosas que un ojo entrenado comprueba al evaluar la calidad tipográfica. También es uno de los diferenciadores clave de Postext.
Cuando ambas columnas contienen solo texto de cuerpo al mismo tamaño, la alineación es trivial -- cada línea tiene la misma altura, así que las líneas base se alinean naturalmente. El desafío aparece en el momento en que una columna contiene un encabezado con un tamaño de fuente mayor, una imagen con una altura arbitraria en píxeles, o espaciado extra alrededor de una cita. Estos elementos "rompen" la rejilla: el contenido por debajo se desplaza una cantidad que no es múltiplo del incremento de línea base, y de repente las líneas base en esa columna se desincronizan con la columna adyacente. La armonía visual desaparece.
El objetivo es recuperarla: las líneas base del texto de cuerpo en columnas adyacentes deben alinearse horizontalmente, incluso cuando encabezados, imágenes u otros elementos con alturas no estándar aparezcan en una columna pero no en la otra.
#Rejilla de líneas base
Todo se ancla a un solo número. El documento define un valor baselineGrid derivado del line-height del texto de cuerpo -- por ejemplo, texto de cuerpo a 16px con un line-height de 1.5 produce una rejilla de líneas base de 24px. Cada línea base del texto de cuerpo debería caer en un múltiplo de este valor. Ese es el contrato.
#Elementos que rompen la rejilla
Algunos elementos rompen la rejilla porque su altura no es múltiplo de baselineGrid:
- Encabezados: tamaño de fuente mayor, line-height diferente
- Imágenes: altura arbitraria en píxeles
- Tablas: altura variable
- Citas: pueden usar tamaño de fuente o padding diferente
- Separadores de notas al pie: filete de altura fija
#Algoritmo de ajuste de espaciado
Tras ajustar el espaciado en cada columna de forma independiente, el motor verifica la alineación entre columnas: las líneas base en la misma posición vertical en columnas adyacentes deben coincidir. Si divergen -- porque las distintas columnas tienen diferentes elementos que rompen la rejilla -- una segunda pasada de alineación ajusta los huecos en ambas columnas para encontrar un ritmo común.
Un ejemplo concreto. La columna 1 tiene un encabezado de 36px (1,5 veces la rejilla de 24px). La columna 2 no tiene encabezado. Después del encabezado, la columna 1 se ha desviado 12px de la rejilla. El algoritmo añade 12px de espacio extra después del encabezado -- incrementando el "espacio después del encabezado" de 16px a 28px. Ahora la siguiente línea de texto de cuerpo en la columna 1 cae sobre una línea de la rejilla de nuevo, y su línea base coincide con la línea correspondiente de la columna 2. Armonía restaurada.
Casos límite:
- Una columna con más elementos que rompen la rejilla que huecos ajustables acepta una alineación parcial (el algoritmo hace lo que puede pero no puede garantizar una alineación perfecta si hay demasiadas disrupciones y pocos puntos para absorber el error)
- Una imagen más alta que la columna abarca columnas o páginas (se gestiona por separado en la Pasada 4)
- Cuando el ajuste necesario crearía un espaciado visualmente incómodo (por ejemplo, 40px de espacio después de un encabezado cuando la norma es 16px), el algoritmo distribuye el error entre varios huecos en lugar de concentrarlo en uno solo
#Interfaz del backend
El backend es una sola interfaz que gestiona tanto la medición de texto como el renderizado de salida para un destino específico. No dos interfaces. Una.
Es una decisión deliberada, y existe por una razón crítica: la forma en que mides el texto debe coincidir exactamente con la forma en que lo renderizas. Imagina que el backend de medición usa métricas de fuente del canvas, pero el backend de renderizado usa una librería PDF con tablas de kerning ligeramente diferentes. La composición no coincidirá con la salida. Líneas que el motor midió como ajustadas en 320px podrían desbordarse o quedarse cortas al renderizar. Cada píxel de desviación es una mentira. Al integrar medición y renderizado en una sola interfaz, cada backend garantiza consistencia interna: las métricas de fuente que usa para medir son exactamente las mismas que usa para dibujar.
Por eso el backend de PDF, por ejemplo, no vuelve a medir el texto: consume un VDT ya convergido producido por el backend de medición canvas y traduce sus coordenadas en píxeles a puntos PDF. Las métricas del canvas son la fuente de verdad; PDF es un transporte. Los usuarios de renderToPdf (del paquete postext-pdf) pasan el mismo VDT que pasarían a renderToCanvas o renderToHtml, y las tres salidas están garantizadas a coincidir en saltos de línea, alturas de columna y colocación de recursos.
#Interfaz
interface PostextBackend {
// Lifecycle
initialize(config: PostextConfig): Promise<void>;
dispose(): void;
// Measurement
measureText(text: string, style: TextStyle): MeasuredText;
measureImage(resource: PostextResource): { width: number; height: number };
// Rendering
renderPage(page: VDTPage): void;
renderBlock(block: VDTBlock): void;
renderLine(line: VDTLine, style: TextStyle): void;
}
interface TextStyle {
font: string; // CSS font shorthand (e.g. '16px/1.5 Inter')
tracking?: number; // letter-spacing adjustment in px
hyphenate?: boolean;
}
interface MeasuredText {
lines: VDTLine[];
height: number;
width: number;
}#Backends
| Backend | Medición | Renderizado | Estado |
|---|---|---|---|
| Canvas | Pretext (métricas de fuente del canvas) | Dibujo bitmap sobre un HTMLCanvasElement (renderToCanvas, renderPage, renderPageToCanvas) | Disponible |
| HTML | Pretext (las mismas métricas que canvas) | Nodos DOM posicionados absolutamente con CSS editorial (renderToHtml, renderToHtmlIndexed) | Disponible |
| Consume el VDT ya medido con Pretext | Construcción de páginas PDF mediante pdf-lib con incrustación de fuentes por peso (renderToPdf en postext-pdf) | Disponible | |
| Server-side | Pretext + node-canvas | Renderizado headless para SSR / generación por lotes | Futuro |
Los tres backends disponibles consumen el mismo VDTDocument. La división entre postext (que exporta los backends de canvas y HTML) y postext-pdf (que exporta el backend de PDF) es puramente cuestión de dependencias: el camino PDF arrastra pdf-lib y @pdf-lib/fontkit, y la mayoría de integraciones web no los necesitan. Instala postext-pdf solo cuando realmente quieras emitir bytes PDF.
Restricción de solo navegador: En la Fase 1, toda la computación de composición ocurre en el lado del cliente, en el navegador. El pipeline puede ejecutarse tanto en el hilo principal (buildDocument) como dentro de un Web Worker dedicado (createLayoutWorker desde postext/worker) -- la ruta del worker es la integración recomendada para aplicaciones orientadas a UI porque mantiene la medición y el bucle de convergencia fuera del hilo principal, soporta cancelación last-wins mediante AbortSignal, y posee su propia caché de medición y caché de rasterización de matemáticas para que las reconstrucciones sucesivas sigan siendo baratas. Consulta Configuración → Ejecutar la composición en un Web Worker para el patrón de integración completo. El renderizado en servidor sigue siendo una decisión de alcance deliberada para más adelante -- clavar primero la experiencia en el navegador, expandir a otros destinos después.
#Estrategia de rendimiento
La diferencia entre una herramienta lenta y una que parece mágica es un factor de 10x. Una composición de 500ms significa que el usuario ve un tirón visible cada vez que redimensiona la ventana. Una composición de 50ms se siente instantánea -- como si el documento siempre hubiera estado ahí. Ese factor no se puede parchear después. Hay que diseñarlo desde el primer día.
Piensa a qué se enfrenta el motor: miles de bloques de texto repartidos en cientos de páginas, con la composición completa potencialmente recalculada en cada redimensionado de ventana. Es la misma clase de problema que enfrentan los motores de juegos -- procesar miles de objetos (geometría, física, iluminación, IA) 60 veces por segundo. Lo resuelven con una arquitectura de pipeline (múltiples pasadas sobre estado mutable compartido, cada pasada haciendo una sola cosa rápido) y con la eliminación agresiva de trabajo innecesario (culling, dirty flags, particionado espacial). Postext toma prestada cada una de estas ideas.
#Principios
-
Computación en memoria. El VDT completo cabe en memoria. No se lee el DOM durante la composición. El DOM solo se toca al final, durante el renderizado.
-
Seguimiento de cambios. Los bloques llevan un dirty flag. Las pasadas saltan subárboles limpios. El bucle de convergencia solo re-ejecuta desde el punto sucio más temprano.
-
Convergencia acotada. El máximo de 5 iteraciones es una garantía firme. El peor caso de rendimiento es predecible y medible.
-
Velocidad de Pretext. La medición de texto a 300–600x la velocidad del DOM significa que el motor puede permitirse re-medir texto de forma especulativa (probando diferentes anchos de columna, puntos de separación silábica, ajustes de tracking) sin bloquear el hilo principal.
-
Builds fuera del hilo principal. El punto de entrada
postext/workerejecuta el pipeline completo dentro de un Web Worker dedicado. El hilo principal publica{ content, config }y unAbortSignal; el worker registra las fuentes (transferidas comoArrayBuffers), ejecuta el bucle de convergencia y publica de vuelta elVDTDocumentterminado. Una nueva llamada abuild()cancela cooperativamente la anterior -- el worker consulta un hook de cancelación por bloque dentro debuildDocumenty lanzaBuildCancelledError, de modo que un usuario escribiendo en un editor nunca espera por una composición ya obsoleta. El worker también mantiene su propia caché persistente de medición y una caché de rasterización de matemáticas con clave por contenido, de modo que los objetosMathRenderclonados estructuralmente sobreviven a las reconstrucciones sin re-rasterizarse. -
Campos numéricos planos. Las cajas delimitadoras se almacenan como campos planos
x, y, width, heighten cada nodo, no como objetos anidados. Esto evita perseguir punteros y es más amigable con la caché. -
VDT de doble acceso. El árbol (
pages > columns > blocks) proporciona acceso jerárquico para pasadas que necesitan trabajar página por página o columna por columna (como la Pasada 6, equilibrado de columnas). Un array plano paraleloblocks[]proporciona acceso indexado O(1) para pasadas que necesitan iterar todos los bloques independientemente de su ubicación (como la Pasada 5, detección de viudas/huérfanas). Ambas vistas referencian los mismos objetos bloque (no hay duplicación, solo dos formas de recorrer los mismos datos).
#Gestión del redimensionado
Cuando el usuario redimensiona la ventana, el motor no reconstruye todo desde cero. Actualiza los anchos de columna en el VDT, marca todos los bloques de texto como sucios, y re-ejecuta el pipeline desde la Pasada 2. Las estructuras de páginas y columnas se reutilizan.
Aquí es donde el VDT mutable paga dividendos. En lugar de descartar toda la composición y empezar de cero, el motor reutiliza todo el trabajo posible. Los resultados de prepare() de Pretext siguen siendo válidos -- dependen de la fuente y el contenido del texto, no del ancho -- así que solo las llamadas baratas a layout() necesitan re-ejecutarse. Un documento de 50 páginas puede re-componerse completamente re-midiendo todos los bloques de texto (rápido, porque prepare() está cacheado) y re-ejecutando las pasadas 3--7, sin re-analizar el markdown ni re-resolver referencias. El usuario arrastra el borde de la ventana y la composición le sigue en tiempo real.
#Benchmarking desde el primer día
Cada pasada se puede medir de forma independiente. Los tests incluyen aserciones de rendimiento, no solo de corrección:
// Example benchmark test
bench('layout 50-page document', () => {
const vdt = createVDT(fiftyPageContent, config);
runPipeline(vdt);
}, { time: 100 }); // must complete in under 100msLos tests de rendimiento se ejecutan junto a los tests unitarios en vitest, y las regresiones se detectan en CI. Si un refactor hace que la Pasada 5 sea el doble de lenta, el build se rompe. El rendimiento es una funcionalidad, no una esperanza.
#Flujo de datos
#Fuera de alcance
Cada uno de estos límites es una decisión consciente — el motor es lo bastante complejo por sí solo, y asumir responsabilidades que pertenecen a otro lugar sería la forma más rápida de no terminar nunca.
- Renderizado en servidor. Toda la composición se ejecuta en el navegador. El motor depende de métricas de fuente del canvas (vía Pretext), que requieren un entorno de navegador. Un backend de servidor usando
node-canvaspodría llegar más adelante, pero no forma parte del diseño inicial. Primero el navegador. - Edición WYSIWYG. Postext es un motor de composición, no un editor. Contenido entra, geometría sale. Construir una superficie de edición interactiva -- gestión de cursor, selección, deshacer/rehacer, manejo de entrada -- es un problema completamente diferente. Postext puede servir como backend de renderizado para un editor, pero no proporciona capacidades de edición por sí mismo.
- Wrapper de CSS column-count. Postext reemplaza la composición multicolumna de CSS; no la envuelve. Calcula geometría posicionada precisa desde cero, porque el algoritmo de columnas del navegador carece de control sobre la colocación de recursos, la prevención de viudas/huérfanas y las reglas tipográficas entre columnas. Esas son precisamente la razón de ser de Postext.
- Gestión de breakpoints responsivos. Postext calcula la composición a un tamaño de página dado. El consumidor decide cuándo re-componer (al redimensionar la ventana, al cambiar la orientación). Postext no gestiona breakpoints, media queries ni decisiones de diseño responsivo. Eso es cosa tuya.
- Edición colaborativa en tiempo real. Postext es un cálculo de composición sin estado -- contenido entra, geometría sale -- no un sistema de documentos colaborativo con resolución de conflictos, transformaciones operacionales ni consciencia multiusuario.
- Carga o gestión de fuentes. Postext asume que las fuentes ya están cargadas y disponibles para la medición. La carga de fuentes, las cadenas de fallback y el subsetting de fuentes son responsabilidad del consumidor. Si una fuente no está cargada cuando Postext mide el texto, las mediciones usarán la fuente de fallback del navegador, y la composición será incorrecta cuando la fuente real se cargue. Carga tus fuentes primero.
#Apéndice: relación con los tipos existentes
Así se relaciona cada tipo ya definido en packages/postext/src/types.ts con la arquitectura descrita arriba:
| Tipo | Rol en la arquitectura |
|---|---|
PostextContent | Punto de entrada: la entrada al motor (Pasada 1) |
PostextConfig | Controla el comportamiento del pipeline en todas las pasadas |
PostextResource | Se convierte en un VDTBlock de tipo 'resource' en la Pasada 1 |
PostextNote | Se convierte en un bloque de nota al pie, nota final o nota al margen en la Pasada 1 |
PlacementStrategy | Determina el comportamiento de colocación de recursos en la Pasada 4 |
ColumnConfig | Controla la creación de páginas/columnas (Pasada 3) y el equilibrado (Pasada 6) |
TypographyConfig | Controla el refinamiento tipográfico (Pasada 5) y el ritmo vertical (Pasada 7) |
ResourcePlacementConfig | Controla la estrategia de colocación y el diferimiento de recursos en la Pasada 4 |
ReferenceConfig | Controla la resolución de referencias (Pasada 1) y la reserva del área de notas al pie (Pasada 3) |
PostextSectionOverride | Crea sobrecargas de configuración por zona durante el análisis de la Pasada 1 |
#Nuevos tipos introducidos por esta arquitectura
Los tipos del VDT (VDTDocument, VDTPage, VDTColumn, VDTBlock, VDTLine, BoundingBox, VDTFootnoteArea) y los tipos del backend (PostextBackend, TextStyle, MeasuredText) son nuevos en esta arquitectura. Se ubicarán en archivos dedicados junto al types.ts existente:
packages/postext/src/vdt.ts(tipos del Árbol Virtual del Documento)packages/postext/src/backend.ts(tipos de la interfaz del backend)
Estos extienden las definiciones de tipos existentes sin modificarlas -- los tipos actuales permanecen intactos.