Saltar al contenido principal

Capítulo 2 · Parte I · Fundamentos

Arquitectura de Postext

Arquitectura técnica del motor de composición tipográfica Postext: sus conceptos, el árbol del documento, el ritmo vertical, los backends y el rendimiento

Actualizado 2026-11-0429 minenescadefritptzhjaar

En pocas palabras

Esta página explica cómo funciona Postext por dentro, y está escrita para programadores. Primero, Postext mide cada palabra sin dibujar nada en pantalla, y por eso es muy rápido. Después calcula dónde va cada línea, cada imagen y cada columna, y guarda ese plan en memoria. Al final dibuja las páginas terminadas como página web, como imagen en un canvas o como archivo PDF. Si cambias una palabra, vuelve a componer las páginas y aprovecha las medidas de los párrafos que no han cambiado. La página siguiente explica, paso a paso, cómo se colocan las líneas y las imágenes.

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 de muchas páginas y varias columnas, calculada antes de dibujar 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 biblioteca que mide el texto sin el DOM entre 300 y 600 veces más rápido que el reflow del navegador. La historia del proyecto (una década de intentos fallidos, el cuello de botella que los frenaba todos y la biblioteca que por fin lo eliminó) se cuenta en 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 dibujar 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 última línea solitaria que se queda al pie de una columna) llevándola a la columna anterior. Perfecto. Pero ese cambio acorta la columna de la página 5, eso desplaza el contenido hacia delante, y eso 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 ajustarlo todo, volver a medirlo y volver a comprobarlo.

Esa es la filosofía de «calcularlo todo primero y dibujar después». No es un truco para ganar velocidad. 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 da por sabidos estos términos: vuelve aquí cuando alguno se haya desdibujado.

TérminoDefinición
VDTVirtual 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.
ColumnSubdivisió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.
BlockUnidad de contenido que ocupa espacio vertical en una columna: párrafo, encabezado, imagen, tabla, cita, cita destacada o área de notas al pie.
LineLí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.
ResourceElemento no textual conectado a la prosa por id: un mapa de bits, un SVG, una tabla o un vídeo (Resource, con kind: 'bitmap' | 'svg' | 'table' | 'video'). Los recursos se numeran por tipo (Figura 1, Tabla 2.1…) en su primera referencia y flotan a una banda en la parte superior o inferior de una página cercana a esa referencia.
NoteNota al pie o de final de capítulo, escrita en el markdown como una llamada [^id] y una definición [^id]: (véase Notas al pie). Las notas al margen, y la lista PostextNote que acepta el modelo de contenido, no están implementadas: el motor no lee notes.
Baseline GridLa retícula vertical derivada del interlineado del texto base (p. ej. 24px para 16px/1,5). Cada línea base del texto base debería caer en un múltiplo suyo, manteniendo las líneas alineadas entre columnas y entre páginas enfrentadas.
BadnessCuánto se desvían los espacios entre palabras de una línea justificada de su anchura natural: la razón de ajuste al cuadrado, que se satura en 10000. El coste base de el corte de líneas de Knuth-Plass.
DemeritsEl coste total de un corte de línea candidato en Knuth-Plass: badness más penalizaciones (separación silábica, una última línea corta, desajuste de clase de fitness). El algoritmo elige el conjunto de cortes con el total más bajo. La partición de un párrafo entre dos columnas se elige de la misma manera, por la suma más baja de los costes de viuda, huérfana y slack.
Fitness ClassUna categoría gruesa de lo apretada o suelta que queda una línea. Dos líneas adyacentes en clases muy distintas (una apretada junto a una muy suelta) reciben un demerit extra, suavizando la textura del párrafo.
SlackLíneas que quedan vacías al final de una columna cuando un párrafo se parte ahí o pasa entero a la siguiente. Un coste cuadrático de slack (ponderado por slackWeight) se sopesa frente a las penalizaciones de viuda y huérfana cuando el motor elige la partición.
BackendDestino de renderizado que consume el VDT terminado. Hoy se distribuyen cuatro: canvas (vista previa en mapa de bits), HTML (lectura en pantalla basada en DOM), PDF (salida lista para imprimir a través de postext-pdf) y EPUB (postext-epub, el libro como libro electrónico). Los cuatro renderizan a partir de la misma medición (el módulo de medida sobre Pretext). El visor 3D postext-folio pinta sus hojas con el backend de canvas.
Placement PassPasada de colocación: un recorrido del contenido, del primer bloque al último, que construye un VDT completo. Analiza el markdown, mide a través de la caché, coloca el texto, los flotantes y las cajas, y aplica las reglas de corte y de cohesión.
RoundRonda: una repetición de la pasada de colocación con indicaciones de la anterior (un tope de banda, palancas de equilibrado, etiquetas de página o números de nota ya asentados). Cada tipo de ronda tiene su propio límite (véase El controlador de pasadas).

#Arquitectura del sistema

Arquitectura del sistema PostextEl markdown enriquecido y PostextConfig entran en el analizador. La pasada de colocación mide el texto con Pretext y rellena el Árbol Virtual del Documento; buildDocument repite la pasada con indicaciones hasta que la maquetación se asienta. Un backend convierte el VDT terminado en HTML o PDF.Motor de maquetaciónMarkdown enriquecidoPostextConfig(columnas, reglas, espaciado)Analizadormarkdown → bloquesPretextmedición de textoÁrbol Virtual delDocumento (VDT)una pasada lo rellena in situpáginas > columnas > bloquescada nodo tiene bboxun árbol nuevo en cada pasadaPasada de colocación (un recorrido del contenido)Medirlas líneas, a través de la cachéColocarlos bloques en columnas y páginasFlotarlos recursos en bandas de páginaPartirlos párrafos al final de la columnaMantenerencabezados y listas con su textoDevolverel flujo a la rejilla baserepetircon indicacionesBackend(interfaz unificada)Canvas / NavegadorPDFEPUBHTML / PDFsalida generada
Analizador → pasada de colocación ↔ Pretext → VDT → backend → salida.

