Documentation menu
Documentation / Platform services / print
npm install @wabbit/tome-printFirst install from our registry?
Get a free registry token from your credentials page. Every install from our registry needs one, free packages included.
Add the registry and your token to the
.npmrcat the root of your project, with your token in place ofYOUR_TOKEN:@wabbit:registry=https://npm.wabbit.com/ //npm.wabbit.com/:_authToken=YOUR_TOKEN
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:
- Pandoc — Markdown → JSON AST (
pandoc -t json).findPandoc()triespandoconPATHfirst, 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. - WeasyPrint (Python) — HTML → PDF, invoked as a bare
weasyprintshell command with no path-search fallback. If it's missing,build.tscatches theexecSyncfailure and printspip install weasyprint, then preserves the intermediate HTML at.tome-print-tmp/report.htmlnext 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.htmlcopy 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 ASTassembleDocument 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 inputThe 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
| Export | Subpath | Description |
|---|---|---|
DocumentMeta, PrintBlock, PrintTemplate | @wabbit/tome-print/types | Core shapes — PrintTemplate = (block, meta) => string |
registerPrintTemplate(slug, template) | @wabbit/tome-print/registry | Register a template function for a block slug |
getPrintTemplate(slug), hasPrintTemplate(slug), getAllPrintTemplates() | @wabbit/tome-print/registry | Registry lookups |
renderBlock(block, meta) | @wabbit/tome-print/registry | Render 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/richtext | Inline-node renderers |
renderBlockNode, renderBlocks | @wabbit/tome-print/richtext | Block-node renderers |
tome-print (bin) | bin only — @wabbit/tome-print/cli is not an importable subpath | Pandoc-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/templates | import '@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/assembleDocumentseam, 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.ctsdeclarations, 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
.mapfiles and thesourceMappingURLcomments 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 snapshotsextractGallerySourcewrites 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-printCLI now opens with a#!/usr/bin/env nodeshebang, 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.
injectChartsnow 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.
buildgains&& node ../../scripts/fix-dist-extensions.mjs --strictas its last step, joining the 13 packages that already ran it. tsup buildsbundle: falseand emits relative specifiers exactly as the TypeScript source wrote them — extensionless — which bundlers resolve and raw Node does not (ESMERR_MODULE_NOT_FOUND; CJS worse,require('./x')finds the ESM.jstwin 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.mjsnow 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--strictexited 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/.cjsappended.assert-node-loadable.mjsgained the matching carve-outs so the new repo-wide CI gate reports real defects only: a resolution failure whose path lands undernode_modulesis a peer SKIP (next@15 has no exports map, sonext/imagefails as an absolute path), and a bundler-asset load failure is an environmental SKIP (CJS surfaces it asSyntaxError: 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 importmissing/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.
homepagedeep-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.urlpoints at the repo issue tracker, so a paying customer has a place to report a defect that is not email.engines.nodeis>=22, matching the rootenginesand.nvmrcset the same day. This is a real floor, not decoration: CI on Node 20 could not expand the glob the block packs use fornode --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(rootpnpm assert:manifest-metadata, wired intoplatform-discipline.ymlbesideassert:license-metadata) fails when any publishable manifest lacksdescription,repository.directorymatching its own folder,homepage,bugs,engines.nodeequal to the repo floor,license,filesorsideEffects. It reported 138 violations before this change and 0 after. - 0.1.3
Every package now declares an explicit
sideEffectsfield (38 added; motion/engine/forms already correct).Registration-bearing modules (render files'
registerRenderer,blocks/*/index.tsdefineBlockself-registration, widgetregister.tsfiles, 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 blanketfalseon a package with registration or CSS. - 0.1.2
Docs-manifest emitter pipeline (W3 ship-readiness).
@wabbit/tome-blocks-corenow ships a standalone Node ESM CLI atscripts/emit-docs-manifests.mjsthat emits per-package documentation manifests (index.json, packages/<slug>.json, changelog.json) by reading what packages already carry — READMEs, the payload-free<pkg>/metablock-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
./metapayload-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'sBlockMetawas 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.mdto its publishedfilesarray 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/, rewritepackage.jsonexports/main/module/types to point atdist, copy CSS assets (report.css + fonts/) via standalone post-build script, expose CLI bin entry.Fixes bundler-incompatible raw
.tssource distribution.