@pyreon/connector-document 0.12.10 → 0.12.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index2.d.ts","names":[],"sources":["../../src/cssValueParser.ts","../../src/extractDocumentTree.ts","../../src/resolveStyles.ts"],"mappings":";;;;;;AAiBA;;;;;AAyBC;iBAzBe,iBAAA,CACd,KAAA,sCACA,QAAA;AAAA,KAyBG,cAAA;;;AAWL;;;;;;;iBAAgB,aAAA,CACd,KAAA,+BACA,QAAA,YACC,cAAA;;AAyBH;;iBAAgB,eAAA,CACd,KAAA;;;AAaF;iBAAgB,eAAA,CACd,KAAA,+BACA,QAAA;;;;UC/Fe,cAAA;EACf,aAAA,EAAe,QAAA;AAAA;AAAA,UAGA,cAAA;EDUf;ECRA,QAAA;EDkCG;EChCH,aAAA;AAAA;;;AD2CF;;;;;;;;;AA4BA;iBCgFgB,mBAAA,CAAoB,KAAA,WAAgB,OAAA,GAAS,cAAA,GAAsB,OAAA;;;;;ADlJnF;;;;;iBEGgB,aAAA,CAAc,WAAA,EAAa,MAAA,mBAAyB,QAAA,YAAgB,cAAA"}
1
+ {"version":3,"file":"index2.d.ts","names":[],"sources":["../../src/cssValueParser.ts","../../src/extractDocumentTree.ts","../../src/resolveStyles.ts"],"mappings":";;;;;;AAiBA;;;;;AAyBC;iBAzBe,iBAAA,CACd,KAAA,sCACA,QAAA;AAAA,KAyBG,cAAA;;;AAWL;;;;;;;iBAAgB,aAAA,CACd,KAAA,+BACA,QAAA,YACC,cAAA;;AAyBH;;iBAAgB,eAAA,CACd,KAAA;;;AAaF;iBAAgB,eAAA,CACd,KAAA,+BACA,QAAA;;;;UC/Fe,cAAA;EACf,aAAA,EAAe,QAAA;AAAA;AAAA,UAGA,cAAA;EDUf;ECRA,QAAA;EDkCG;EChCH,aAAA;AAAA;;;AD2CF;;;;;;;;;AA4BA;iBC0KgB,mBAAA,CAAoB,KAAA,WAAgB,OAAA,GAAS,cAAA,GAAsB,OAAA;;;;;AD5OnF;;;;;iBEGgB,aAAA,CAAc,WAAA,EAAa,MAAA,mBAAyB,QAAA,YAAgB,cAAA"}
package/lib/index.js CHANGED
@@ -187,9 +187,23 @@ function extractNode(vnode, options) {
187
187
  const rootSize = options.rootSize ?? 16;
188
188
  const docType = getDocumentType(type);
189
189
  if (docType) {
190
+ let rawDocProps;
191
+ let extractedFromCall = null;
192
+ if (props._documentProps && typeof props._documentProps === "object") rawDocProps = props._documentProps;
193
+ else if (typeof type === "function") {
194
+ const mergedProps = { ...props };
195
+ if (children && children.length > 0) mergedProps.children = children.length === 1 ? children[0] : children;
196
+ const result = type(mergedProps);
197
+ if (isVNode(result)) {
198
+ extractedFromCall = result;
199
+ const innerProps = result.props;
200
+ if (innerProps?._documentProps && typeof innerProps._documentProps === "object") rawDocProps = innerProps._documentProps;
201
+ }
202
+ }
190
203
  const docProps = {};
191
- if (props._documentProps && typeof props._documentProps === "object") Object.assign(docProps, props._documentProps);
192
- const styles = includeStyles && props.$rocketstyle ? resolveStyles(props.$rocketstyle, rootSize) : void 0;
204
+ if (rawDocProps) for (const [key, value] of Object.entries(rawDocProps)) docProps[key] = typeof value === "function" ? value() : value;
205
+ const stylesSource = props.$rocketstyle ?? extractedFromCall?.props?.$rocketstyle;
206
+ const styles = includeStyles && stylesSource ? resolveStyles(stylesSource, rootSize) : void 0;
193
207
  const node = {
194
208
  type: docType,
195
209
  props: docProps,
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/cssValueParser.ts","../src/resolveStyles.ts","../src/extractDocumentTree.ts"],"sourcesContent":["const PX_RE = /^(-?\\d+(?:\\.\\d+)?)px$/\nconst REM_RE = /^(-?\\d+(?:\\.\\d+)?)rem$/\nconst EM_RE = /^(-?\\d+(?:\\.\\d+)?)em$/\nconst PT_RE = /^(-?\\d+(?:\\.\\d+)?)pt$/\nconst NUMBER_RE = /^-?\\d+(?:\\.\\d+)?$/\n\nconst DEFAULT_ROOT_SIZE = 16\n\n/**\n * Parse a CSS dimension value to a number.\n *\n * - `14` → `14`\n * - `'14px'` → `14`\n * - `'1.5rem'` → `24` (with rootSize=16)\n * - `'12pt'` → `16` (pt × 1.333)\n * - `'auto'` → `undefined`\n */\nexport function parseCssDimension(\n value: string | number | null | undefined,\n rootSize = DEFAULT_ROOT_SIZE,\n): number | undefined {\n if (value == null) return undefined\n if (typeof value === 'number') return value\n if (typeof value !== 'string') return undefined\n\n const trimmed = value.trim()\n\n const pxMatch = PX_RE.exec(trimmed)\n if (pxMatch?.[1]) return Number.parseFloat(pxMatch[1])\n\n const remMatch = REM_RE.exec(trimmed)\n if (remMatch?.[1]) return Number.parseFloat(remMatch[1]) * rootSize\n\n const emMatch = EM_RE.exec(trimmed)\n if (emMatch?.[1]) return Number.parseFloat(emMatch[1]) * rootSize\n\n const ptMatch = PT_RE.exec(trimmed)\n if (ptMatch?.[1]) return Number.parseFloat(ptMatch[1]) * (4 / 3)\n\n if (NUMBER_RE.test(trimmed)) return Number.parseFloat(trimmed)\n\n return undefined\n}\n\ntype BoxModelResult = number | [number, number] | [number, number, number, number] | undefined\n\n/**\n * Parse a CSS padding/margin shorthand to document tuple format.\n *\n * - `8` → `8`\n * - `'8px'` → `8`\n * - `'8px 16px'` → `[8, 16]`\n * - `'8px 16px 8px 16px'` → `[8, 16, 8, 16]`\n * - `'8px 16px 12px'` → `[8, 16, 12, 16]` (CSS 3-value shorthand)\n */\nexport function parseBoxModel(\n value: string | number | undefined,\n rootSize = DEFAULT_ROOT_SIZE,\n): BoxModelResult {\n if (value == null) return undefined\n if (typeof value === 'number') return value\n\n const parts = value\n .trim()\n .split(/\\s+/)\n .map((p) => parseCssDimension(p, rootSize))\n\n const nums = parts.filter((p): p is number => p != null)\n if (nums.length !== parts.length) return undefined\n\n if (nums.length === 1) return nums[0]\n if (nums.length === 2) return [nums[0], nums[1]] as [number, number]\n if (nums.length === 3)\n return [nums[0], nums[1], nums[2], nums[1]] as [number, number, number, number]\n if (nums.length === 4)\n return [nums[0], nums[1], nums[2], nums[3]] as [number, number, number, number]\n\n return undefined\n}\n\n/**\n * Parse a CSS font-weight value.\n */\nexport function parseFontWeight(\n value: string | number | undefined,\n): 'normal' | 'bold' | number | undefined {\n if (value == null) return undefined\n if (typeof value === 'number') return value\n if (value === 'normal' || value === 'bold') return value\n const num = Number.parseInt(value, 10)\n if (!Number.isNaN(num)) return num\n return undefined\n}\n\n/**\n * Parse a CSS line-height value to a unitless number.\n */\nexport function parseLineHeight(\n value: string | number | undefined,\n rootSize = DEFAULT_ROOT_SIZE,\n): number | undefined {\n if (value == null) return undefined\n if (typeof value === 'number') return value\n if (value === 'normal') return undefined\n\n const dim = parseCssDimension(value, rootSize)\n if (dim != null) return dim\n\n return undefined\n}\n","import {\n parseBoxModel,\n parseCssDimension,\n parseFontWeight,\n parseLineHeight,\n} from './cssValueParser'\nimport type { ResolvedStyles } from './types'\n\nconst TEXT_ALIGN_VALUES = new Set(['left', 'center', 'right', 'justify'])\nconst FONT_STYLE_VALUES = new Set(['normal', 'italic'])\nconst TEXT_DECORATION_VALUES = new Set(['none', 'underline', 'line-through'])\nconst BORDER_STYLE_VALUES = new Set(['solid', 'dashed', 'dotted'])\n\n/**\n * Convert a rocketstyle `$rocketstyle` theme object into a `ResolvedStyles`\n * object compatible with `@pyreon/document`.\n *\n * Only extracts properties that `ResolvedStyles` supports — everything else\n * (transitions, cursor, display, etc.) is silently ignored.\n */\nexport function resolveStyles(rocketstyle: Record<string, unknown>, rootSize = 16): ResolvedStyles {\n const styles: ResolvedStyles = {}\n\n // Typography\n const fontSize = parseCssDimension(rocketstyle.fontSize as string | number, rootSize)\n if (fontSize != null) styles.fontSize = fontSize\n\n if (typeof rocketstyle.fontFamily === 'string') styles.fontFamily = rocketstyle.fontFamily\n\n const fontWeight = parseFontWeight(rocketstyle.fontWeight as string | number | undefined)\n if (fontWeight != null) styles.fontWeight = fontWeight\n\n if (typeof rocketstyle.fontStyle === 'string' && FONT_STYLE_VALUES.has(rocketstyle.fontStyle))\n styles.fontStyle = rocketstyle.fontStyle as 'normal' | 'italic'\n\n if (\n typeof rocketstyle.textDecoration === 'string' &&\n TEXT_DECORATION_VALUES.has(rocketstyle.textDecoration)\n )\n styles.textDecoration = rocketstyle.textDecoration as 'none' | 'underline' | 'line-through'\n\n if (typeof rocketstyle.color === 'string') styles.color = rocketstyle.color\n\n if (typeof rocketstyle.backgroundColor === 'string')\n styles.backgroundColor = rocketstyle.backgroundColor\n\n if (typeof rocketstyle.textAlign === 'string' && TEXT_ALIGN_VALUES.has(rocketstyle.textAlign))\n styles.textAlign = rocketstyle.textAlign as 'left' | 'center' | 'right' | 'justify'\n\n const lineHeight = parseLineHeight(\n rocketstyle.lineHeight as string | number | undefined,\n rootSize,\n )\n if (lineHeight != null) styles.lineHeight = lineHeight\n\n const letterSpacing = parseCssDimension(rocketstyle.letterSpacing as string | number, rootSize)\n if (letterSpacing != null) styles.letterSpacing = letterSpacing\n\n // Box model\n const padding = parseBoxModel(rocketstyle.padding as string | number | undefined, rootSize)\n if (padding != null) styles.padding = padding\n\n const margin = parseBoxModel(rocketstyle.margin as string | number | undefined, rootSize)\n if (margin != null) styles.margin = margin\n\n // Border\n const borderRadius = parseCssDimension(rocketstyle.borderRadius as string | number, rootSize)\n if (borderRadius != null) styles.borderRadius = borderRadius\n\n const borderWidth = parseCssDimension(rocketstyle.borderWidth as string | number, rootSize)\n if (borderWidth != null) styles.borderWidth = borderWidth\n\n if (typeof rocketstyle.borderColor === 'string') styles.borderColor = rocketstyle.borderColor\n\n if (\n typeof rocketstyle.borderStyle === 'string' &&\n BORDER_STYLE_VALUES.has(rocketstyle.borderStyle)\n )\n styles.borderStyle = rocketstyle.borderStyle as 'solid' | 'dashed' | 'dotted'\n\n // Sizing\n if (rocketstyle.width != null) {\n const w = parseCssDimension(rocketstyle.width as string | number, rootSize)\n styles.width = w ?? (rocketstyle.width as string)\n }\n\n if (rocketstyle.height != null) {\n const h = parseCssDimension(rocketstyle.height as string | number, rootSize)\n styles.height = h ?? (rocketstyle.height as string)\n }\n\n if (rocketstyle.maxWidth != null) {\n const mw = parseCssDimension(rocketstyle.maxWidth as string | number, rootSize)\n styles.maxWidth = mw ?? (rocketstyle.maxWidth as string)\n }\n\n // Opacity\n if (typeof rocketstyle.opacity === 'number') styles.opacity = rocketstyle.opacity\n\n return styles\n}\n","import { resolveStyles } from './resolveStyles'\nimport type { DocChild, DocNode, NodeType } from './types'\n\n/** Marker interface: components with _documentType are extractable. */\nexport interface DocumentMarker {\n _documentType: NodeType\n}\n\nexport interface ExtractOptions {\n /** Root font size for rem→px conversion. Default: 16. */\n rootSize?: number\n /** Include resolved styles from $rocketstyle. Default: true. */\n includeStyles?: boolean\n}\n\ntype VNodeLike = {\n type: string | ((...args: any[]) => any)\n props: Record<string, any>\n children: unknown[]\n}\n\nfunction isVNode(value: unknown): value is VNodeLike {\n return value != null && typeof value === 'object' && 'type' in value && 'props' in value\n}\n\nfunction getDocumentType(fn: unknown): NodeType | undefined {\n if (typeof fn !== 'function') return undefined\n const meta = (fn as any).meta\n if (meta?._documentType) return meta._documentType as NodeType\n // Fallback: check directly on function (non-rocketstyle components)\n if ('_documentType' in fn) return (fn as any)._documentType as NodeType\n return undefined\n}\n\nfunction flattenChildren(children: unknown[]): unknown[] {\n const result: unknown[] = []\n for (const child of children) {\n if (Array.isArray(child)) {\n result.push(...flattenChildren(child))\n } else if (typeof child === 'function') {\n // Reactive getter — call to resolve\n const resolved = child()\n if (Array.isArray(resolved)) {\n result.push(...flattenChildren(resolved))\n } else {\n result.push(resolved)\n }\n } else {\n result.push(child)\n }\n }\n return result\n}\n\nfunction extractChildren(children: unknown[], options: ExtractOptions): DocChild[] {\n const flat = flattenChildren(children)\n const result: DocChild[] = []\n\n for (const child of flat) {\n if (child == null || child === false || child === true) continue\n\n if (typeof child === 'string') {\n result.push(child)\n continue\n }\n\n if (typeof child === 'number') {\n result.push(String(child))\n continue\n }\n\n if (isVNode(child)) {\n const extracted = extractNode(child, options)\n if (Array.isArray(extracted)) {\n result.push(...extracted)\n } else if (extracted != null) {\n result.push(extracted)\n }\n }\n }\n\n return result\n}\n\nfunction extractNode(vnode: VNodeLike, options: ExtractOptions): DocNode | DocChild[] | null {\n const { type, props, children } = vnode\n const includeStyles = options.includeStyles !== false\n const rootSize = options.rootSize ?? 16\n\n // Component function with _documentType marker (via .statics() or direct)\n const docType = getDocumentType(type)\n if (docType) {\n const docProps: Record<string, unknown> = {}\n\n // Extract document-specific props from _documentProps\n if (props._documentProps && typeof props._documentProps === 'object') {\n Object.assign(docProps, props._documentProps)\n }\n\n // Resolve styles from $rocketstyle\n const styles =\n includeStyles && props.$rocketstyle\n ? resolveStyles(props.$rocketstyle as Record<string, unknown>, rootSize)\n : undefined\n\n // Recurse into children\n const docChildren = extractChildren(children ?? [], options)\n\n const node: DocNode = {\n type: docType,\n props: docProps,\n children: docChildren,\n }\n\n if (styles && Object.keys(styles).length > 0) {\n node.styles = styles\n }\n\n return node\n }\n\n // Component function WITHOUT _documentType — call it to get its VNode output\n if (typeof type === 'function') {\n const mergedProps = { ...props }\n if (children && children.length > 0) {\n mergedProps.children = children.length === 1 ? children[0] : children\n }\n\n const result = type(mergedProps)\n\n if (isVNode(result)) {\n return extractNode(result, options)\n }\n\n // The component returned a primitive or null\n if (typeof result === 'string') return [result]\n if (typeof result === 'number') return [String(result)]\n return null\n }\n\n // DOM element (string type like 'div', 'span') — transparent, extract children\n if (typeof type === 'string') {\n const docChildren = extractChildren(children ?? [], options)\n // If there's text content in the DOM element, collect it\n if (docChildren.length > 0) return docChildren\n return null\n }\n\n return null\n}\n\n/**\n * Walk a Pyreon VNode tree and extract a `DocNode` tree for `@pyreon/document`.\n *\n * For each VNode whose component has a `_documentType` marker:\n * 1. Read `_documentType` → `DocNode.type`\n * 2. Read `_documentProps` → `DocNode.props`\n * 3. Read `$rocketstyle` → `resolveStyles()` → `DocNode.styles`\n * 4. Recurse into children\n *\n * VNodes without `_documentType` are transparent — their children\n * are flattened into the parent's children list.\n */\nexport function extractDocumentTree(vnode: unknown, options: ExtractOptions = {}): DocNode {\n if (isVNode(vnode)) {\n const result = extractNode(vnode, options)\n if (result && !Array.isArray(result)) return result\n\n // Wrap loose children in a document node\n const children = Array.isArray(result) ? result : []\n return { type: 'document', props: {}, children }\n }\n\n // If passed a component function directly, call it\n if (typeof vnode === 'function') {\n const result = (vnode as () => unknown)()\n return extractDocumentTree(result, options)\n }\n\n return { type: 'document', props: {}, children: [] }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/cssValueParser.ts","../src/resolveStyles.ts","../src/extractDocumentTree.ts"],"sourcesContent":["const PX_RE = /^(-?\\d+(?:\\.\\d+)?)px$/\nconst REM_RE = /^(-?\\d+(?:\\.\\d+)?)rem$/\nconst EM_RE = /^(-?\\d+(?:\\.\\d+)?)em$/\nconst PT_RE = /^(-?\\d+(?:\\.\\d+)?)pt$/\nconst NUMBER_RE = /^-?\\d+(?:\\.\\d+)?$/\n\nconst DEFAULT_ROOT_SIZE = 16\n\n/**\n * Parse a CSS dimension value to a number.\n *\n * - `14` → `14`\n * - `'14px'` → `14`\n * - `'1.5rem'` → `24` (with rootSize=16)\n * - `'12pt'` → `16` (pt × 1.333)\n * - `'auto'` → `undefined`\n */\nexport function parseCssDimension(\n value: string | number | null | undefined,\n rootSize = DEFAULT_ROOT_SIZE,\n): number | undefined {\n if (value == null) return undefined\n if (typeof value === 'number') return value\n if (typeof value !== 'string') return undefined\n\n const trimmed = value.trim()\n\n const pxMatch = PX_RE.exec(trimmed)\n if (pxMatch?.[1]) return Number.parseFloat(pxMatch[1])\n\n const remMatch = REM_RE.exec(trimmed)\n if (remMatch?.[1]) return Number.parseFloat(remMatch[1]) * rootSize\n\n const emMatch = EM_RE.exec(trimmed)\n if (emMatch?.[1]) return Number.parseFloat(emMatch[1]) * rootSize\n\n const ptMatch = PT_RE.exec(trimmed)\n if (ptMatch?.[1]) return Number.parseFloat(ptMatch[1]) * (4 / 3)\n\n if (NUMBER_RE.test(trimmed)) return Number.parseFloat(trimmed)\n\n return undefined\n}\n\ntype BoxModelResult = number | [number, number] | [number, number, number, number] | undefined\n\n/**\n * Parse a CSS padding/margin shorthand to document tuple format.\n *\n * - `8` → `8`\n * - `'8px'` → `8`\n * - `'8px 16px'` → `[8, 16]`\n * - `'8px 16px 8px 16px'` → `[8, 16, 8, 16]`\n * - `'8px 16px 12px'` → `[8, 16, 12, 16]` (CSS 3-value shorthand)\n */\nexport function parseBoxModel(\n value: string | number | undefined,\n rootSize = DEFAULT_ROOT_SIZE,\n): BoxModelResult {\n if (value == null) return undefined\n if (typeof value === 'number') return value\n\n const parts = value\n .trim()\n .split(/\\s+/)\n .map((p) => parseCssDimension(p, rootSize))\n\n const nums = parts.filter((p): p is number => p != null)\n if (nums.length !== parts.length) return undefined\n\n if (nums.length === 1) return nums[0]\n if (nums.length === 2) return [nums[0], nums[1]] as [number, number]\n if (nums.length === 3)\n return [nums[0], nums[1], nums[2], nums[1]] as [number, number, number, number]\n if (nums.length === 4)\n return [nums[0], nums[1], nums[2], nums[3]] as [number, number, number, number]\n\n return undefined\n}\n\n/**\n * Parse a CSS font-weight value.\n */\nexport function parseFontWeight(\n value: string | number | undefined,\n): 'normal' | 'bold' | number | undefined {\n if (value == null) return undefined\n if (typeof value === 'number') return value\n if (value === 'normal' || value === 'bold') return value\n const num = Number.parseInt(value, 10)\n if (!Number.isNaN(num)) return num\n return undefined\n}\n\n/**\n * Parse a CSS line-height value to a unitless number.\n */\nexport function parseLineHeight(\n value: string | number | undefined,\n rootSize = DEFAULT_ROOT_SIZE,\n): number | undefined {\n if (value == null) return undefined\n if (typeof value === 'number') return value\n if (value === 'normal') return undefined\n\n const dim = parseCssDimension(value, rootSize)\n if (dim != null) return dim\n\n return undefined\n}\n","import {\n parseBoxModel,\n parseCssDimension,\n parseFontWeight,\n parseLineHeight,\n} from './cssValueParser'\nimport type { ResolvedStyles } from './types'\n\nconst TEXT_ALIGN_VALUES = new Set(['left', 'center', 'right', 'justify'])\nconst FONT_STYLE_VALUES = new Set(['normal', 'italic'])\nconst TEXT_DECORATION_VALUES = new Set(['none', 'underline', 'line-through'])\nconst BORDER_STYLE_VALUES = new Set(['solid', 'dashed', 'dotted'])\n\n/**\n * Convert a rocketstyle `$rocketstyle` theme object into a `ResolvedStyles`\n * object compatible with `@pyreon/document`.\n *\n * Only extracts properties that `ResolvedStyles` supports — everything else\n * (transitions, cursor, display, etc.) is silently ignored.\n */\nexport function resolveStyles(rocketstyle: Record<string, unknown>, rootSize = 16): ResolvedStyles {\n const styles: ResolvedStyles = {}\n\n // Typography\n const fontSize = parseCssDimension(rocketstyle.fontSize as string | number, rootSize)\n if (fontSize != null) styles.fontSize = fontSize\n\n if (typeof rocketstyle.fontFamily === 'string') styles.fontFamily = rocketstyle.fontFamily\n\n const fontWeight = parseFontWeight(rocketstyle.fontWeight as string | number | undefined)\n if (fontWeight != null) styles.fontWeight = fontWeight\n\n if (typeof rocketstyle.fontStyle === 'string' && FONT_STYLE_VALUES.has(rocketstyle.fontStyle))\n styles.fontStyle = rocketstyle.fontStyle as 'normal' | 'italic'\n\n if (\n typeof rocketstyle.textDecoration === 'string' &&\n TEXT_DECORATION_VALUES.has(rocketstyle.textDecoration)\n )\n styles.textDecoration = rocketstyle.textDecoration as 'none' | 'underline' | 'line-through'\n\n if (typeof rocketstyle.color === 'string') styles.color = rocketstyle.color\n\n if (typeof rocketstyle.backgroundColor === 'string')\n styles.backgroundColor = rocketstyle.backgroundColor\n\n if (typeof rocketstyle.textAlign === 'string' && TEXT_ALIGN_VALUES.has(rocketstyle.textAlign))\n styles.textAlign = rocketstyle.textAlign as 'left' | 'center' | 'right' | 'justify'\n\n const lineHeight = parseLineHeight(\n rocketstyle.lineHeight as string | number | undefined,\n rootSize,\n )\n if (lineHeight != null) styles.lineHeight = lineHeight\n\n const letterSpacing = parseCssDimension(rocketstyle.letterSpacing as string | number, rootSize)\n if (letterSpacing != null) styles.letterSpacing = letterSpacing\n\n // Box model\n const padding = parseBoxModel(rocketstyle.padding as string | number | undefined, rootSize)\n if (padding != null) styles.padding = padding\n\n const margin = parseBoxModel(rocketstyle.margin as string | number | undefined, rootSize)\n if (margin != null) styles.margin = margin\n\n // Border\n const borderRadius = parseCssDimension(rocketstyle.borderRadius as string | number, rootSize)\n if (borderRadius != null) styles.borderRadius = borderRadius\n\n const borderWidth = parseCssDimension(rocketstyle.borderWidth as string | number, rootSize)\n if (borderWidth != null) styles.borderWidth = borderWidth\n\n if (typeof rocketstyle.borderColor === 'string') styles.borderColor = rocketstyle.borderColor\n\n if (\n typeof rocketstyle.borderStyle === 'string' &&\n BORDER_STYLE_VALUES.has(rocketstyle.borderStyle)\n )\n styles.borderStyle = rocketstyle.borderStyle as 'solid' | 'dashed' | 'dotted'\n\n // Sizing\n if (rocketstyle.width != null) {\n const w = parseCssDimension(rocketstyle.width as string | number, rootSize)\n styles.width = w ?? (rocketstyle.width as string)\n }\n\n if (rocketstyle.height != null) {\n const h = parseCssDimension(rocketstyle.height as string | number, rootSize)\n styles.height = h ?? (rocketstyle.height as string)\n }\n\n if (rocketstyle.maxWidth != null) {\n const mw = parseCssDimension(rocketstyle.maxWidth as string | number, rootSize)\n styles.maxWidth = mw ?? (rocketstyle.maxWidth as string)\n }\n\n // Opacity\n if (typeof rocketstyle.opacity === 'number') styles.opacity = rocketstyle.opacity\n\n return styles\n}\n","import { resolveStyles } from './resolveStyles'\nimport type { DocChild, DocNode, NodeType } from './types'\n\n/** Marker interface: components with _documentType are extractable. */\nexport interface DocumentMarker {\n _documentType: NodeType\n}\n\nexport interface ExtractOptions {\n /** Root font size for rem→px conversion. Default: 16. */\n rootSize?: number\n /** Include resolved styles from $rocketstyle. Default: true. */\n includeStyles?: boolean\n}\n\ntype VNodeLike = {\n type: string | ((...args: any[]) => any)\n props: Record<string, any>\n children: unknown[]\n}\n\nfunction isVNode(value: unknown): value is VNodeLike {\n return value != null && typeof value === 'object' && 'type' in value && 'props' in value\n}\n\nfunction getDocumentType(fn: unknown): NodeType | undefined {\n if (typeof fn !== 'function') return undefined\n const meta = (fn as any).meta\n if (meta?._documentType) return meta._documentType as NodeType\n // Fallback: check directly on function (non-rocketstyle components)\n if ('_documentType' in fn) return (fn as any)._documentType as NodeType\n return undefined\n}\n\nfunction flattenChildren(children: unknown[]): unknown[] {\n const result: unknown[] = []\n for (const child of children) {\n if (Array.isArray(child)) {\n result.push(...flattenChildren(child))\n } else if (typeof child === 'function') {\n // Reactive getter — call to resolve\n const resolved = child()\n if (Array.isArray(resolved)) {\n result.push(...flattenChildren(resolved))\n } else {\n result.push(resolved)\n }\n } else {\n result.push(child)\n }\n }\n return result\n}\n\nfunction extractChildren(children: unknown[], options: ExtractOptions): DocChild[] {\n const flat = flattenChildren(children)\n const result: DocChild[] = []\n\n for (const child of flat) {\n if (child == null || child === false || child === true) continue\n\n if (typeof child === 'string') {\n result.push(child)\n continue\n }\n\n if (typeof child === 'number') {\n result.push(String(child))\n continue\n }\n\n if (isVNode(child)) {\n const extracted = extractNode(child, options)\n if (Array.isArray(extracted)) {\n result.push(...extracted)\n } else if (extracted != null) {\n result.push(extracted)\n }\n }\n }\n\n return result\n}\n\nfunction extractNode(vnode: VNodeLike, options: ExtractOptions): DocNode | DocChild[] | null {\n const { type, props, children } = vnode\n const includeStyles = options.includeStyles !== false\n const rootSize = options.rootSize ?? 16\n\n // Component function with _documentType marker (via .statics() or direct)\n const docType = getDocumentType(type)\n if (docType) {\n // ── _documentProps resolution ────────────────────────────────────\n //\n // Two paths to find _documentProps on a documentType vnode:\n //\n // (A) **Pre-resolved on the JSX vnode itself** — used by\n // test fixtures that hand-construct vnodes without\n // going through rocketstyle. Less common in real usage.\n //\n // (B) **Post-attrs result of calling the component** — the\n // real-world path. When a real `DocDocument` (or any\n // rocketstyle primitive with `.statics({ _documentType })`)\n // is rendered via JSX, the JSX vnode's `props` are the\n // USER-PROVIDED props (e.g. `{ title, author }`) — NOT\n // `_documentProps`. The rocketstyle attrs HOC adds\n // `_documentProps` to the wrapped component's vnode by\n // running the `.attrs()` callback during invocation. To\n // see the post-attrs result, we must CALL the component\n // function and read from THAT vnode's props.\n //\n // We try path (A) first because mock-vnode tests rely on it\n // and we don't want to invoke component functions when we\n // don't have to. If path (A) yields no _documentProps, we\n // fall back to path (B) and call the component.\n //\n // **Function values in _documentProps are resolved at this\n // point** — primitives like DocDocument can store accessor\n // thunks (`() => string`) for reactive metadata, and the\n // export pipeline reads the LIVE value on each extraction.\n // See PR #197 for the original use case (resume builder).\n //\n // ── Architectural note ──────────────────────────────────────────\n //\n // Path B is a workaround. The architecturally cleaner fix is to\n // have rocketstyle's `.statics()` mechanism hoist `_documentProps`\n // (or its accessor functions) directly onto the component\n // function — so `extractNode` could read it via\n // `(type as { _documentProps?: ... })._documentProps` without\n // ever invoking the component.\n //\n // That would require teaching rocketstyle that `.statics()`\n // values can be derived from `.attrs()` callbacks. It's a\n // bigger change in `@pyreon/rocketstyle/src/utils/statics.ts`\n // and was deemed out of scope for PR #197. The current\n // workaround works because:\n //\n // 1. rocketstyle's attrs HOC is meant to be PURE setup —\n // no observable side effects on the second call.\n // 2. The idempotence test in\n // `document-primitives/src/__tests__/useDocumentExport.test.ts`\n // locks in the purity assumption: extracting twice produces\n // structurally equivalent doc nodes.\n // 3. Path A is tried first, so existing fast-path tests don't\n // pay the component-invocation cost.\n //\n // If a future primitive accidentally introduces a side effect\n // in its setup body, the idempotence test catches it. If\n // performance becomes a concern (extractDocumentTree is called\n // per export, not per render — so this is unlikely), the\n // architectural fix in rocketstyle becomes worth doing.\n\n let rawDocProps: Record<string, unknown> | undefined\n let extractedFromCall: VNodeLike | null = null\n\n // Path A: pre-resolved on the JSX vnode (test fixtures)\n if (props._documentProps && typeof props._documentProps === 'object') {\n rawDocProps = props._documentProps as Record<string, unknown>\n } else if (typeof type === 'function') {\n // Path B: invoke the component to get the post-attrs vnode\n const mergedProps = { ...props }\n if (children && children.length > 0) {\n mergedProps.children = children.length === 1 ? children[0] : children\n }\n const result = (type as (p: Record<string, unknown>) => unknown)(mergedProps)\n if (isVNode(result)) {\n extractedFromCall = result\n const innerProps = (result as { props?: Record<string, unknown> }).props\n if (innerProps?._documentProps && typeof innerProps._documentProps === 'object') {\n rawDocProps = innerProps._documentProps as Record<string, unknown>\n }\n }\n }\n\n // Resolve function values (accessors) at extraction time\n const docProps: Record<string, unknown> = {}\n if (rawDocProps) {\n for (const [key, value] of Object.entries(rawDocProps)) {\n docProps[key] = typeof value === 'function' ? (value as () => unknown)() : value\n }\n }\n\n // Resolve styles from $rocketstyle. Look on the JSX vnode props\n // first; if the call result has its own $rocketstyle (because the\n // post-attrs vnode carries it down), use that as a fallback.\n const stylesSource =\n props.$rocketstyle ??\n (extractedFromCall as { props?: Record<string, unknown> } | null)?.props?.$rocketstyle\n const styles =\n includeStyles && stylesSource\n ? resolveStyles(stylesSource as Record<string, unknown>, rootSize)\n : undefined\n\n // Children: prefer the JSX vnode's children (the user-supplied\n // tree). The post-attrs call might wrap children in additional\n // styled elements that aren't part of the document tree.\n const docChildren = extractChildren(children ?? [], options)\n\n const node: DocNode = {\n type: docType,\n props: docProps,\n children: docChildren,\n }\n\n if (styles && Object.keys(styles).length > 0) {\n node.styles = styles\n }\n\n return node\n }\n\n // Component function WITHOUT _documentType — call it to get its VNode output\n if (typeof type === 'function') {\n const mergedProps = { ...props }\n if (children && children.length > 0) {\n mergedProps.children = children.length === 1 ? children[0] : children\n }\n\n const result = type(mergedProps)\n\n if (isVNode(result)) {\n return extractNode(result, options)\n }\n\n // The component returned a primitive or null\n if (typeof result === 'string') return [result]\n if (typeof result === 'number') return [String(result)]\n return null\n }\n\n // DOM element (string type like 'div', 'span') — transparent, extract children\n if (typeof type === 'string') {\n const docChildren = extractChildren(children ?? [], options)\n // If there's text content in the DOM element, collect it\n if (docChildren.length > 0) return docChildren\n return null\n }\n\n return null\n}\n\n/**\n * Walk a Pyreon VNode tree and extract a `DocNode` tree for `@pyreon/document`.\n *\n * For each VNode whose component has a `_documentType` marker:\n * 1. Read `_documentType` → `DocNode.type`\n * 2. Read `_documentProps` → `DocNode.props`\n * 3. Read `$rocketstyle` → `resolveStyles()` → `DocNode.styles`\n * 4. Recurse into children\n *\n * VNodes without `_documentType` are transparent — their children\n * are flattened into the parent's children list.\n */\nexport function extractDocumentTree(vnode: unknown, options: ExtractOptions = {}): DocNode {\n if (isVNode(vnode)) {\n const result = extractNode(vnode, options)\n if (result && !Array.isArray(result)) return result\n\n // Wrap loose children in a document node\n const children = Array.isArray(result) ? result : []\n return { type: 'document', props: {}, children }\n }\n\n // If passed a component function directly, call it\n if (typeof vnode === 'function') {\n const result = (vnode as () => unknown)()\n return extractDocumentTree(result, options)\n }\n\n return { type: 'document', props: {}, children: [] }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/connector-document",
3
- "version": "0.12.10",
3
+ "version": "0.12.12",
4
4
  "description": "Bridge between @pyreon/pyreon styled components and @pyreon/document rendering",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,14 +40,14 @@
40
40
  "typecheck": "tsc --noEmit"
41
41
  },
42
42
  "devDependencies": {
43
- "@pyreon/core": "^0.12.10",
44
- "@pyreon/document": "^0.12.10",
45
- "@pyreon/typescript": "^0.12.10",
43
+ "@pyreon/core": "^0.12.12",
44
+ "@pyreon/document": "^0.12.12",
45
+ "@pyreon/typescript": "^0.12.12",
46
46
  "@vitus-labs/tools-rolldown": "^1.15.4"
47
47
  },
48
48
  "peerDependencies": {
49
- "@pyreon/core": "^0.12.10",
50
- "@pyreon/document": "^0.12.10"
49
+ "@pyreon/core": "^0.12.12",
50
+ "@pyreon/document": "^0.12.12"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">= 22"
@@ -150,4 +150,203 @@ describe('extractDocumentTree', () => {
150
150
  expect(result.type).toBe('document')
151
151
  expect(result.children).toEqual([])
152
152
  })
153
+
154
+ describe('_documentProps function value resolution (D1)', () => {
155
+ // Document primitives like DocDocument now accept reactive
156
+ // accessors (e.g. `title={() => store.name()}`) and store the
157
+ // function in _documentProps. extractDocumentTree resolves the
158
+ // function at extraction time so the export pipeline always
159
+ // sees the live value, not a stale snapshot from component
160
+ // mount time.
161
+ //
162
+ // These tests lock in the behavior at the connector-document
163
+ // boundary so any future change to the resolution logic gets
164
+ // caught by a focused unit test.
165
+
166
+ it('calls function values in _documentProps and stores the result', () => {
167
+ const Document = docComponent('document')
168
+ const tree = vnode(
169
+ Document,
170
+ {
171
+ _documentProps: {
172
+ title: () => 'Resolved title',
173
+ author: () => 'Alice',
174
+ subject: 'Plain string still works',
175
+ },
176
+ },
177
+ [],
178
+ )
179
+
180
+ const result = extractDocumentTree(tree)
181
+
182
+ expect(result.props).toEqual({
183
+ title: 'Resolved title',
184
+ author: 'Alice',
185
+ subject: 'Plain string still works',
186
+ })
187
+ })
188
+
189
+ it('reads the live value each time extractDocumentTree is called', () => {
190
+ // The whole point of accessors: every export call sees the
191
+ // current state, not a value frozen at component mount.
192
+ // We simulate this by mutating the closure variable between
193
+ // two extractDocumentTree calls on the same vnode.
194
+ let counter = 0
195
+ const Document = docComponent('document')
196
+ const tree = vnode(
197
+ Document,
198
+ {
199
+ _documentProps: {
200
+ title: () => `Export ${++counter}`,
201
+ },
202
+ },
203
+ [],
204
+ )
205
+
206
+ const first = extractDocumentTree(tree)
207
+ const second = extractDocumentTree(tree)
208
+ const third = extractDocumentTree(tree)
209
+
210
+ expect(first.props.title).toBe('Export 1')
211
+ expect(second.props.title).toBe('Export 2')
212
+ expect(third.props.title).toBe('Export 3')
213
+ })
214
+
215
+ it('mixes function and plain values in the same _documentProps object', () => {
216
+ const Image = docComponent('image')
217
+ const tree = vnode(
218
+ Image,
219
+ {
220
+ _documentProps: {
221
+ src: 'static-url.png', // plain
222
+ alt: () => 'dynamic alt text', // accessor
223
+ width: 800, // plain number
224
+ caption: () => 'caption ' + 42, // accessor returning a string
225
+ },
226
+ },
227
+ [],
228
+ )
229
+
230
+ const result = extractDocumentTree(tree)
231
+ expect(result.props).toEqual({
232
+ src: 'static-url.png',
233
+ alt: 'dynamic alt text',
234
+ width: 800,
235
+ caption: 'caption 42',
236
+ })
237
+ })
238
+
239
+ it('preserves backward compatibility — existing primitives with plain props still work', () => {
240
+ // Regression case: every existing document primitive uses
241
+ // plain values in _documentProps. The function-resolution
242
+ // change must not break them. This test mirrors the shape
243
+ // of DocHeading's existing _documentProps.
244
+ const Heading = docComponent('heading')
245
+ const tree = vnode(
246
+ Heading,
247
+ { _documentProps: { level: 1 } },
248
+ ['Hello'],
249
+ )
250
+
251
+ const result = extractDocumentTree(tree)
252
+ expect(result.props).toEqual({ level: 1 })
253
+ expect(typeof result.props.level).toBe('number')
254
+ })
255
+ })
256
+
257
+ describe('component invocation path (extracts from post-attrs vnodes)', () => {
258
+ // Real-world case: rocketstyle-based primitives never have
259
+ // `_documentProps` on the JSX vnode. The user passes
260
+ // `<DocDocument title="X" />`, which produces a JSX vnode
261
+ // with `props = { title: 'X' }`. The `_documentProps` only
262
+ // appears AFTER the rocketstyle attrs HOC runs the
263
+ // `.attrs()` callback.
264
+ //
265
+ // Before the fix, extractDocumentTree only looked for
266
+ // `_documentProps` on the JSX vnode's props directly — so
267
+ // every real primitive's metadata was silently dropped during
268
+ // export. The mock-vnode tests above hand-constructed
269
+ // `_documentProps` to bypass this and never noticed.
270
+ //
271
+ // After the fix, extractDocumentTree CALLS the component
272
+ // function for documentType vnodes that don't have
273
+ // `_documentProps` directly, captures the post-attrs result,
274
+ // and reads `_documentProps` from THAT.
275
+ //
276
+ // These tests use a hand-constructed component that mimics
277
+ // the rocketstyle attrs pattern: the component is a function
278
+ // with `_documentType` set, and calling it returns a vnode
279
+ // whose props contain `_documentProps`. No real rocketstyle
280
+ // dependency needed for the unit test.
281
+
282
+ it('calls the component function and reads _documentProps from the post-attrs vnode', () => {
283
+ // Component that mimics a rocketstyle-wrapped primitive:
284
+ // takes user props, returns a vnode with _documentProps
285
+ // populated by the "attrs callback".
286
+ const DocDocLike = ((userProps: { title?: string; author?: string }) =>
287
+ vnode('div', {
288
+ _documentProps: {
289
+ ...(userProps.title ? { title: userProps.title } : {}),
290
+ ...(userProps.author ? { author: userProps.author } : {}),
291
+ },
292
+ })) as ((...args: any[]) => any) & DocumentMarker
293
+ ;(DocDocLike as any)._documentType = 'document'
294
+
295
+ // The JSX vnode has user props but NO _documentProps directly
296
+ const jsxVnode = vnode(DocDocLike, { title: 'My Doc', author: 'Alice' }, [])
297
+
298
+ const result = extractDocumentTree(jsxVnode)
299
+
300
+ expect(result.type).toBe('document')
301
+ expect(result.props.title).toBe('My Doc')
302
+ expect(result.props.author).toBe('Alice')
303
+ })
304
+
305
+ it('also resolves function values from the post-attrs path', () => {
306
+ // The two fixes compose: a real primitive that stores
307
+ // accessor functions in its _documentProps (via the attrs
308
+ // callback) gets the live value resolved at extraction time.
309
+ let liveTitle = 'First'
310
+ const DocDocLike = ((userProps: { title?: () => string }) =>
311
+ vnode('div', {
312
+ _documentProps: {
313
+ title: userProps.title, // store the accessor as-is
314
+ },
315
+ })) as ((...args: any[]) => any) & DocumentMarker
316
+ ;(DocDocLike as any)._documentType = 'document'
317
+
318
+ const jsxVnode = vnode(DocDocLike, { title: () => liveTitle }, [])
319
+
320
+ const first = extractDocumentTree(jsxVnode)
321
+ expect(first.props.title).toBe('First')
322
+
323
+ liveTitle = 'Second'
324
+ const second = extractDocumentTree(jsxVnode)
325
+ expect(second.props.title).toBe('Second')
326
+ })
327
+
328
+ it('prefers JSX-vnode _documentProps when both paths are available (back-compat)', () => {
329
+ // If a vnode has _documentProps directly on its props (the
330
+ // mock-vnode test pattern), extractDocumentTree should use
331
+ // it WITHOUT calling the component. This preserves the
332
+ // existing tests' fast path and avoids invoking components
333
+ // unnecessarily.
334
+ let componentCalled = false
335
+ const DocDocLike = (() => {
336
+ componentCalled = true
337
+ return vnode('div', { _documentProps: { title: 'from-call' } })
338
+ }) as ((...args: any[]) => any) & DocumentMarker
339
+ ;(DocDocLike as any)._documentType = 'document'
340
+
341
+ const jsxVnode = vnode(
342
+ DocDocLike,
343
+ { _documentProps: { title: 'from-jsx' } },
344
+ [],
345
+ )
346
+
347
+ const result = extractDocumentTree(jsxVnode)
348
+ expect(result.props.title).toBe('from-jsx')
349
+ expect(componentCalled).toBe(false)
350
+ })
351
+ })
153
352
  })
@@ -90,20 +90,110 @@ function extractNode(vnode: VNodeLike, options: ExtractOptions): DocNode | DocCh
90
90
  // Component function with _documentType marker (via .statics() or direct)
91
91
  const docType = getDocumentType(type)
92
92
  if (docType) {
93
- const docProps: Record<string, unknown> = {}
93
+ // ── _documentProps resolution ────────────────────────────────────
94
+ //
95
+ // Two paths to find _documentProps on a documentType vnode:
96
+ //
97
+ // (A) **Pre-resolved on the JSX vnode itself** — used by
98
+ // test fixtures that hand-construct vnodes without
99
+ // going through rocketstyle. Less common in real usage.
100
+ //
101
+ // (B) **Post-attrs result of calling the component** — the
102
+ // real-world path. When a real `DocDocument` (or any
103
+ // rocketstyle primitive with `.statics({ _documentType })`)
104
+ // is rendered via JSX, the JSX vnode's `props` are the
105
+ // USER-PROVIDED props (e.g. `{ title, author }`) — NOT
106
+ // `_documentProps`. The rocketstyle attrs HOC adds
107
+ // `_documentProps` to the wrapped component's vnode by
108
+ // running the `.attrs()` callback during invocation. To
109
+ // see the post-attrs result, we must CALL the component
110
+ // function and read from THAT vnode's props.
111
+ //
112
+ // We try path (A) first because mock-vnode tests rely on it
113
+ // and we don't want to invoke component functions when we
114
+ // don't have to. If path (A) yields no _documentProps, we
115
+ // fall back to path (B) and call the component.
116
+ //
117
+ // **Function values in _documentProps are resolved at this
118
+ // point** — primitives like DocDocument can store accessor
119
+ // thunks (`() => string`) for reactive metadata, and the
120
+ // export pipeline reads the LIVE value on each extraction.
121
+ // See PR #197 for the original use case (resume builder).
122
+ //
123
+ // ── Architectural note ──────────────────────────────────────────
124
+ //
125
+ // Path B is a workaround. The architecturally cleaner fix is to
126
+ // have rocketstyle's `.statics()` mechanism hoist `_documentProps`
127
+ // (or its accessor functions) directly onto the component
128
+ // function — so `extractNode` could read it via
129
+ // `(type as { _documentProps?: ... })._documentProps` without
130
+ // ever invoking the component.
131
+ //
132
+ // That would require teaching rocketstyle that `.statics()`
133
+ // values can be derived from `.attrs()` callbacks. It's a
134
+ // bigger change in `@pyreon/rocketstyle/src/utils/statics.ts`
135
+ // and was deemed out of scope for PR #197. The current
136
+ // workaround works because:
137
+ //
138
+ // 1. rocketstyle's attrs HOC is meant to be PURE setup —
139
+ // no observable side effects on the second call.
140
+ // 2. The idempotence test in
141
+ // `document-primitives/src/__tests__/useDocumentExport.test.ts`
142
+ // locks in the purity assumption: extracting twice produces
143
+ // structurally equivalent doc nodes.
144
+ // 3. Path A is tried first, so existing fast-path tests don't
145
+ // pay the component-invocation cost.
146
+ //
147
+ // If a future primitive accidentally introduces a side effect
148
+ // in its setup body, the idempotence test catches it. If
149
+ // performance becomes a concern (extractDocumentTree is called
150
+ // per export, not per render — so this is unlikely), the
151
+ // architectural fix in rocketstyle becomes worth doing.
152
+
153
+ let rawDocProps: Record<string, unknown> | undefined
154
+ let extractedFromCall: VNodeLike | null = null
94
155
 
95
- // Extract document-specific props from _documentProps
156
+ // Path A: pre-resolved on the JSX vnode (test fixtures)
96
157
  if (props._documentProps && typeof props._documentProps === 'object') {
97
- Object.assign(docProps, props._documentProps)
158
+ rawDocProps = props._documentProps as Record<string, unknown>
159
+ } else if (typeof type === 'function') {
160
+ // Path B: invoke the component to get the post-attrs vnode
161
+ const mergedProps = { ...props }
162
+ if (children && children.length > 0) {
163
+ mergedProps.children = children.length === 1 ? children[0] : children
164
+ }
165
+ const result = (type as (p: Record<string, unknown>) => unknown)(mergedProps)
166
+ if (isVNode(result)) {
167
+ extractedFromCall = result
168
+ const innerProps = (result as { props?: Record<string, unknown> }).props
169
+ if (innerProps?._documentProps && typeof innerProps._documentProps === 'object') {
170
+ rawDocProps = innerProps._documentProps as Record<string, unknown>
171
+ }
172
+ }
173
+ }
174
+
175
+ // Resolve function values (accessors) at extraction time
176
+ const docProps: Record<string, unknown> = {}
177
+ if (rawDocProps) {
178
+ for (const [key, value] of Object.entries(rawDocProps)) {
179
+ docProps[key] = typeof value === 'function' ? (value as () => unknown)() : value
180
+ }
98
181
  }
99
182
 
100
- // Resolve styles from $rocketstyle
183
+ // Resolve styles from $rocketstyle. Look on the JSX vnode props
184
+ // first; if the call result has its own $rocketstyle (because the
185
+ // post-attrs vnode carries it down), use that as a fallback.
186
+ const stylesSource =
187
+ props.$rocketstyle ??
188
+ (extractedFromCall as { props?: Record<string, unknown> } | null)?.props?.$rocketstyle
101
189
  const styles =
102
- includeStyles && props.$rocketstyle
103
- ? resolveStyles(props.$rocketstyle as Record<string, unknown>, rootSize)
190
+ includeStyles && stylesSource
191
+ ? resolveStyles(stylesSource as Record<string, unknown>, rootSize)
104
192
  : undefined
105
193
 
106
- // Recurse into children
194
+ // Children: prefer the JSX vnode's children (the user-supplied
195
+ // tree). The post-attrs call might wrap children in additional
196
+ // styled elements that aren't part of the document tree.
107
197
  const docChildren = extractChildren(children ?? [], options)
108
198
 
109
199
  const node: DocNode = {