Ir al contenido
Docxcelerate

Fundamentos

Templates

Compón un árbol de documento con TSX, y calcula la estructura con el mismo JavaScript que escribes en todas partes.

Una plantilla nombra un árbol de documento para que un proyecto pueda apuntar a él. Nada de lo que hay en ella se renderiza cuando se escribe — evaluar el JSX solo construye elementos, y eso es lo que permite a un componente decidir qué es más tarde, con los datos en la mano.

La forma

import { Document, Section, template } from "docxcelerate/template";
import { Greeting, NextSteps } from "./nodes/index.ts";
import type { DocumentData } from "./types.ts";

export const documentTemplate = template<DocumentData>(
  <Document id="tenancy-renewal" title="Tenancy Renewal">
    <Section id="opening" title="Opening">
      <Greeting />
    </Section>
    <Section id="closing" title="Closing">
      <NextSteps />
    </Section>
  </Document>,
);

Lo que conecta ese JSX con Docxcelerate en lugar de con React es tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "docxcelerate/template"
  }
}

Los workspaces generados ya lo traen, así que no hay nada que añadir al principio de cada archivo. Si estás incorporando Docxcelerate a un proyecto que apunta jsxImportSource a otro sitio, un comentario /** @jsxImportSource docxcelerate/template */ en la primera línea lo sobrescribe para ese archivo.

Estructura calculada

Una plantilla se escribe en el ámbito del módulo, donde todavía no hay datos, así que la estructura que depende de los datos va en un componente, no en la plantilla:

export const Enclosures: Section = () => {
  const [state] = useState((data: DocumentData) => ({ items: data.enclosures }));

  return (
    <Section id="enclosures" title="Enclosed">
      {state.items.map((item) => <Paragraph>{item.label}</Paragraph>)}
    </Section>
  );
};

Cualquier hijo falsy se omite, así que {condition && <Node />} se lee como en cualquier otro sitio. Los nodos sin id toman uno de donde están, y eso impide que un map exija nombres que no tienes.

El mismo .map() sirve cuando la lista solo se conoce por petición. Aquí recorre la colección, y al publicar llega al motor como un bucle — mira los bucles se publican como bucles más abajo.

Piezas reutilizables

Un componente es una función corriente, así que una envoltura de estilo de la casa también lo es. Los componentes aceptan hijos como cualquier otra cosa:

export const Letterhead: Section<{ children?: Yield }> = ({ children }) => (
  <Section id="letterhead" title="">
    <Image id="logo" src="assets/logo.png" alt="Ashcroft Housing" />
    {children}
  </Section>
);

Un hook compartido es la otra mitad de esto. Como useSetPrompts fija prompts en lo que produzca el componente que lo llama, el estilo de la casa puede aplicarse a un nodo que el hook ni siquiera posee:

export function useHouseVoice() {
  useSetPrompts({
    systemPrompt: "You write for a housing association. Plain English, second person.",
  });
}

Cualquier componente que llame a useHouseVoice() recibe ese prompt de sistema en el nodo que devuelve, y aún puede sobrescribirlo con una prop.

Proyectos de documento

Un proyecto de documento ata una plantilla a su estilo y a sus datos de vista previa mediante un único punto de entrada, document.project.ts:

import { defineDocumentProject } from "docxcelerate/document";
import { documentTemplate } from "./document.tsx";
import { documentStyle } from "./document-style.ts";
import type { DocumentData } from "./types.ts";

export default defineDocumentProject<DocumentData>({
  id: "tenancy-renewal",
  name: "Tenancy Renewal",
  version: "1.0.0",
  template: documentTemplate,
  style: documentStyle,
  previewData: {
    residentName: "Avery Mitchell",
    propertyRef: "Flat 4, Ashcroft House",
  },
});

previewData es contra lo que resuelve la app de vista previa. Manténlo realista — los nombres cortos y las ciudades de relleno ocultan problemas de maquetación que los datos reales sí exponen.

Qué cambia al publicar

Todo lo anterior da por hecho que tienes los datos. Cuando compilas con los datos en la mano — una vista previa, un empaquetado local, un documento escrito sobre la marcha — un if significa exactamente lo que dice, y un .map() recorre la lista que se le dio.

Publicar en un motor es distinto. El artefacto se compila una vez, contra valores de relleno para una petición que nadie ha hecho todavía, y se guarda. Una decisión que dependa de los datos de la petición no se puede zanjar durante esa compilación; tiene que viajar al motor y tomarse por documento.

De ahí salen tres reglas, y la compilación hace cumplir las tres en lugar de dejar que un documento equivocado llegue a quien lo recibe.

Calcula por documento, no por compilación

Interpolar un valor funciona al publicar: se convierte en el token que el motor sustituye.

<Paragraph id="greeting">Hello {state.name},</Paragraph>
// publicado como: "Hello {{data.name}},"

Calcular sobre él no, porque el valor todavía no se conoce. Formatear una moneda, sumar una lista, comparar una fecha — eso necesita un deriver, que el motor ejecuta por documento:

<Paragraph
  id="balance"
  derivers={[
    derive("currencyLabel", { output: "balanceLabel", inputs: [dataRef("balanceDue")] }),
  ]}
>
  Your balance is {"{{derived.balanceLabel}}"}.
</Paragraph>

useFormat y useDeriver calculan durante la compilación, que es lo correcto siempre que el valor ya se conozca. Echa mano de un deriver cuando no.

Los bucles se publican como bucles

Una bifurcación tiene dos ramas y las dos se pueden publicar. Un bucle tiene tantas como entradas traiga la petición, y ese número no lo sabe nadie hasta que se escribe un documento — así que lo que se guarda es el bucle en sí. Lo que se escribe es el .map() de siempre:

const [visits] = useState((data: TenancyData) => data.visits);

return visits.map((visit) => <Paragraph id="visit">Visit: {visit.label}</Paragraph>);

Con datos reales esto es el .map() de la biblioteca estándar: recorre la colección de inmediato, así que las vistas previas enseñan la repetición en vez de una descripción de ella. Al publicar no se puede recorrer, así que el sustituto intercepta esa misma llamada, ejecuta el cuerpo una vez contra un sustituto de una entrada, y el bucle llega intacto al motor.

Aquí nadie escribe un token {{ctx…}}. La entrada que recibió el cuerpo sabe la ruta que representa, así que la referencia se escribe sola — y las pasadas se nombran por posición en ambos caminos, de modo que un id significa lo mismo en una vista previa y en el documento que recibe alguien.

Todo lo que tenga que mirar las entradas antes de poder escribir el cuerpo — .filter(), .sort(), .length, un bucle for — no se puede responder al publicar. Eso remite a un deriver, que se ejecuta donde están los datos.

Las bifurcaciones multiplican

Publicar una bifurcación guarda las dos ramas, cada una bajo la condición que la selecciona. Anidar bifurcaciones multiplica lo que carga un documento, así que hay un límite — branchLimit, 32 por defecto — y pasarlo hace fallar la compilación en vez de producir un artefacto calladamente enorme. Sube la decisión a un deriver que dé un solo valor, o eleva el límite si el documento de verdad es así de condicional.


Editar esta página en GitHub ↗ Leer esta página en Markdown