Documentation menu

Documentation / Platform services / print

Print

Package
@wabbit/tome-print
Version
v0.1.8
npm install @wabbit/tome-print
First install from our registry?
  1. Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.

  2. Add the registry and your token to the .npmrc at the root of your project, with your token in place of YOUR_TOKEN:

    @wabbit:registry=https://npm.wabbit.com/
    //npm.wabbit.com/:_authToken=YOUR_TOKEN
Platform servicesStable

Tome platform print layer — document assembly, template registry, and Pandoc-driven PDF generation for structured reports.

Overview

Document assembly, a print-template registry, and Pandoc-driven PDF generation for structured reports (e.g. @wabbit/tome-deals' proposal/quote documents). domain layer per root ARCHITECTURE.md — zero @wabbit/* dependencies.

Relationship to `@wabbit/tome-deals` — read this before wiring invoices. This package never calls registerLayer and exposes no job-dispatch function, so installing it does not make deals print anything. Deals prints only through the printAdapter you pass to its layer: to print deal documents, call this package's mapDocument/assembleDocument (or the CLI) from inside that adapter.

Peer dependencies

No peer dependencies — this is a standalone Node package (document assembly + a CLI wrapper around two external binaries). It ships zero React and zero .tsx files.

External requirements — two system binaries, not one: the @wabbit/tome-print/cli build path (tome-print bin → src/cli/build.ts) needs both:

  1. Pandoc — Markdown → JSON AST (pandoc -t json). findPandoc() tries pandoc on PATH first, then known Windows install locations (%LOCALAPPDATA%\Pandoc\pandoc.exe, C:\Program Files\Pandoc\pandoc.exe); if none resolve, the CLI exits with an install-url error rather than a cryptic spawn failure.
  2. WeasyPrint (Python) — HTML → PDF, invoked as a bare weasyprint shell command with no path-search fallback. If it's missing, build.ts catches the execSync failure and prints pip install weasyprint, then preserves the intermediate HTML at .tome-print-tmp/report.html next to the target PDF so the run isn't a total loss. On success the CLI deletes .tome-print-tmp/ and also writes a <output>.debug.html copy beside the PDF.

Neither binary is vendored by pnpm add — both are separate host installs.

60-second quickstart

import { registerPrintTemplate, assembleDocument, mapDocument, extractMeta } from '@wabbit/tome-print'

registerPrintTemplate('quote-line-item', (block, meta) => `<tr><td>${String(block.data.label)}</td></tr>`)

// pandocAst = JSON.parse(output of `pandoc -t json report.md`)
const { meta, blocks } = mapDocument(pandocAst)         // @wabbit/tome-print/mapper — frontmatter + AST → PrintBlock[]
const html = assembleDocument(blocks, meta, 'report.css') // @wabbit/tome-print/document — 3 args: blocks, meta, CSS href for the <link>
const sameMeta = extractMeta(pandocAst.meta)            // takes the AST's `meta` object, not the whole AST

assembleDocument returns an HTML string; turning it into a PDF is your job (the CLI hands it to WeasyPrint). A block whose slug has no registered template renders as an HTML comment and logs a warning — it does not throw. The CLI also runs an internal chart-injection pass (a data-chart block after each matching table) that is not exported, so library callers who go straight from mapDocument to assembleDocument get no auto-charts.

CLI (Markdown → PDF via Pandoc + WeasyPrint, positional args — no subcommand, no flags):

pnpm exec tome-print ./report.md ./report.pdf   # output arg optional — defaults to <input-basename>.pdf beside the input

The compiled CLI opens with #!/usr/bin/env node, so the tome-print bin runs directly on macOS/Linux as well as Windows. You can also call the file with Node directly: node node_modules/@wabbit/tome-print/dist/cli/build.js ./report.md. The CLI is ESM-only in practice (it reads import.meta.url to find report.css).

Set TOME_PRINT_WORDMARK=/abs/path/to/logo.svg before running the CLI to brand the editorial templates with a real wordmark; unset (or a missing file) falls back to a plain-text brand mark — the CLI warns but does not fail.

Public API

ExportSubpathDescription
DocumentMeta, PrintBlock, PrintTemplate@wabbit/tome-print/typesCore shapes — PrintTemplate = (block, meta) => string
registerPrintTemplate(slug, template)@wabbit/tome-print/registryRegister a template function for a block slug
getPrintTemplate(slug), hasPrintTemplate(slug), getAllPrintTemplates()@wabbit/tome-print/registryRegistry lookups
renderBlock(block, meta)@wabbit/tome-print/registryRender one block via its registered template
assembleDocument(blocks, meta, cssPath)root (@wabbit/tome-print/document)Walk a PrintBlock[] and assemble the final document HTML; cssPath is the stylesheet href written into the <link> tag (relative or absolute — the CLI resolves it to a temp-dir-local report.css before handing off to WeasyPrint)
mapDocument(pandocAst)root (@wabbit/tome-print/mapper)Map a Pandoc AST to { meta, blocks }
extractMeta(pandocMeta)root (@wabbit/tome-print/mapper)Pull DocumentMeta out of a document's frontmatter — pass the Pandoc AST's meta object (ast.meta), not the whole AST. Defined in mapper/frontmatter.ts but re-exported from both the root barrel and @wabbit/tome-print/mapper; @wabbit/tome-print/mapper/frontmatter is not a subpath in the exports map
renderInline, renderInlines@wabbit/tome-print/richtextInline-node renderers
renderBlockNode, renderBlocks@wabbit/tome-print/richtextBlock-node renderers
tome-print (bin)bin only — @wabbit/tome-print/cli is not an importable subpathPandoc-driven Markdown → PDF build CLI (dist/cli/build.js) — parses <input.md> via pandoc -t json, maps the AST, injects charts, assembles HTML, then renders to PDF
Template registration side-effect@wabbit/tome-print/templatesimport '@wabbit/tome-print/templates' registers every built-in template; assembleDocument also imports this internally

Built-in templates (registered via @wabbit/tome-print/templates's side-effect barrel)

editorial-opener, prose-section, pull-quote, signal-data-table, editorial-bridge, editorial-colophon, metric-strip, data-chart — one file per template under src/templates/, each calling registerPrintTemplate() at import time. A consumer's own block templates (e.g. a quote-line-item slug from a deal artifact) register the same way, either from an app-owned module imported once at startup or alongside @wabbit/tome-print/templates's barrel.

Server / client posture

Not applicable — this package has no .tsx files and no React dependency at all. Only the CLI touches Node APIs (node:child_process to run Pandoc and WeasyPrint, node:fs for the temp files). Everything you can import — the registry, assembleDocument, the mapper, the richtext renderers and the templates — is plain string work with no Node or browser APIs, so it is safe in a build script, a Payload server hook, or a Node CLI. The registry is a module-level Map, so registrations are per process.

Testing

pnpm --filter @wabbit/tome-print test runs the Vitest suite in tests/ (registry, frontmatter, AST mapping, richtext, templates, chart injection, assembleDocument). None of it needs Pandoc or WeasyPrint; the tests feed hand-built Pandoc ASTs.

Extending

New print templates: registerPrintTemplate(slug, (block, meta) => htmlString) in a module of your own, and import that module once before you call assembleDocument (the registry is a shared Map, so the order of registration does not matter, only that it happens first). Registering a built-in slug again replaces the built-in. Inside this package, a new built-in template also gets a line in src/templates/index.ts, which assembleDocument imports for its side effects. New document sources beyond Pandoc AST would need a new @wabbit/tome-print/mapper sibling implementing the same { meta, blocks } output shape consumed by assembleDocument.

Design history

  • Original design: Pandoc + WeasyPrint CLI orchestration behind a registerPrintTemplate/assembleDocument seam, chosen so document generation is pure Node with no React or browser dependency.

Exports

Import path
@wabbit/tome-print
@wabbit/tome-print/types
@wabbit/tome-print/registry
@wabbit/tome-print/templates
@wabbit/tome-print/richtext
@wabbit/tome-print/mapper

Changelog

0.1.x

10 fixes (0.1.1 – 0.1.8)
  • 0.1.8
    The changelog, README and license text now use American spellings.

    No code or behavior changes.

  • 0.1.7

    README: subpath and file mentions are written package-qualified (for example @wabbit/tome-x/render) or as code text, so crawlers no longer resolve them as site URLs.

  • 0.1.6
    Published packages now contain compiled JavaScript and type declarations under a one-line license banner, and no longer include source maps.

    What you install: one compiled .js (ESM) and .cjs (CommonJS) file per source module, its .d.ts / .d.cts declarations, and the stylesheets, fonts and other assets a package already shipped. Every JavaScript module opens with a comment naming the package and its license: /*! @wabbit/<package> — © Wabbit, LLC. Wabbit Tome Commercial License (see LICENSE.md). Not for redistribution. */.

    The .map files and the sourceMappingURL comments that pointed at them are gone, which roughly halves the size of each tarball.

    Debugging: the code is still unbundled and unminified, one readable file per module, so a stack trace points at real code with real names. Line numbers in a stack trace are one higher than before, because of the banner line.

    A 'use client' directive stays the first statement of its module (the banner is a comment above it), so React Server Component boundaries are unchanged.

    No API change, no runtime behavior change, and nothing to do on upgrade.

    In @wabbit/tome-blocks-gallery, the source snapshots extractGallerySource writes from an installed pack leave out the license banner line, so a component or config snapshot starts at the code and a paid block's preview shows its first 15 lines of real code.

  • 0.1.5

    The tome-print CLI now opens with a #!/usr/bin/env node shebang, so the bin starts on macOS and Linux instead of being handed to the shell.

  • 0.1.5
    customer-facing wording: internal references removed from admin descriptions, error messages and block metadata.

    injectCharts now ships a neutral fictional sample dataset in place of one engagement's figures; its section-heading rules for the projection and launch charts now match "What this means going forward" and "How the launch result was built".

  • 0.1.4
    dist now raw-Node loadable: relative specifiers get explicit extensions post-build.

    build gains && node ../../scripts/fix-dist-extensions.mjs --strict as its last step, joining the 13 packages that already ran it. tsup builds bundle: false and emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESM ERR_MODULE_NOT_FOUND; CJS worse, require('./x') finds the ESM .js twin and Node 22+ require(esm) then dies on that file's own extensionless import). Every consumer outside a bundler hit this: the payload CLI under plain node, generate:types, generate:importmap, ops scripts, codegen tools.

    No source changes, no API changes, and bundler consumers are unaffected — extensioned relative specifiers are universally resolvable.

    Two supporting changes made the wiring possible, both in repo scripts rather than package source. fix-dist-extensions.mjs now skips bundler-asset specifiers (.css, .module.css, .scss, fonts, images, shaders) by explicit extension allowlist instead of reporting them as unresolvable — that single gap is why the 13 prior adopters were exactly the 13 packages that ship no CSS, since --strict exited 1 on any package with a relative stylesheet import.

    Dotted MODULE names (./config.meta, ./x.variants, ./y.demo) are deliberately NOT treated as assets and still get .js/.cjs appended. assert-node-loadable.mjs gained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands under node_modules is a peer SKIP (next@15 has no exports map, so next/image fails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it as SyntaxError: Unexpected token '.' raised from inside the stylesheet).

    Verified before/after on four packages built one at a time: print 8 FAIL → 0, readout 22 FAIL → 0, ai 3 FAIL → 0, gamification 2 FAIL → 0 (its failure was the other signature — a directory import missing /index). cop was already clean on a fresh build, so the audit's "27 of 46 fail" figure includes at least one package whose local dist was merely stale.

  • 0.1.4
    Manifest metadata: homepage, bugs, engines.

    All 46 publishable manifests were missing the three fields a consumer sees before any code (2026-09-01 sale-readiness audit §6). Metadata only — no source, no build, no runtime change.

    • homepage deep-links to that package README on GitHub (.../tree/main/packages/<dir>#readme). Without it a registry page links to the monorepo root and the reader has to guess which of 46 folders they want.
    • bugs.url points at the repo issue tracker, so a paying customer has a place to report a defect that is not email.
    • engines.node is >=22, matching the root engines and .nvmrc set the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use for node --test, and a package installed on Node 20 fails at a runtime the installer cannot connect back to the version.

    The forcing function ships with the change: scripts/assert-manifest-metadata.mjs (root pnpm assert:manifest-metadata, wired into platform-discipline.yml beside assert:license-metadata) fails when any publishable manifest lacks description, repository.directory matching its own folder, homepage, bugs, engines.node equal to the repo floor, license, files or sideEffects. It reported 138 violations before this change and 0 after.

  • 0.1.3
    Every package now declares an explicit sideEffects field (38 added; motion/engine/forms already correct).

    Registration-bearing modules (render files' registerRenderer, blocks/*/index.ts defineBlock self-registration, widget register.ts files, productHooks, permission self-registrations, print templates, chrome built-in variants) are listed so bundlers can tree-shake everything else WITHOUT dropping import-time registrations — previously the field was unset, which blocked cross-module tree-shaking through the barrels entirely. Never blanket false on a package with registration or CSS.

  • 0.1.2
    Docs-manifest emitter pipeline (W3 ship-readiness).

    @wabbit/tome-blocks-core now ships a standalone Node ESM CLI at scripts/emit-docs-manifests.mjs that emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free <pkg>/meta block-usage barrels, package.json exports maps, and CHANGELOG.md. It is the docs-pipeline sibling of the gallery source extractor and is consumed by host sites at prebuild: node node_modules/@wabbit/tome-blocks-core/scripts/emit-docs-manifests.mjs --output-dir <dir> --scope <scope.json>.

    To let the emitter import block metadata uniformly without dragging Payload config into a build script, the ./meta payload-free subpath (BlockMetaEntry[]) is extended to the remaining offered blocks packs — agency-essentials, catalog-pack, lms-pack, org-pack, and signal-theme — mirroring the existing editorial-pack / marketing-starter / content-writer / extras barrels. Each block's BlockMeta was relocated verbatim into a payload-free sibling meta module and re-imported by its block config; no meta values changed.

    Every supported-core package additionally adds CHANGELOG.md to its published files array so the next publish cascade ships changelogs the emitter can read from installed tarballs at prebuild.

  • 0.1.1
    Add tsup build pipeline: emit ESM+CJS+DTS to dist/, rewrite package.json exports/main/module/types to point at dist, copy CSS assets (report.css + fonts/) via standalone post-build script, expose CLI bin entry.

    Fixes bundler-incompatible raw .ts source distribution.

Full changelog across all packages