Este es el recorrido de tu contenido a través del motor:

  1. El analizador lee el markdown enriquecido y lo convierte en una lista plana de bloques de contenido tipados: contenido y estructura, todavía sin posiciones
  2. Una pasada de colocación recorre esa lista una vez y construye el VDT. Mide cada bloque a través de Pretext al ancho de la columna en la que va a entrar, coloca el texto, los flotantes y las cajas en páginas y columnas, y aplica sobre la marcha las reglas de corte y de cohesión
  3. El controlador (buildDocument) repite la pasada cuando una decisión necesita el resultado de una composición entera: un índice que imprime números de página, una caja a ancho de página que tiene que cortar a nivel las columnas de encima, una columna que quedó corta. Cada repetición recibe indicaciones de la pasada anterior, y cada tipo de repetición tiene su propio límite
  4. 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
  5. Un backend recorre el VDT terminado y lo renderiza al formato destino: un mapa de bits en un canvas, un árbol DOM de elementos HTML posicionados, un documento PDF con fuentes incrustadas o un EPUB. El mismo VDT alimenta a los cuatro; 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.

Modelo de contenido: markdown, recursos y notasLa entrada de Postext separa el markdown (orden de lectura y estructura semántica) de los recursos (mapas de bits, SVG, tablas) y de las notas. El motor resuelve por id las referencias en línea :ref y las inserciones ::resource, y produce el VDT.PostextContentmarkdownorden de lectura + :ref / ::resource por id:ref{id=fig-1}::resource{id=tbl-1}resources[]mapas de bits, SVG, tablasfig-1bitmapsvg-1svgtbl-1tablemetadatatítulo, autoría, fechasresuelto por id: :ref{id=…}, ::resource{id=…}motorVDT
El markdown aporta el orden de lectura; los recursos aportan los datos visuales.
// packages/postext/src/types.ts
 
interface PostextContent {
  markdown: string;            // markdown enriquecido con marcadores :ref / ::resource
  metadata?: DocumentMetadata; // título, subtítulo, autoría, fecha de publicación, …
  resources?: Resource[];      // bitmaps, SVGs, tablas, vídeos — referenciados por id
  notes?: PostextNote[];       // no se lee: las notas al pie se escriben como [^id] en el markdown
  continuation?: LayoutContinuation; // contadores y números de página que vienen de capítulos anteriores
  outline?: OutlineEntry[];    // los encabezados del libro con su página, para :::toc / :::index
  citations?: CitationContext; // la parte de las citas del libro que toca a este capítulo
  laterPageIds?: Record<string, number>; // bloques de página de capítulos posteriores donde puede seguir una historia
}
 
interface Resource {
  id: string;                  // id estable, referenciado por los :ref en línea
  typeId: string;              // ResourceType al que pertenece ('figure', 'table', …)
  kind: 'bitmap' | 'svg' | 'table' | 'video';
  caption?: string;            // el prefijo del tipo + número se calculan, no se escriben aquí
  altText?: string;
  createdAt: number;
  updatedAt: number;
  // Exactamente un payload específico del kind:
  bitmap?: { fileId: string; format: string; width: number; height: number };
  svg?: { fileId: string; width?: number; height?: number };
  table?: { model: TableModel };
  video?: ResourceVideo;       // de dónde se reproduce; en la página se dibuja su póster
  placement?: ResourcePlacement; // sobrescritura opcional de flotado por recurso
  safeArea?: ResourceSafeArea;  // bitmap / svg: { x, y, width, height } en fracciones, la parte que siempre se ve
}

Los recursos llevan los datos visuales: pies, texto alternativo y un contenido específico de su kind. Los contenidos binarios (mapas de bits, SVG) van fuera de banda: el recurso solo guarda un fileId, y el renderizador lo resuelve en el momento de dibujar (el Sandbox guarda los bytes en IndexedDB). Las tablas son la excepción: su TableModel (una cuadrícula de celdas con fusiones y alineación) viaja en línea, porque son datos estructurados, no bytes. Las notas se escriben en el markdown: una llamada [^id] donde se cita la nota y una definición [^id]:, un párrafo propio en cualquier lugar del capítulo. El motor las numera en el orden en que se citan por primera vez y compone cada una al pie de la columna que contiene la línea que la cita, bajo un filete corto, o tras el último bloque del capítulo con footnotes.placement: 'chapterEnd' (véase Notas al pie y Configuración → Notas al pie). El modelo de contenido sigue aceptando un array notes, pero ninguna etapa del pipeline lo lee.

Esta separación es una decisión de diseño deliberada, y tiene más importancia de lo que parece. Del Markdown dependen el orden de lectura y la estructura semántica: qué va primero, qué es un encabezado, dónde se cita una nota al pie y qué dice. Del array de recursos dependen los datos visuales: las dimensiones de las imágenes, el texto de los pies, las celdas de las tablas. 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 pasar una imagen a la columna siguiente porque aquí no cabe) sin tocar nunca tu contenido original.

// Ejemplo: un artículo sencillo con una figura referenciada
const content: PostextContent = {
  markdown: `
# El arte de la tipografía
 
La historia de la tipografía comienza con los tipos
móviles de Gutenberg, mostrados en :ref{id="imprenta"}.
Su invención transformó la producción de libros.
 
La técnica se extendió rápidamente por Europa, llegando
a Italia en 1465 y a Francia en 1470.
  `,
  resources: [
    {
      id: 'imprenta',
      typeId: 'figure',
      kind: 'bitmap',
      caption: 'Una reconstrucción de la imprenta original.',
      altText: 'Reconstrucción de la imprenta de Gutenberg',
      createdAt: 1765379100000,
      updatedAt: 1765379100000,
      bitmap: { fileId: 'foto-imprenta', format: 'jpeg', width: 600, height: 400 },
    },
  ],
};

Los recursos se conectan a la prosa por id, y referenciar uno basta para incorporarlo. El :ref{id="imprenta"} en línea hace dos cosas a la vez: imprime el número calculado del recurso en el texto corrido («Fig. 1») y, en la primera referencia del orden de lectura, coloca el recurso como flotante en la página, reservando una banda en la parte superior o inferior cercana a esa referencia, exactamente como haría un tipógrafo de imprenta. La figura nunca se coloca una segunda vez. Para el recurso ocasional que debe quedar en un punto exacto del flujo, ::resource{id="…"} en su propia línea es una inserción en bloque opcional: solo se renderiza en línea cuando el placement.position resuelto del recurso es 'here' (con lo que deja de flotar); para un recurso flotante, se trata simplemente como una referencia más. El autor nunca tiene que pensar en la colocación. De eso se encarga el motor.

