Documentation
Calculations & Logic
How variables, math, and templates are resolved.
PaperCast supports dynamic logic injection using the resolver.ts module. The schema is static JSON, but it can describe dynamic conditions—such as which header to show on the last page.
Dynamic Headers and Footers
The resolvePageRegion function evaluates the current page number against conditions defined in the JSON schema ("first", "last", "even", "odd", "all", or a "custom" expression).
Here is the exact API reference pulled from @papercast/engine/src/resolver.ts:
export function resolvePageRegion(
regionType: "header" | "footer",
pageNumber: number,
totalPages: number,
doc: DocumentSchema["document"]
): string | undefined {
const regions: Record<string, PageRegion> =
regionType === "header" ? doc.headers || {} : doc.footers || {};
const overrides = doc.pageOverrides?.[pageNumber.toString()] || {};
const overrideId = overrides[`${regionType}Id`];
// 1. Check page overrides
if (overrideId !== undefined) {
if (overrideId === null) return undefined;
return overrideId;
}
const entries = Object.entries(regions);
// 2. Evaluate conditions by priority: first/last > even/odd > custom > all
const priorityOrder = ["first", "last", "even", "odd", "custom", "all"];
for (const priority of priorityOrder) {
const match = entries.find(([_, region]) => {
const cond = region.condition || "all";
const condType = typeof cond === "string" ? cond : cond.type;
if (condType === priority) {
return evaluateCondition(cond, pageNumber, totalPages);
}
return false;
});
if (match) return match[0];
}
return undefined;
}
Custom Conditions
You can use the custom condition type to execute any sandboxed JavaScript expression that returns a boolean, utilizing the pageNumber and totalPages variables.
"condition": {
"type": "custom",
"expression": "pageNumber !== 1 && pageNumber !== totalPages"
}
Two-Pass Pagination
Notice the totalPages argument. Because we don't know the total number of
pages until pagination finishes, PaperCast performs a Two-Pass pagination.
It paginates once to find totalPages, then paginates a second time to inject
the correct "last" page headers and footers.
Data Binding & Variable Injection
PaperCast allows you to inject dynamic data from a JSON payload into your document using the bind property on a DocNode.
The DataBinding interface defines how this works:
export interface DataBinding {
path: string; // e.g. "invoice.total"
mode?: "single" | "repeat";
itemAlias?: string; // used when iterating over arrays
}
Resolution Logic
During the rendering phase, components like the TextNode look for the bind property. If it exists, they call a data resolver utility that uses lodash.get (or similar path resolution logic) to query the DocumentSchema["data"] object.
For example, if the AST contains:
{
"type": "text",
"content": "Total: ",
"bind": { "path": "invoice.totalAmount" }
}
And the root document data is {"invoice": {"totalAmount": "$1,200.00"}}, the engine will render "Total: $1,200.00".
Conditional Visibility (visibleIf)
You can conditionally show or hide any node in the AST before pagination occurs by using the visibleIf property. This property accepts a JavaScript-like expression string that is evaluated against the data payload. If the expression evaluates to a falsy value, the node (and all of its children) are completely pruned from the document tree.
This is extremely powerful for building data-driven documents where entire sections should only exist if certain data is present.
Example
{
"type": "row",
"visibleIf": "invoice.discount > 0",
"children": [
{
"type": "text",
"props": { "literal": "Discount Applied!" }
}
]
}
In this example, if the root data is {"invoice": {"discount": 0}}, the entire row node is removed before the pagination engine even measures it, ensuring no empty space is left behind.