pyreon

@pyreon/connector-document is the bridge between Pyreon's component layer and the @pyreon/document rendering pipeline. It walks a Pyreon VNode tree, finds components carrying a _documentType marker, resolves their CSS-in-JS styles into a flat style object, and produces a format-agnostic DocNode tree that @pyreon/document's render() can turn into PDF, DOCX, email, Slack, and 16 other formats.

@pyreon/connector-documentstable

Installation

npm install @pyreon/connector-document
bun add @pyreon/connector-document
pnpm add @pyreon/connector-document
yarn add @pyreon/connector-document

The Bridge: One Tree, Two Destinations

Pyreon's headline document feature is that the same JSX tree renders in the browser AND exports to 20 output formats. That works because of a three-package split:

PackageRole
@pyreon/document-primitivesThe 18 rocketstyle components you author (DocDocument, DocHeading, DocText, …). Each carries a _documentType static marker.
@pyreon/connector-documentThis package. Walks the primitive tree and extracts a DocNode.
@pyreon/documentThe renderer. Takes a DocNode and produces PDF / DOCX / email / Markdown / etc.

The connector is the seam between the browser-rendered view and the exported view. Document primitives are real components — they mount and render styled DOM in the browser like any other Pyreon component. When you want to export the same tree to a non-browser format, the connector traverses that exact VNode tree and lowers it to a DocNode the renderers understand.

┌──────────────────────────────┐
│  DocDocument / DocHeading /…  │   @pyreon/document-primitives
│  (rocketstyle components,     │   — carry _documentType markers
│   _documentType markers)      │
└───────────────┬──────────────┘
                │ extractDocumentTree(vnode)

┌──────────────────────────────┐
│        DocNode tree           │   @pyreon/connector-document
│  { type, props, children,     │   — format-agnostic
│    styles }                   │
└───────────────┬──────────────┘
                │ render(node, format)

   PDF · DOCX · XLSX · email · Markdown · Slack · …   @pyreon/document

How Extraction Works

extractDocumentTree(vnode) walks the tree node by node:

  1. Component with a _documentType marker → emit a DocNode:

    • _documentTypeDocNode.type (e.g. 'document', 'heading', 'text')

    • _documentPropsDocNode.props (function values are resolved to their live value at this point)

    • $rocketstyleresolveStyles()DocNode.styles

    • Recurse into children.

  2. Component without a marker → call it to get its VNode output, then recurse (transparent).

  3. DOM element ('div', 'span', …) → transparent: its children are flattened into the parent's children. Text content is collected.

  4. Strings / numbers → collected as text children. null / false / true are skipped.

Reactive children (function getters) and nested arrays are flattened and resolved during the walk, so a tree built with signals exports its live state.

import { extractDocumentTree } from '@pyreon/connector-document'
import { render } from '@pyreon/document'
import { DocDocument, DocPage, DocHeading, DocText } from '@pyreon/document-primitives'

// 1. Build a tree with document primitives (this same tree can render in the browser)
const tree = (
  <DocDocument title="Q4 Report" author="Alice">
    <DocPage>
      <DocHeading h1>Sales Report</DocHeading>
      <DocText>Revenue grew 24% quarter over quarter.</DocText>
    </DocPage>
  </DocDocument>
)

// 2. Extract a format-agnostic DocNode
const node = extractDocumentTree(tree)

// 3. Render to any format
const pdf = await render(node, 'pdf') // Uint8Array
const email = await render(node, 'email') // email-safe HTML string
const md = await render(node, 'md') // Markdown string

extractDocumentTree also accepts a component function directly (it will call it to obtain the tree):

const node = extractDocumentTree(() => <ResumeTemplate resume={store.resume} />)

extractDocumentTree(vnode, options?)

Walks a Pyreon VNode tree (or a component function) and returns a single DocNode. If the root produces loose children rather than a single document node, they are wrapped in a synthetic { type: 'document', props: {}, children } node, so the return type is always a DocNode.