Resolución de referenciasEl markdown cita una figura en línea con :ref{id=imprenta}. El array de recursos aporta el contenido real, identificado por su id. El motor resuelve la referencia, imprime el número calculado en el texto y coloca la figura como flotante en una banda en la parte superior de la página.markdown# El arte de la tipografíatipos móviles, mostrados en:ref{id=imprenta} …resources[]{ id: 'imprenta', kind: 'bitmap', … }::resource{id=…}inserción opcional para placement 'here'resolverpágina compuesta[ Figura 1: flotante en una banda ]…mostrados en Fig. 1…
En el markdown, las referencias son solo nombres. El motor las busca en los recursos, las numera y coloca la figura como flotante cerca de la referencia.

#Configuración

Cada aspecto del pipeline de composición está controlado por PostextConfig. El repaso completo de secciones (página, disposición, texto base, encabezados, listas, matemáticas, cabeceras y pies…) está en la página de Configuración; las más relevantes para este documento son:

ConfigControlaUsado en
bodyText / headingsPenalizaciones por campo de viudas/huérfanas/líneas cortas, reglas de cohesión, límites de justificación, separación silábica; véase Configuración → Texto baseMedición, cortes y reglas de cohesión, equilibrado
tableStyleTipografía de celdas, bordes, radio de las esquinas y rellenos de cabecera/cuerpo para recursos kind: 'table', más las variantes con nombre de tableStyles que una tabla elige con table.styleId; véase Configuración → Estilo de tablasColocación de recursos
captionStyleTipografía de los pies de recurso: la etiqueta numerada y el texto descriptivo; véase Configuración → Estilo de pies de recursoColocación de recursos
diagramStylesingleInk + inkColor: recolorean los diagramas SVG con matices de una sola tinta según su luminancia, para que las figuras se reproduzcan con fidelidad al imprimir con una única tinta directa; véase Configuración → Estilo de diagramasBackends
resourceTypesNumeración tipada de recursos: plantillas, formatos de contador, ámbitos de reinicio, colocación flotante predeterminada; véase Configuración → Tipos de recursoEstructuración del contenido, colocación de recursos

#Estrategia de análisis

El análisis es deliberadamente el paso más simple de todo el pipeline. El markdown entra y sale convertido en una lista plana de bloques de contenido tipados (ContentBlock, en packages/postext/src/parse/): un encabezado, un párrafo con sus tramos en línea, un ítem de lista, una directiva. El resultado se memoiza (parseMarkdownMemo), de modo que las pasadas de una composición analizan el texto una sola vez, y lo mismo hace una composición posterior del mismo texto. Las referencias a recursos se resuelven contra el array resources[] por ID al principio de cada pasada de colocación. Las definiciones de notas al pie (párrafos que empiezan por [^id]:) salen del flujo también ahí y se guardan por id para las llamadas que las citan; el array notes[] no se lee.

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 medidas. Sin posiciones. Sin ninguna decisión de maquetación. Un VDTBlock solo existe cuando la pasada de colocación ha medido un bloque y le ha dado un sitio.

#Á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, que se modifica in situ y que representa cada página, columna, bloque y línea, cada uno con una caja delimitadora precisa. Una vez que buildDocument termina, el VDT es la respuesta. Puedes preguntar «¿qué hay en la página 72, columna 2?» sin dibujar un solo píxel.

#Por qué mutable

Es el mismo enfoque que usan los motores de videojuegos para dibujar, donde varios sistemas sucesivos actualizan, en un bucle muy rápido, un estado del mundo compartido y mutable. Y por la misma razón.

Una pasada de colocación rellena el árbol in situ. Abre una página, le añade columnas, descuenta la altura de un bloque del availableHeight de la columna en la que cae y cierra las columnas de una banda allí donde la corta una caja a ancho de página. Un libro tiene miles de bloques, y un árbol inmutable (como el DOM virtual de React) asignaría un objeto nuevo en cada uno de esos pasos. Por eso los nodos son objetos planos y la pasada escribe en ellos.

Una pasada terminada no se parchea. Cuando buildDocument necesita otro intento (un tope de banda, una palanca de equilibrado), repite la pasada desde el contenido y obtiene un árbol nuevo. El árbol de la pasada anterior queda como estaba, y así el controlador puede comparar los dos, quedarse con el mejor o tomar unas páginas de cada uno. La caché de medición hace asumible la repetición: un párrafo cuyo texto, fuentes y ancho no han cambiado se lee de la caché en lugar de medirse otra vez.

#Estructura

Estructura del Árbol Virtual del DocumentoUn VDT jerárquico: el documento contiene páginas; cada página, columnas; cada columna, bloques (encabezado, párrafo, recurso), y cada bloque de texto, líneas medidas. Cada bloque tiene un rectángulo envolvente (bbox), su tipo y sus índices de página y de columna.VDTDocumentVDTPage [0]VDTPage [1]VDTPage [n]VDTColumn [0]VDTColumn [1]encabezadopárraforecursolínea 0línea 1Cada bloque tiene:bbox: { x, y, w, h }type: VDTBlockTypepageIndex: numbercolumnIndex: numberLas líneas tienen además:baseline: numberhyphenated: boolean
Cada bloque tiene su bbox, su tipo y sus índices de página y de columna.

#Definiciones de tipos

Las formas que siguen están simplificadas para la explicación: las definiciones reales, en packages/postext/src/vdt.ts, tienen muchos más campos orientados al dibujo (cadenas de fuente, colores, viñetas de lista, fórmulas ya dibujadas, ranuras de diseño). Lo que importa aquí es la estructura:

// Simplificado — véase packages/postext/src/vdt.ts para las definiciones completas
 
