Перейти к содержимому
Docxcelerate

Основы

Templates

Собирайте дерево документа на TSX и вычисляйте структуру тем же JavaScript, который пишете везде.

Шаблон называет дерево документа, чтобы проект мог на него указать. Ничего из него не отрисовывается в момент написания — вычисление JSX только строит элементы, и именно это позволяет компоненту решить, чем он является, позже, уже с данными на руках.

Форма

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

С Docxcelerate, а не с React, этот JSX связывает tsconfig.json:

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

В созданных workspace это уже прописано, так что вверху файла добавлять нечего. Если вы подключаете Docxcelerate к проекту, где jsxImportSource указывает в другое место, комментарий /** @jsxImportSource docxcelerate/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>
  );
};

Всё ложное среди потомков пропускается, поэтому {condition && <Node />} читается так же, как и везде. Узлы без id получают его из своего места, и это избавляет map от требования имён, которых у вас нет.

Тот же .map() работает и когда список известен только на каждый запрос. Здесь он обходит коллекцию, а при публикации доходит до движка как цикл: см. циклы публикуются как циклы ниже.

Переиспользуемые части

Компонент — обычная функция, значит и обёртка в фирменном стиле тоже. Компоненты принимают потомков, как и всё остальное:

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

Общий хук — вторая половина этого. Поскольку useSetPrompts задаёт промпты на том узле, который отдаёт вызывающий компонент, фирменный стиль можно применить к узлу, который хуку вовсе не принадлежит:

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

Любой компонент, вызвавший useHouseVoice(), получает этот системный промпт на возвращаемом узле — и всё ещё может переопределить его пропсом.

Проекты документов

Проект документа связывает шаблон с его стилем и данными предпросмотра через единственную точку входа, 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 разрешает приложение предпросмотра. Держите их реалистичными — короткие имена и выдуманные города скрывают проблемы вёрстки, которые настоящие данные как раз обнажают.

Что меняется при публикации

Всё вышеописанное исходит из того, что данные у вас есть. Когда вы собираете с данными на руках — предпросмотр, локальная упаковка, документ, написанный тут же, — if означает ровно то, что написано, а .map() проходит по тому списку, который ему дали.

Публикация в движок устроена иначе. Артефакт собирается один раз, на подставных значениях для запроса, которого ещё никто не сделал, и сохраняется. Решение, зависящее от данных запроса, на этой сборке принять нельзя: оно должно доехать до движка и приниматься на каждый документ.

Отсюда следуют три правила, и сборка обеспечивает все три, вместо того чтобы позволить неверному документу дойти до получателя.

Считайте на документ, а не на сборку

Подстановка значения при публикации работает: она становится токеном, который подставит движок.

<Paragraph id="greeting">Hello {state.name},</Paragraph>
// опубликовано как: "Hello {{data.name}},"

А вычисления над ним — нет, потому что значение ещё неизвестно. Отформатировать сумму, сложить список, сравнить дату — для этого нужен деривер, который движок выполняет на каждый документ:

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

useFormat и useDeriver считают во время сборки, и это правильно всегда, когда значение уже известно. За деривером тянитесь тогда, когда оно неизвестно.

Циклы публикуются как циклы

У ветвления две ветки, и обе можно опубликовать. У цикла их столько, сколько записей в запросе, и это число никому не известно, пока документ не пишется, — так что сохраняется сам цикл. Пишется при этом обычный .map():

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

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

С настоящими данными это просто .map() из стандартной библиотеки: он сразу обходит коллекцию, поэтому предпросмотры показывают повторение, а не его описание. При публикации обойти нечего, поэтому заглушка перехватывает тот же вызов, прогоняет тело один раз против заглушки для одной записи, и цикл доходит до движка нетронутым.

Токен {{ctx…}} здесь никто не пишет. Запись, которую получило тело, знает путь, за который она стоит, так что ссылка пишется сама — а проходы на обоих путях именуются по позиции, поэтому id значит одно и то же и в предпросмотре, и в документе, который получит адресат.

Всё, что должно сперва посмотреть на записи, прежде чем тело можно написать — .filter(), .sort(), .length, цикл for, — при публикации ответить нельзя. Это отсылает к дериверу, который выполняется там, где лежат данные.

Ветвления умножаются

Публикация ветвления сохраняет обе ветки, каждую под условием, которое её выбирает. Вложенные ветвления умножают то, что несёт в себе документ, поэтому есть предел — branchLimit, по умолчанию 32, — и его превышение роняет сборку, вместо того чтобы тихо породить огромный артефакт. Поднимите решение в деривер, дающий одно значение, или поднимите предел, если документ действительно настолько условный.


Изменить эту страницу на GitHub ↗ Читать эту страницу в Markdown