function extractDocumentTree(vnode: unknown, options?: ExtractOptions): DocNode

ExtractOptions

OptionTypeDefaultDescription
rootSizenumber16Root font size in px used for rem / em → px conversion when resolving styles.
includeStylesbooleantrueWhether to resolve $rocketstyle into DocNode.styles. Set false to emit a structure-only tree.
resolveVarVarResolverInline var(--…) style values to raw values during extraction. Needed under init({ cssVariables: true }) because PDF/DOCX/email targets can't evaluate CSS custom properties.

Resolving CSS variables (cssVariables mode)

When your app runs under init({ cssVariables: true }), $rocketstyle style values can be var(--px-…) reference strings. PDF/DOCX/email render targets can't evaluate CSS custom properties, so they have to be inlined to raw values at extraction time. Supply a resolveVar that composes resolveModeVar (from @pyreon/rocketstyle, resolves mode(a, b) pairs) with resolveCssVarReferences (from @pyreon/unistyle, resolves theme-leaf vars via a themeToCssVars(theme).registry):

import { extractDocumentTree } from '@pyreon/connector-document'
import { resolveModeVar } from '@pyreon/rocketstyle'
import { resolveCssVarReferences, themeToCssVars } from '@pyreon/unistyle'

const { registry } = themeToCssVars(theme)

const node = extractDocumentTree(tree, {
  resolveVar: (v) => resolveCssVarReferences(resolveModeVar(v, mode), registry),
})

resolveStyles(source, rootSize?, resolveVar?)

Converts a rocketstyle $rocketstyle theme object into a ResolvedStyles object compatible with @pyreon/document. extractDocumentTree calls this internally for every marked node, but it is exported so custom primitives and tooling can resolve styles directly.

function resolveStyles(
  source: Record<string, unknown>,
  rootSize?: number,
  resolveVar?: VarResolver,
): ResolvedStyles
ParameterTypeDefaultDescription
sourceRecord<string, unknown>The $rocketstyle theme object to convert.
rootSizenumber16Root font size for rem / em → px conversion.
resolveVarVarResolverOptional var(--…) inliner (see cssVariables mode above).

Only properties that ResolvedStyles supports are extracted — everything else (transitions, cursor, display, etc.) is silently ignored. The recognized properties are typography (fontSize, fontFamily, fontWeight, fontStyle, textDecoration, color, backgroundColor, textAlign, lineHeight, letterSpacing), box model (padding, margin), border (borderRadius, borderWidth, borderColor, borderStyle), sizing (width, height, maxWidth), and opacity. Enum-typed properties are validated against an allowlist — an unrecognized value (e.g. textAlign: 'start') is dropped rather than passed through.

import { resolveStyles } from '@pyreon/connector-document'

resolveStyles({
  fontSize: '1.5rem',
  fontWeight: 'bold',
  color: '#222',
  padding: '8px 16px',
  textAlign: 'center',
})
// → {
//     fontSize: 24,        // 1.5rem × rootSize(16)
//     fontWeight: 'bold',
//     color: '#222',
//     padding: [8, 16],
//     textAlign: 'center',
//   }

CSS Value Parsers

The package ships four pure parsers that normalize CSS values into the numeric units document renderers expect. They underpin resolveStyles but are exported for direct use in custom primitives or renderers.

parseCssDimension(value, rootSize?)

Parses a CSS dimension into a px-equivalent number. Returns undefined for values it can't normalize (e.g. 'auto', '100%').

function parseCssDimension(
  value: string | number | null | undefined,
  rootSize?: number,
): number | undefined
InputOutput (rootSize = 16)Notes
1414Numbers pass through unchanged.
'14px'14
'1.5rem'24value × rootSize.
'2em'32em is treated like remrootSize).
'12pt'16pt × 4/3.
'1.5'1.5Unitless numeric strings parse.
'auto'undefinedUnrecognized → undefined.