// La raíz del Árbol Virtual del Documento
interface VDTDocument {
  pages: VDTPage[];
  blocks: VDTBlock[];         // vista plana de los mismos objetos bloque
  config: ResolvedConfig;     // cada sub-config resuelta a no-opcional
  baselineGrid: number;       // incremento de línea base en px (p. ej. 24 para 16px/1.5)
  converged: boolean;         // cada segmento de equilibrado terminó a ras o sin palancas
  iterationCount: number;     // pasadas de colocación tras esta composición (ver El controlador de pasadas)
  metadata: DocumentMetadata;
}
 
// Una página física
interface VDTPage {
  index: number;
  width: number;
  height: number;
  columns: VDTColumn[];
  header?: VDTDesignSlot;     // cabecera (slot de diseño)
  footer?: VDTDesignSlot;     // pie / número de página
  floats?: VDTBlock[];        // bandas de recursos flotados arriba/abajo de esta página
  pageNumberValue: number;
  pageLabel: string;          // etiqueta renderizada ('iv', '7', 'A', …)
}
 
// Una columna dentro de una página
interface VDTColumn {
  index: number;
  bbox: BoundingBox;          // posición dentro de la página
  blocks: VDTBlock[];
  availableHeight: number;    // espacio vertical restante
  baselineOffset: number;     // posición y actual de la línea base
  band?: number;              // banda de columnas (0 salvo que un bloque a ancho de página parta la página)
  kind?: 'text' | 'span' | 'side'; // 'span' = columna a todo el ancho con un bloque a ancho de página,
                              // 'side' = la columna lateral solo para flotados de una disposición de columna y media
}
 
// Un bloque de contenido (párrafo, encabezado, recurso, etc.)
type VDTBlockType =
  | 'paragraph' | 'heading' | 'resource' | 'blockquote'
  | 'listItem' | 'footnoteRef' | 'mathDisplay' | 'callout' | 'code';
 
interface VDTBlock {
  id: string;
  type: VDTBlockType;
  bbox: BoundingBox;
  lines: VDTLine[];           // para bloques de texto: las líneas medidas
  resourceBlock?: ResolvedResourceBlock; // para bloques de recurso
  pageIndex: number;
  columnIndex: number;
  dirty: boolean;             // sin uso: se borra al colocar el bloque y nada lo lee
  snappedToGrid: boolean;     // el flujo volvió a la rejilla base bajo este bloque
  balancing?: VDTBalancing;   // las palancas de equilibrado de columnas aplicadas a este bloque
}
 
// Una línea de texto medida
interface VDTLine {
  text: string;
  bbox: BoundingBox;            // ancho natural: una línea justificada se pinta hasta el borde derecho de su bloque
  baseline: number;             // posición y de la línea base del texto
  hyphenated: boolean;          // la línea termina dentro de una palabra, o tras una raya sin espacios («I.—» | «Que»)
  hardHyphen?: boolean;         // …tras un guion del propio texto («físico-» | «química»): no se añade nada
  repeatedHyphen?: boolean;     // empieza con ese guion repetido («léxico-» | «-semántico»), que no está en la fuente
  segments?: VDTLineSegment[];  // tramos palabra/espacio/matemática para justificar
  isLastLine?: boolean;         // última línea de su párrafo
  justifiedSpaceRatio?: number; // ancho al que se pinta cada espacio ÷ espacio de la redonda
  justifiedSpaceLimitRatio?: number; // el espaciado que acotan los límites, cuando los espacios de la línea no son de redonda (lineWordSpacing lee los dos)
  sourceStart?: number;         // mapa al markdown original (offsets de carácter; una línea que empieza por `\$` empieza en la barra)
  sourceEnd?: number;
  plainStart?: number;          // mapa al texto plano del bloque
  plainEnd?: number;
  qr?: VDTLineQr;               // el código QR de una nota de enlace (links.show), en la primera línea de la nota
}
 
// Un embebido de recurso medido y listo para colocar (bitmap / svg / tabla / vídeo)
interface ResolvedResourceBlock {
  resource: Resource;
  kind: 'bitmap' | 'svg' | 'table' | 'video';
  number: string;             // número calculado, p. ej. "1.7"
  captionPrefix: string;      // p. ej. "Figura"
  bodyRect: BoundingBox;      // el área de la imagen / tabla
  bodySource?: ResourceSafeArea; // la parte de la imagen que se ve en bodyRect cuando se recorta a su zona segura
  bodyFlex?: { shrink: number; grow: number; delta: number }; // px que el cuerpo aún puede encoger / crecer, px aplicados
  fileId?: string;            // binario fuera de banda (bitmap / svg)
  captionLines: VDTLine[];    // pie medido, con prefijo + número incluidos
  table?: VDTResourceTableLayout; // geometría de celdas para recursos tabla
}
 
// Caja delimitadora — todos los valores en px, relativos al origen de la página
interface BoundingBox {
  x: number;
  y: number;
  width: number;
  height: number;
}

Algunos de estos campos merecen una nota:

  • isLastLine gobierna el dibujo justificado: las últimas líneas se dibujan en bandera aunque el párrafo esté justificado, salvo las últimas líneas desbordadas, cuyos espacios entre palabras se comprimen para caber en la medida (como el ajuste del pegamento en TeX).
  • sourceStart/sourceEnd y plainStart/plainEnd son mapas desde cada línea hacia el Markdown original y hacia el texto plano del bloque; sirven para la sincronización de cursor y selección en integraciones de editor.
  • VDTResourceTableLayout (con sus entradas VDTResourceTableCell) lleva la geometría completa de una tabla compuesta: los bordes x de las columnas, los bordes y de las filas y los rectángulos por celda con sus líneas de contenido medidas, de modo que todos los backends dibujan la misma tabla.
  • dirty y snappedToGrid se escriben al colocar un bloque y no los lee ninguna etapa ni ningún backend. dirty nace a true en un bloque nuevo y pasa a false cuando el bloque se coloca; el motor no lleva la cuenta de los cambios del árbol, repite la pasada de colocación (véase El controlador de pasadas). snappedToGrid es true en la primera parte de un bloque de texto tras el que el flujo vuelve a la rejilla (véase Sistema de ritmo vertical).
  • balancing dice qué palancas del equilibrado de columnas movieron o estiraron el bloque; un bloque que el equilibrado dejó como estaba no lo lleva (véase Equilibrado de columnas).
  • computePageTextExtent(page) es un pequeño helper público que devuelve la extensión vertical realmente cubierta por texto en una página (incluyendo los pies de los flotantes). Las superposiciones de depuración lo usan para que las líneas de la rejilla base abarquen solo texto real, no el final vacío de la página.

