Zum Inhalt springen
Docxcelerate

Grundlagen

Templates

Einen Dokumentbaum mit TSX zusammensetzen — und Struktur mit demselben JavaScript berechnen, das Sie überall sonst schreiben.

Ein Template benennt einen Dokumentbaum, damit ein Projekt darauf zeigen kann. Beim Schreiben wird nichts davon gerendert — das Auswerten des JSX baut nur Elemente, und genau das erlaubt es einer Komponente, später zu entscheiden, was sie ist, mit Daten in der Hand.

Die Form

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>,
);

Was dieses JSX mit Docxcelerate statt mit React verdrahtet, ist die tsconfig.json:

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

In angelegten Workspaces steht das bereits, oben in einer Datei ist also nichts zu ergänzen. Wenn Sie Docxcelerate zu einem Projekt hinzufügen, das jsxImportSource anderswohin zeigen lässt, überschreibt ein Kommentar /** @jsxImportSource docxcelerate/template */ in Zeile eins das für diese Datei.

Berechnete Struktur

Ein Template steht auf Modulebene, wo es noch keine Daten gibt. Struktur, die von Daten abhängt, gehört deshalb in eine Komponente, nicht in das Template:

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>
  );
};

Alles Falsy unter den Kindern wird übersprungen, {condition && <Node />} liest sich also wie überall sonst. Nodes ohne id bekommen eine aus ihrer Position, und das hindert ein map daran, Namen zu verlangen, die Sie nicht haben.

Dasselbe .map() funktioniert auch, wenn die Liste erst je Anfrage feststeht. Hier läuft es über die Sammlung, und beim Veröffentlichen erreicht es die Engine als Schleife — siehe Schleifen werden als Schleifen veröffentlicht weiter unten.

Wiederverwendbare Teile

Eine Komponente ist eine gewöhnliche Funktion, eine Hülle im Hausstil also ebenso. Komponenten nehmen Kinder wie alles andere:

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

Ein geteilter Hook ist die andere Hälfte davon. Weil useSetPrompts Prompts an dem Node setzt, den die aufrufende Komponente liefert, lässt sich Hausstil auf einen Node anwenden, der dem Hook gar nicht gehört:

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

Jede Komponente, die useHouseVoice() aufruft, bekommt diesen System-Prompt an dem Node, den sie zurückgibt — und kann ihn per Prop weiterhin überschreiben.

Dokumentprojekte

Ein Dokumentprojekt bindet ein Template über einen einzigen Entrypoint, document.project.ts, an seinen Stil und seine Vorschaudaten:

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",
  },
});

Gegen previewData löst die Vorschau-App auf. Halten Sie es realistisch — kurze Namen und erfundene Ortsnamen verdecken Layout-Probleme, die echte Daten sichtbar machen.

Was sich beim Veröffentlichen ändert

Alles oben setzt voraus, dass Sie die Daten haben. Wenn Sie mit Daten in der Hand bauen — eine Vorschau, ein lokales Pack, ein spontan geschriebenes Dokument —, bedeutet ein if genau das, was dasteht, und ein .map() läuft über die Liste, die es bekommen hat.

An eine Engine zu veröffentlichen ist etwas anderes. Das Artefakt wird einmal gebaut, gegen Stellvertreterwerte für eine Anfrage, die noch niemand gestellt hat, und gespeichert. Eine Entscheidung, die von Anfragedaten abhängt, kann bei diesem Build nicht fallen; sie muss zur Engine reisen und je Dokument getroffen werden.

Daraus folgen drei Regeln, und der Build erzwingt alle drei, statt ein falsches Dokument bis zur Empfängerin oder zum Empfänger durchzulassen.

Je Dokument rechnen, nicht je Build

Einen Wert zu interpolieren funktioniert beim Veröffentlichen: Er wird zu dem Token, das die Engine ersetzt.

<Paragraph id="greeting">Hello {state.name},</Paragraph>
// veröffentlicht als: "Hello {{data.name}},"

Auf ihm zu rechnen nicht, denn der Wert ist noch nicht bekannt. Eine Währung formatieren, eine Liste summieren, ein Datum vergleichen — dafür braucht es einen Deriver, den die Engine je Dokument ausführt:

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

useFormat und useDeriver rechnen stattdessen während des Builds, was richtig ist, wann immer der Wert schon bekannt ist. Zum Deriver greifen Sie, wenn er es nicht ist.

Schleifen werden als Schleifen veröffentlicht

Eine Verzweigung hat zwei Arme, und beide lassen sich veröffentlichen. Eine Schleife hat so viele, wie die Anfrage Einträge hat, und diese Zahl kennt niemand, bevor ein Dokument geschrieben wird — gespeichert wird also die Schleife selbst. Geschrieben wird dafür das gewöhnliche .map():

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

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

Mit echten Daten ist das schlicht das .map() aus der Standardbibliothek: es läuft sofort über die Sammlung, Vorschauen zeigen also die Wiederholung statt einer Beschreibung davon. Beim Veröffentlichen geht das nicht, also fängt der Platzhalter denselben Aufruf ab, lässt den Rumpf einmal gegen einen Platzhalter für einen Eintrag laufen, und die Schleife erreicht die Engine unversehrt.

Ein {{ctx…}}-Token schreibt hier niemand. Der Eintrag, den der Rumpf bekommen hat, kennt den Pfad, für den er steht, also schreibt sich die Referenz selbst — und die Durchläufe werden auf beiden Wegen nach ihrer Position benannt, damit eine id in der Vorschau dasselbe bedeutet wie im Dokument, das eine Empfängerin erhält.

Alles, was die Einträge erst ansehen muss, bevor der Rumpf geschrieben werden kann — .filter(), .sort(), .length, eine for-Schleife — lässt sich beim Veröffentlichen nicht beantworten. Das verweist auf einen Deriver, der dort läuft, wo die Daten sind.

Verzweigungen multiplizieren

Eine veröffentlichte Verzweigung speichert beide Arme, jeden unter der Bedingung, die ihn auswählt. Verschachtelte Verzweigungen multiplizieren, was ein Dokument mitträgt, deshalb gibt es ein Limit — branchLimit, standardmäßig 32 — und es zu überschreiten lässt den Build scheitern, statt ein still und leise riesiges Artefakt zu erzeugen. Heben Sie die Entscheidung in einen Deriver, der einen Wert liefert, oder erhöhen Sie das Limit, wenn das Dokument wirklich so bedingt ist.


Diese Seite auf GitHub bearbeiten ↗ Diese Seite als Markdown lesen