parseBoxModel(value, rootSize?)

Parses a CSS padding / margin shorthand into the document tuple format. Returns undefined if any segment fails to parse.

function parseBoxModel(
  value: string | number | undefined,
  rootSize?: number,
): number | [number, number] | [number, number, number, number] | undefined
InputOutput
88
'8px'8
'8px 16px'[8, 16]
'8px 16px 12px'[8, 16, 12, 16] (CSS 3-value shorthand → top/right/bottom/left)
'8px 16px 8px 16px'[8, 16, 8, 16]

parseFontWeight(value)

Normalizes a CSS font-weight keyword or number.

function parseFontWeight(
  value: string | number | undefined,
): 'normal' | 'bold' | number | undefined
InputOutput
700700
'700'700
'bold''bold'
'normal''normal'
'oblique'undefined

parseLineHeight(value, rootSize?)

Parses a CSS line-height. Unitless numbers pass through; dimensions are normalized via parseCssDimension; 'normal' returns undefined (let the renderer apply its default).

function parseLineHeight(
  value: string | number | undefined,
  rootSize?: number,
): number | undefined
InputOutput
1.51.5
'1.5'1.5
'24px'24
'normal'undefined

Authoring a Custom Document Primitive

A document primitive is just a component that carries a _documentType marker the connector recognizes. The two requirements:

  • A _documentType static set to one of the recognized NodeType values.

  • A _documentProps object the connector reads as DocNode.props.

The connector finds these through three resolution paths, in order:

  1. _documentProps already on the JSX vnode props — used by hand-constructed test fixtures.

  2. The __rs_attrs hoisted-attrs fast path — when the component is a real rocketstyle primitive, it exposes its accumulated .attrs() callback chain as __rs_attrs. The connector runs that chain directly against the JSX props (chain.reduce(Object.assign, {})) to read _documentProps — no styled-wrapper invocation, no styling work, no dimension resolution. This is the production path for every shipped primitive.

  3. Full component invocation (legacy fallback) — only when neither of the above applies (a non-rocketstyle component marked with _documentType directly).

The idiomatic rocketstyle authoring shape — taken from the real DocDocument source:

import { Element } from '@pyreon/elements'
import rocketstyle from '@pyreon/rocketstyle'

const DocDocument = rocketstyle()({ name: 'DocDocument', component: Element })
  .statics({ _documentType: 'document' as const })
  .attrs<{
    title?: string | (() => string)
    author?: string | (() => string)
    subject?: string | (() => string)
  }>((props) => ({
    tag: 'div',
    _documentProps: {
      // Pass values through unmodified — extractDocumentTree resolves
      // any function (accessor) values at export time. Nullish values
      // are omitted so they don't appear as `title: undefined`.
      ...(props.title != null ? { title: props.title } : {}),
      ...(props.author != null ? { author: props.author } : {}),
      ...(props.subject != null ? { subject: props.subject } : {}),
    },
  }))

Reactive metadata via accessor props

A _documentProps value may be a function (() => T). The connector resolves it at extraction time, so each export reads the live signal value rather than a value captured at component mount:

function ResumeTemplate({ resume }: { resume: () => Resume }) {
  return (
    <DocDocument
      title={() => `${resume().name} — Resume`}
      author={() => resume().name}
    >
      {/* … */}
    </DocDocument>
  )
}

Because rocketstyle's .attrs() callback runs once at mount, a string-only prop bound to a live signal would freeze at its initial value. Storing the accessor in _documentProps and resolving it at extraction time keeps export metadata in sync with the live UI. See @pyreon/document-primitives for the full set of 18 primitives.

The DocNode Tree

extractDocumentTree produces a DocNode — the format-agnostic intermediate representation @pyreon/document renders. DocNode, DocChild, NodeType, and ResolvedStyles are re-exported from @pyreon/document so consumers can type the bridge without importing the renderer.