#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 base 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 cuerpo 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 base 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 interlineado del texto base: por ejemplo, un texto base a 16px con un line-height de 1.5 produce una rejilla base de 24px. Cada línea base del texto base 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: cuerpo mayor, interlineado diferente
  • Imágenes: altura arbitraria en píxeles
  • Tablas: altura variable
  • Citas: pueden usar otro cuerpo u otro relleno
  • Áreas de notas al pie: las notas al pie de una columna y su filete; el área de texto de la columna termina encima

#Cómo vuelve el flujo a la rejilla

Alineación con el ritmo verticalLa columna 1 contiene un encabezado que se sale de la rejilla base en 12 píxeles. Al colocar el encabezado, el motor redondea la altura usada en la columna hasta la siguiente línea de la rejilla, lo que añade 12 píxeles bajo el encabezado, y así la siguiente línea de texto base vuelve a caer en la rejilla. La columna 2 sigue alineada en todo momento.024487296120144168Columna 1Columna 2Texto base (línea base: 24px)Línea de texto baseEncabezado (36px)+12px de ajusteTexto base (de nuevo en la rejilla)Línea de texto basealineadoAjuste a la rejilla, al colocar1. Componer el bloque: sus líneas y después su espacio inferior (marginBottom)2. Redondear la altura usada en la columna hasta la siguiente línea de rejilla3. El bloque siguiente empieza ahí, así que el espacio inferior es un mínimo4. snapToGrid: false conserva el espacio exacto hasta el siguiente bloque que se ajuste
El espacio bajo un elemento que se sale de la rejilla se redondea hasta la rejilla al colocarlo, y así las líneas base de todas las columnas siguen alineadas.

La rejilla se mantiene en la colocación, bloque a bloque. Ninguna pasada posterior recorre las columnas buscando desviaciones. Las líneas de cuerpo miden una línea de rejilla, así que una tirada de texto base que empieza en la rejilla sigue en ella. Cuando la pasada de colocación ha compuesto un bloque que puede salirse de la rejilla, redondea la altura usada en la columna hasta la siguiente línea de rejilla antes de que empiece el bloque siguiente. Los bloques que se ajustan así son:

  • el último encabezado de una serie de encabezados (los encabezados consecutivos se siguen unos a otros sin ajuste, y el último realinea el texto que viene detrás);
  • el último ítem de una lista, sea cual sea el espaciado entre ítems;
  • una fórmula destacada;
  • un recuadro, un listado de código, y una figura o una tabla insertadas en línea;
  • el último párrafo de un contenedor :::paragraphs y un poema :::verse, que tienen su propio interlineado.

El espacio inferior del bloque (marginBottom) se cuenta antes del redondeo, de modo que el margen es un mínimo y la rejilla decide el resto. En la figura, la columna 1 tiene un encabezado de 36px de alto sobre una rejilla de 24px, sin margen inferior. Su pie cae 12px más allá de una línea de rejilla, el ajuste añade 12px, y la siguiente línea de cuerpo empieza en la rejilla, a la misma altura que la línea que tiene al lado en la columna 2. Con un marginBottom de 16px el pie estaría en 52px, y el ajuste lo redondearía a 72px.

Las bandas de flotantes mantienen la rejilla a su manera, descrita en la Etapa 4: una banda superior se redondea hacia arriba a líneas enteras de rejilla, y un flotante inferior se ancla de forma que la última línea base de su pie quede sobre la rejilla. El equilibrado de columnas añade su espacio en líneas enteras de rejilla, así que también la mantiene.

Nada compara una columna con otra. Todas las columnas empiezan en la rejilla y todos los ajustes vuelven a ella, de modo que las líneas de columnas adyacentes, y de páginas enfrentadas, coinciden sin una segunda pasada.

Renunciar al ajuste. snapToGrid: false conserva en su lugar el espacio exacto bajo un bloque. Se puede fijar en headings (para todos los niveles, para un nivel o para un estilo de encabezado), en un estilo de párrafo, en un estilo de recuadro y en codeStyle. El texto que sigue a un bloque así puede quedar fuera de la rejilla hasta el siguiente bloque que se ajusta. El texto de dentro de un recuadro sigue el interlineado propio de la caja y no tiene rejilla.

#Interfaz del backend

Hay exactamente una única referencia para medir el texto (el módulo de medida, sobre las métricas de fuente del canvas de Pretext), y todos los backends dibujan a partir del mismo VDT terminado que ese módulo produjo.

Es una decisión deliberada, y responde a una razón fundamental: la forma en que se mide el texto debe coincidir exactamente con la forma en que se dibuja. Imagina que la medición usara las métricas de fuente del canvas, pero un backend usara una biblioteca de PDF con tablas de kerning ligeramente distintas. La maquetación no coincidiría con la salida. Líneas que el motor midió como si cupieran en 320 px podrían desbordarse o quedarse cortas al dibujarlas. Cada píxel de desviación es una mentira. Al medir una sola vez y dibujar siempre a partir de la geometría resultante, los backends no pueden discrepar: los saltos de línea, las alturas de columna y la colocación de recursos quedan congelados en el VDT antes de que ningún backend se ejecute.

Por eso el backend de PDF, por ejemplo, no vuelve a medir el texto: consume un VDT terminado y traduce sus coordenadas en píxeles a puntos PDF. Las métricas del canvas son la referencia; el PDF es solo un medio de transporte. Los usuarios de renderToPdf (del paquete postext-pdf) pasan el mismo VDT que pasarían a renderToCanvas o renderToHtml, y se garantiza que las tres salidas coinciden.

#API pública

Los backends son funciones sin más que reciben un VDTDocument, no una jerarquía de clases. No existe una interfaz PostextBackend: solo los puntos de entrada de renderizado de cada salida y los helpers que necesita:

