Основы
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