interface DocNode {
  type: NodeType
  props: Record<string, unknown>
  children: DocChild[]
  styles?: ResolvedStyles // resolved CSS from the ui-system connector
}

type DocChild = DocNode | string

NodeType

The recognized document node types — set as the _documentType marker on a primitive:

type NodeType =
  | 'document'
  | 'page'
  | 'section'
  | 'row'
  | 'column'
  | 'heading'
  | 'text'
  | 'link'
  | 'image'
  | 'table'
  | 'list'
  | 'list-item'
  | 'page-break'
  | 'code'
  | 'divider'
  | 'spacer'
  | 'button'
  | 'quote'

ResolvedStyles

The flat, render-target-friendly style shape resolveStyles produces. Every field is optional; only resolvable properties are emitted.

interface ResolvedStyles {
  fontSize?: number
  fontFamily?: string
  fontWeight?: 'normal' | 'bold' | number
  fontStyle?: 'normal' | 'italic'
  textDecoration?: 'none' | 'underline' | 'line-through'
  color?: string
  backgroundColor?: string
  textAlign?: 'left' | 'center' | 'right' | 'justify'
  lineHeight?: number
  letterSpacing?: number
  padding?: number | [number, number] | [number, number, number, number]
  margin?: number | [number, number] | [number, number, number, number]
  borderRadius?: number
  borderWidth?: number
  borderColor?: string
  borderStyle?: 'solid' | 'dashed' | 'dotted'
  width?: number | string
  height?: number | string
  maxWidth?: number | string
  opacity?: number
}

API Reference

Functions

ExportSignatureDescription
extractDocumentTree(vnode: unknown, options?: ExtractOptions) => DocNodeWalks a Pyreon VNode tree (or component function) and produces a DocNode for @pyreon/document.
resolveStyles(source, rootSize?, resolveVar?) => ResolvedStylesConverts a $rocketstyle theme object into a flat ResolvedStyles.
parseCssDimension(value, rootSize?) => number | undefinedParses a CSS dimension to a px-equivalent number.
parseBoxModel(value, rootSize?) => number | [number, number] | [number, number, number, number] | undefinedParses a padding / margin shorthand to a tuple.
parseFontWeight(value) => 'normal' | 'bold' | number | undefinedNormalizes a font-weight keyword or number.
parseLineHeight(value, rootSize?) => number | undefinedParses a line-height to a number.

Types

TypeShapeDescription
ExtractOptions{ rootSize?, includeStyles?, resolveVar? }Options for extractDocumentTree.
VarResolver(value: unknown) => unknownMaps a style value to a render-target-evaluable one; inlines var(--…) references. Non-string / non-var values pass through unchanged.
DocumentMarker{ _documentType: NodeType }Marker interface — components carrying it are extractable.
DocNode{ type, props, children, styles? }A format-agnostic document node (re-exported from @pyreon/document).
DocChildDocNode | stringA child node — a nested DocNode or text (re-exported from @pyreon/document).
NodeTypeunion of 18 type stringsDocument node type identifiers (re-exported from @pyreon/document).
ResolvedStylesflat style objectThe resolved style shape (re-exported from @pyreon/document).
  • @pyreon/document-primitives — the 18 rocketstyle components you author; re-exports extractDocumentTree / resolveStyles and adds the higher-level extractDocNode(templateFn) / createDocumentExport(templateFn) helpers most apps use.

  • @pyreon/document — the renderer. Takes a DocNode and produces 20 output formats (PDF, DOCX, XLSX, PPTX, HTML, email, Markdown, text, CSV, SVG, Slack, Teams, Discord, Telegram, Notion, Confluence, WhatsApp, Google Chat, JSON, JSONL).

  • @pyreon/rocketstyle / @pyreon/styler / @pyreon/unistyle — the CSS-in-JS layer the connector reads $rocketstyle and var(--…) values from.

Connector Document