// Canvas (desde 'postext')
renderToCanvas(doc, options?): HTMLCanvasElement[];     // un canvas por página
renderPage(page, doc, options?): HTMLCanvasElement;     // una sola página
renderPageToCanvas(page, doc, canvas, options?): void;  // dibuja en un canvas existente
 
// Registro de imágenes de recurso del canvas — imágenes decodificadas por fileId
registerResourceImage(fileId, image, options?): void;   // options: { vector?, singleInk? }
unregisterResourceImage(fileId): void;
clearResourceImages(): void;
 
// HTML (desde 'postext')
renderToHtml(doc, options?): string;
renderToHtmlIndexed(doc, options?): HtmlRenderIndex;    // desglose por página / por bloque
 
// PDF (desde 'postext-pdf')
renderToPdf(doc, options): Promise<Uint8Array>;
 
// EPUB (desde 'postext-epub'), los capítulos de un libro en orden
renderToEpub(docs, options): Promise<Uint8Array>;
 
// SCORM (desde 'postext-scorm'), el contenido del escritor EPUB como paquete para un LMS
renderToScorm(docs, options): Promise<Uint8Array>;

Como los contenidos binarios de los recursos van fuera de banda, cada backend resuelve los fileId a su manera. El backend de canvas mantiene un registro de CanvasImageSource decodificados: la aplicación anfitriona registra cada mapa de bits o SVG una vez con registerResourceImage(fileId, image) y el renderizador lo consulta al dibujar. El backend HTML acepta un resolutor resourceImageUrl(fileId) en sus opciones y emite etiquetas <img> apuntando a las URLs (object URLs, data URIs, rutas de CDN) que devuelva el anfitrión. El backend PDF recibe un proveedor resourceBytes y incrusta los bytes reales; el escritor EPUB también recibe uno y guarda cada imagen una sola vez como archivo del libro. Un recurso de vídeo se dibuja como su portada, un mapa de bits más; el backend HTML recibe además un resolutor resourceVideoUrl(fileId) para los archivos de vídeo que reproduce, y el escritor EPUB guarda esos archivos en media/. El backend PDF dibuja la marca de reproducción y el código QR como trazados vectoriales sobre la portada y, con video.linkPoster activado, le pone encima una anotación de enlace URI que abre video.link; en un PDF etiquetado el enlace se une a la Figure de la portada, cuyo texto alternativo empieza por el nombre del tipo (Vídeo: …). Los recursos de tabla no necesitan nada de esto: su modelo viaja dentro del contenido, y todos los backends dibujan las celdas a partir de la geometría de tabla del VDT.

renderToHtmlIndexed merece una nota: además de la cadena HTML completa, devuelve un desglose por página y por bloque (HtmlRenderIndex) para que quien lo llama pueda comparar con un render anterior y parchear solo los subárboles del DOM cuyo HTML cambió de verdad; así funciona la vista previa en vivo del Sandbox.

#Backends

BackendMediciónRenderizadoEstado
CanvasPretext (métricas de fuente del canvas)Dibujo en mapa de bits sobre un HTMLCanvasElement (renderToCanvas, renderPage, renderPageToCanvas)Disponible
HTMLPretext (las mismas métricas que canvas)Nodos DOM con posición absoluta con CSS editorial (renderToHtml, renderToHtmlIndexed)Disponible
PDFConsume el VDT ya medido con PretextConstrucción de páginas PDF mediante pdf-lib con incrustación de fuentes por peso (renderToPdf en postext-pdf)Disponible
EPUBConsume el VDT ya medido con PretextArchivo EPUB 3: un documento XHTML por página impresa (maquetación fija) o por capítulo (maquetación fluida), con las fuentes incrustadas (renderToEpub en postext-epub)Disponible
SCORMConsume el VDT ya medido con PretextUn zip SCORM 1.2 o 2004 para un sistema de gestión del aprendizaje: las páginas o el texto del escritor EPUB en HTML5, un reproductor por SCO y ejercicios que envían su puntuación, si se han completado y si se han aprobado (renderToScorm en postext-scorm)Disponible
Folio (visor 3D)Consume el VDT ya medido con PretextCada página la pinta el backend de canvas (renderPageToCanvas) y se muestra como textura de una hoja de three.js, solo alrededor del pliego abierto (createFolioFromDocument en postext-folio)Disponible
Línea de comandosPretext sobre Skia (@napi-rs/canvas), en lugar del canvas del navegadorLos generadores de PDF, HTML y EPUB, las imágenes de página y Word, desde un terminal y sin navegador (postext-cli)Disponible

Los cuatro backends y el visor Folio 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. postext-epub es un paquete aparte por la misma razón, y solo hace falta para libros electrónicos; postext-scorm se apoya en él y empaqueta el contenido del escritor EPUB para las plataformas de aprendizaje, así que no cuenta como un backend propio. La línea de comandos no es un quinto backend: postext-cli ejecuta estos mismos generadores donde no hay navegador. Los archivos de Word están a su lado (postext/word): semanticDocx escribe los capítulos en Markdown y sus estilos en un .docx, sin maquetación; formattedDocx escribe las páginas de la misma cadena de VDTDocument de la que sale el PDF, y applyDocxEdits devuelve al Markdown las correcciones hechas en cualquiera de los dos; readDocx con wordToPostext leen como Markdown cualquier otro archivo de Word.

Dónde se ejecuta la composición: El motor mide el texto en un canvas, así que funciona allí donde se le da uno. En un navegador, el pipeline puede ejecutarse tanto en el hilo principal (buildDocument) como dentro de un Web Worker dedicado (createLayoutWorker, de postext/worker). El worker es la integración recomendada para las aplicaciones con interfaz, porque mantiene la medición y las pasadas de colocación fuera del hilo principal, admite la 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. Fuera del navegador, la línea de comandos ejecuta el mismo pipeline con Skia en el papel del canvas del navegador, de modo que un libro se parte en las mismas páginas en un terminal, en un script o en un servidor de compilación.

#Estrategia de rendimiento

