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 resolveHeader and resolveFooter functions evaluate the current page number against conditions defined in the JSON schema ("first", "last", "even", "odd", "all").

Here is the exact API reference pulled from @papercast/engine/src/resolver.ts:

export function resolveHeader(
  pageNumber: number,
  totalPages: number,
  doc: DocumentSchema["document"]
): string | undefined {
  // 1. Check strict page overrides first
  const override = doc.pageOverrides?.[pageNumber.toString()]?.headerId;
  if (override !== undefined) {
    if (override === null) return undefined;
    return override;
  }

  const entries = Object.entries(doc.headers || {});

  // 2. Evaluate conditions by priority: first/last > even/odd > all
  let matchedKey: string | undefined = undefined;

  if (pageNumber === 1) {
    matchedKey = entries.find(([_, h]) => h.condition === "first")?.[0];
  } else if (pageNumber === totalPages) {
    matchedKey = entries.find(([_, h]) => h.condition === "last")?.[0];
  }

  if (!matchedKey) {
    const isEven = pageNumber % 2 === 0;
    matchedKey = entries.find(
      ([_, h]) => h.condition === (isEven ? "even" : "odd")
    )?.[0];
  }

  if (!matchedKey) {
    matchedKey = entries.find(
      ([_, h]) => h.condition === "all" || !h.condition
    )?.[0];
  }

  // 3. Fallback to default
  if (!matchedKey && doc.headerDefaultId) {
    return doc.headerDefaultId;
  }

  return matchedKey;
}
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".