La diferencia entre una herramienta lenta y una que parece mágica es un factor de 10x. Con una maquetación de 500 ms, el usuario ve un tirón cada vez que cambia una palabra. Una composición de 50 ms parece 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, compuestos de nuevo en cada edición, y varias veces seguidas cuando la composición necesita sus rondas. Los motores de videojuegos se enfrentan al mismo tipo de problema (procesar miles de objetos, como geometría, física, iluminación o IA, 60 veces por segundo) y lo resuelven no repitiendo el trabajo que ya han hecho. Postext aplica esa idea a lo que más le cuesta, que es medir texto.

#Principios

  1. 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.

  2. Pasadas enteras sobre medidas en caché. Una repetición de la pasada de colocación no busca qué ha cambiado: coloca el documento de nuevo desde el primer bloque. Es asumible porque medir, que es la parte cara, se cachea por párrafo (principio 5). Una pasada que no cambia nada del texto, las fuentes o el ancho de un párrafo lee sus líneas de la caché.

  3. Rondas acotadas. Cada tipo de repetición tiene un límite fijo (ver El controlador de pasadas), y el equilibrado de columnas tiene uno por documento además de uno por segmento, de modo que un libro de muchos capítulos no puede gastar pasadas sin fin. El coste de una composición se puede medir pasada a pasada: BuildDocumentOptions.onPass informa de cada pasada y de lo que ha tardado.

  4. Velocidad de Pretext. La medición de texto a 300–600x la velocidad del DOM significa que el motor puede permitirse volver a medir texto de forma especulativa (probando diferentes anchos de columna, puntos de separación silábica, ajustes del espaciado entre letras) sin bloquear el hilo principal.

  5. Caché de medición por capas. El módulo de medida (packages/postext/src/measure/) mantiene una caché de medición explícita cuya clave incluye el texto, las fuentes, el ancho y cada opción que afecta a la composición, por encima de la caché propia de prepare() de Pretext y de una caché cruda de anchuras de texto. Volver a medir un párrafo sin cambios cuesta una consulta a un mapa. Cuando cambian las caras de una familia, el motor descarta las anchuras de esa familia y la caché de Pretext, y una caché de medición descarta los bloques compuestos en ella en su siguiente consulta (lleva la generación con la que se llenó), de modo que las anchuras de glifo son correctas tras la carga de fuentes sin que el anfitrión tenga que llamar a nada; clearMeasurementCache() descarta todas las familias.

  6. Corte de líneas en la ruta crítica. La gestión de nodos activos de Knuth-Plass se reescribió por velocidad: el conjunto activo se compacta in situ a medida que los nodos se retiran, y los candidatos se deduplican por (línea, clase de fitness), de modo que solo sobrevive el nodo de menores demerits por clave. Los resultados algorítmicos son idénticos: los mismos conjuntos de cortes, calculados más rápido. El mismo algoritmo está escrito también en Rust (rust/crates/linebreak) y compilado a WebAssembly, incrustado en el paquete: hace la misma aritmética en el mismo orden, de dos a tres veces más rápido, y responde allí donde el módulo se ha compilado. En el hilo principal de una página se compila en segundo plano, y hasta entonces, o donde WebAssembly falta o está bloqueado, responde el de TypeScript. initLineBreaker() adelanta la compilación; useWasmLineBreaker(false) deja todos los párrafos en el de TypeScript.

  7. Builds fuera del hilo principal. El punto de entrada postext/worker ejecuta el pipeline completo dentro de un Web Worker dedicado. El hilo principal publica { content, config } y un AbortSignal; el worker registra las fuentes (transferidas como ArrayBuffers), ejecuta las pasadas de colocación y publica de vuelta el VDTDocument terminado. Una nueva llamada a build() cancela cooperativamente la anterior: el worker ejecuta buildDocumentAsync, que consulta un hook de cancelación una vez por bloque y entre pasadas y lanza BuildCancelledError, 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 objetos MathRender copiados con structured clone sobreviven a las reconstrucciones sin volver a rasterizarse.

  8. VDT de doble acceso. El árbol (pages > columns > blocks) proporciona acceso jerárquico para el trabajo que va página por página o columna por columna (el equilibrado de columnas mide el hueco al pie de cada columna). Un array plano paralelo blocks[] da acceso directo a todos los bloques estén donde estén (la ronda de números de nota lee de él la llamada que imprimió cada nota). Ambas vistas referencian los mismos objetos bloque (no hay duplicación, solo dos formas de recorrer los mismos datos).

#Volver a componer

No hay un punto de entrada incremental: ninguna llamada toma un VDT terminado y lo actualiza. Cuando cambia el contenido o la configuración (una pulsación de tecla, otro tamaño de página), el anfitrión llama otra vez a buildDocument y recibe un documento nuevo.

Lo que pasa de una composición a la siguiente es la medición. Crea una MeasurementCache una sola vez con createMeasurementCache() y pásala como tercer argumento en cada llamada a buildDocument. Un párrafo cuyo texto, fuentes, ancho y opciones de corte no han cambiado se lee de ella, de modo que un editor que recompone el documento en cada pulsación solo mide el párrafo en el que se está escribiendo. Un cambio de tamaño de página o de número de columnas cambia los anchos, y esos párrafos se miden de nuevo; los resultados de prepare() de Pretext siguen valiendo, porque dependen de la fuente y del texto y no del ancho, así que solo se ejecuta la aritmética barata de layout(). El análisis del markdown también se memoiza. También se conservan las páginas compuestas: una página compuesta cuya plantilla, Markdown, estilos y fuentes no han cambiado se reutiliza de la composición anterior (composed/pageMemo.ts), así que una edición en una página de un capítulo de páginas compuestas solo vuelve a componer esa página. Una página con una nota al pie, una referencia cruzada, una cita, un índice general o analítico o una tira de cómic se compone siempre de nuevo.

Para un editor en vivo, createLayoutWorker (desde postext/worker) hace todo esto fuera del hilo principal: el worker tiene una caché de medición que dura lo que dura él, y un build() más reciente cancela el que está en curso. Consulta Configuración → Caché de medidas y Ejecutar la composición en un Web Worker.

#Pruebas de rendimiento

En packages/postext/src/__bench__/ hay tres benchmarks. pnpm test se los salta; pnpm bench, lanzado en packages/postext, los ejecuta. chapter.bench.test.ts compone un capítulo del tamaño de los de un libro de texto (cuarenta secciones de encabezados, párrafos y listas) cinco veces contra una caché de medición vacía y cinco contra una ya llena, e informa de las dos medianas y del número de pasadas. cjkChapter.bench.test.ts compone el capítulo 1 de 红楼梦 y cuenta los caracteres enviados a measureText, para mantener el compositor CJK en un coste lineal respecto a la longitud del párrafo. composedPage.bench.test.ts mide un capítulo de veinte páginas compuestas: una composición en frío, una recomposición sin cambios, el cambio de un atributo de una página y una edición dentro de uno de sus marcos, cada una con la memoria de páginas compuestas y sin ella.

// De packages/postext/src/__bench__/chapter.bench.test.ts
const cold: number[] = [];
for (let i = 0; i < ROUNDS; i++) cold.push(time(() => buildDocument({ markdown }, config, createMeasurementCache())));
 
const warmCache = createMeasurementCache();
buildDocument({ markdown }, config, warmCache);
const warm: number[] = [];
for (let i = 0; i < ROUNDS; i++) warm.push(time(() => buildDocument({ markdown }, config, warmCache)));
 
const passes: BuildPassInfo[] = [];
const doc = buildDocument({ markdown }, config, createMeasurementCache(), { onPass: (p) => passes.push(p) });
// [bench] chapter: … pages, … passes (… ms), iterationCount=…; cold median … ms, warm median … ms (5 rounds)

Una salvedad, en aras de la honestidad: en Node no hay canvas, así que los benchmarks simulan la medición de texto y sus tiempos absolutos no son los del navegador. La proporción entre caché vacía y caché llena y el número de pasadas sí lo son, y una regresión en cualquiera de los dos se ve aquí antes que en el Sandbox. Los benchmarks del capítulo y de las páginas compuestas informan de los números y no hacen fallar la compilación; el chino comprueba que los caracteres medidos se quedan por debajo de tres veces la longitud del párrafo más largo. Ninguno se ejecuta en CI: las regresiones se detectan comparando los números entre ejecuciones cuando cambia una ruta caliente (como se hizo con la reescritura de nodos activos de Knuth-Plass). Todavía ninguna compilación falla por un tiempo más lento: una pérdida de velocidad se detecta midiendo y en la revisión.

#Flujo de datos

Flujo de datos a través del motorEl contenido y la configuración entran en la pasada de colocación, que estructura, mide y coloca cada bloque, flota los recursos y aplica las reglas de corte y de cohesión. buildDocument repite la pasada con indicaciones hasta que todas las rondas se asientan. El VDT terminado pasa al backend, que genera el HTML o el PDF.Contenido +Configpasada de colocación1estructurar2medir3colocar4flotantes5cohesiónse repite con indicaciones, cada ronda hasta su límiteVDT (terminado)Backend: renderizadoHTML / PDF
Flujo de datos de extremo a extremo: una pasada de colocación, repetida con indicaciones, y después el renderizado.

#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.

  • Edición WYSIWYG. Postext es un motor de maquetación, no un editor. Entra contenido, sale geometría. Construir una superficie de edición interactiva (gestión del cursor, selección, deshacer y rehacer, manejo de la entrada) es un problema completamente distinto. 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 volver a maquetar (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 maquetación sin estado (entra contenido, sale geometría), no un sistema de documentos colaborativo con resolución de conflictos, transformaciones operacionales ni consciencia multiusuario.
  • Archivos de fuente. El paquete postext carga las fuentes, pero no las trae consigo. prepareFonts carga antes de la primera composición todas las caras que piden una configuración y su texto, y una cara que el conjunto de fuentes no pudo dar figura entre los avisos del documento como fontFallback. Los archivos los pone quien integra el motor: una regla @font-face en la página, o una función resolve que dice dónde está cada cara. Consulta Configuración → Cargar las fuentes antes de componer.

#Apéndice: relación con los tipos existentes

Así se relacionan los tipos principales definidos en packages/postext/src/types.ts con la arquitectura descrita arriba:

TipoRol en la arquitectura
PostextContentPunto de entrada: la entrada al motor (estructuración del contenido)
PostextConfigControla todas las etapas de la pasada de colocación y las rondas que la rodean
ResourceRecurso tipado de mapa de bits, SVG, tabla o vídeo. Se convierte en un ResolvedResourceBlock durante la medición y en un VDTBlock en línea de tipo 'resource' (colocación 'here') o en un flotante de una banda de la página (colocación de recursos)
ResourceTypeGobierna la numeración tipada, los prefijos de pie, las etiquetas de referencia y el colocación flotante predeterminada (estructuración del contenido, colocación de recursos)
ResourcePlacementAjuste de la colocación flotante de cada recurso: position ('auto' / 'top' / 'bottom' / 'here') y span ('column' / 'page' / 'side'), además de rotate, width, align, columns, captionSide, shrink, minScale, captionMeasure, wrap, wrapGap y citingPage; cada campo recurre, si falta, al defaultPlacement del tipo, y se resuelve durante la colocación de recursos
PostextNoteEl motor no la lee. Las notas al pie se escriben en el markdown ([^id]) y se componen al pie de la columna que las cita o tras el capítulo; las notas al margen no están implementadas
PostextResourceObsoleto. El recurso del modelo de contenido heredado, conservado solo hasta que la última referencia del renderizador (VDTBlock.resource) migre al modelo Resource

#Dónde están los tipos del VDT

Los tipos del VDT (VDTDocument, VDTPage, VDTColumn, VDTBlock, VDTLine, VDTLineSegment, ResolvedResourceBlock, VDTResourceTableLayout, BoundingBox y compañía) están en packages/postext/src/vdt.ts, junto a los helpers de fábrica (createVDTDocument, createVDTPage, createVDTBlock, …) y computePageTextExtent. No existe un módulo separado de interfaz de backend: los puntos de entrada de renderizado descritos en Interfaz del backend se exportan directamente desde postext (canvas, HTML), postext-pdf (PDF) y postext-epub (EPUB).