@pyreon/connector-document 0.12.10 → 0.12.11
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.
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +16 -2
- package/lib/index.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/extractDocumentTree.test.ts +199 -0
- package/src/extractDocumentTree.ts +97 -7
package/lib/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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 (
|
|
192
|
-
const
|
|
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":";AAAA,MAAM,QAAQ;AACd,MAAM,SAAS;AACf,MAAM,QAAQ;AACd,MAAM,QAAQ;AACd,MAAM,YAAY;AAElB,MAAM,oBAAoB;;;;;;;;;;AAW1B,SAAgB,kBACd,OACA,WAAW,mBACS;AACpB,KAAI,SAAS,KAAM,QAAO;AAC1B,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,OAAO,UAAU,SAAU,QAAO;CAEtC,MAAM,UAAU,MAAM,MAAM;CAE5B,MAAM,UAAU,MAAM,KAAK,QAAQ;AACnC,KAAI,UAAU,GAAI,QAAO,OAAO,WAAW,QAAQ,GAAG;CAEtD,MAAM,WAAW,OAAO,KAAK,QAAQ;AACrC,KAAI,WAAW,GAAI,QAAO,OAAO,WAAW,SAAS,GAAG,GAAG;CAE3D,MAAM,UAAU,MAAM,KAAK,QAAQ;AACnC,KAAI,UAAU,GAAI,QAAO,OAAO,WAAW,QAAQ,GAAG,GAAG;CAEzD,MAAM,UAAU,MAAM,KAAK,QAAQ;AACnC,KAAI,UAAU,GAAI,QAAO,OAAO,WAAW,QAAQ,GAAG,IAAI,IAAI;AAE9D,KAAI,UAAU,KAAK,QAAQ,CAAE,QAAO,OAAO,WAAW,QAAQ;;;;;;;;;;;AAgBhE,SAAgB,cACd,OACA,WAAW,mBACK;AAChB,KAAI,SAAS,KAAM,QAAO;AAC1B,KAAI,OAAO,UAAU,SAAU,QAAO;CAEtC,MAAM,QAAQ,MACX,MAAM,CACN,MAAM,MAAM,CACZ,KAAK,MAAM,kBAAkB,GAAG,SAAS,CAAC;CAE7C,MAAM,OAAO,MAAM,QAAQ,MAAmB,KAAK,KAAK;AACxD,KAAI,KAAK,WAAW,MAAM,OAAQ,QAAO;AAEzC,KAAI,KAAK,WAAW,EAAG,QAAO,KAAK;AACnC,KAAI,KAAK,WAAW,EAAG,QAAO,CAAC,KAAK,IAAI,KAAK,GAAG;AAChD,KAAI,KAAK,WAAW,EAClB,QAAO;EAAC,KAAK;EAAI,KAAK;EAAI,KAAK;EAAI,KAAK;EAAG;AAC7C,KAAI,KAAK,WAAW,EAClB,QAAO;EAAC,KAAK;EAAI,KAAK;EAAI,KAAK;EAAI,KAAK;EAAG;;;;;AAQ/C,SAAgB,gBACd,OACwC;AACxC,KAAI,SAAS,KAAM,QAAO;AAC1B,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,UAAU,YAAY,UAAU,OAAQ,QAAO;CACnD,MAAM,MAAM,OAAO,SAAS,OAAO,GAAG;AACtC,KAAI,CAAC,OAAO,MAAM,IAAI,CAAE,QAAO;;;;;AAOjC,SAAgB,gBACd,OACA,WAAW,mBACS;AACpB,KAAI,SAAS,KAAM,QAAO;AAC1B,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,UAAU,SAAU,QAAO;CAE/B,MAAM,MAAM,kBAAkB,OAAO,SAAS;AAC9C,KAAI,OAAO,KAAM,QAAO;;;;;AClG1B,MAAM,oBAAoB,IAAI,IAAI;CAAC;CAAQ;CAAU;CAAS;CAAU,CAAC;AACzE,MAAM,oBAAoB,IAAI,IAAI,CAAC,UAAU,SAAS,CAAC;AACvD,MAAM,yBAAyB,IAAI,IAAI;CAAC;CAAQ;CAAa;CAAe,CAAC;AAC7E,MAAM,sBAAsB,IAAI,IAAI;CAAC;CAAS;CAAU;CAAS,CAAC;;;;;;;;AASlE,SAAgB,cAAc,aAAsC,WAAW,IAAoB;CACjG,MAAM,SAAyB,EAAE;CAGjC,MAAM,WAAW,kBAAkB,YAAY,UAA6B,SAAS;AACrF,KAAI,YAAY,KAAM,QAAO,WAAW;AAExC,KAAI,OAAO,YAAY,eAAe,SAAU,QAAO,aAAa,YAAY;CAEhF,MAAM,aAAa,gBAAgB,YAAY,WAA0C;AACzF,KAAI,cAAc,KAAM,QAAO,aAAa;AAE5C,KAAI,OAAO,YAAY,cAAc,YAAY,kBAAkB,IAAI,YAAY,UAAU,CAC3F,QAAO,YAAY,YAAY;AAEjC,KACE,OAAO,YAAY,mBAAmB,YACtC,uBAAuB,IAAI,YAAY,eAAe,CAEtD,QAAO,iBAAiB,YAAY;AAEtC,KAAI,OAAO,YAAY,UAAU,SAAU,QAAO,QAAQ,YAAY;AAEtE,KAAI,OAAO,YAAY,oBAAoB,SACzC,QAAO,kBAAkB,YAAY;AAEvC,KAAI,OAAO,YAAY,cAAc,YAAY,kBAAkB,IAAI,YAAY,UAAU,CAC3F,QAAO,YAAY,YAAY;CAEjC,MAAM,aAAa,gBACjB,YAAY,YACZ,SACD;AACD,KAAI,cAAc,KAAM,QAAO,aAAa;CAE5C,MAAM,gBAAgB,kBAAkB,YAAY,eAAkC,SAAS;AAC/F,KAAI,iBAAiB,KAAM,QAAO,gBAAgB;CAGlD,MAAM,UAAU,cAAc,YAAY,SAAwC,SAAS;AAC3F,KAAI,WAAW,KAAM,QAAO,UAAU;CAEtC,MAAM,SAAS,cAAc,YAAY,QAAuC,SAAS;AACzF,KAAI,UAAU,KAAM,QAAO,SAAS;CAGpC,MAAM,eAAe,kBAAkB,YAAY,cAAiC,SAAS;AAC7F,KAAI,gBAAgB,KAAM,QAAO,eAAe;CAEhD,MAAM,cAAc,kBAAkB,YAAY,aAAgC,SAAS;AAC3F,KAAI,eAAe,KAAM,QAAO,cAAc;AAE9C,KAAI,OAAO,YAAY,gBAAgB,SAAU,QAAO,cAAc,YAAY;AAElF,KACE,OAAO,YAAY,gBAAgB,YACnC,oBAAoB,IAAI,YAAY,YAAY,CAEhD,QAAO,cAAc,YAAY;AAGnC,KAAI,YAAY,SAAS,KAEvB,QAAO,QADG,kBAAkB,YAAY,OAA0B,SAAS,IACtD,YAAY;AAGnC,KAAI,YAAY,UAAU,KAExB,QAAO,SADG,kBAAkB,YAAY,QAA2B,SAAS,IACtD,YAAY;AAGpC,KAAI,YAAY,YAAY,KAE1B,QAAO,WADI,kBAAkB,YAAY,UAA6B,SAAS,IACtD,YAAY;AAIvC,KAAI,OAAO,YAAY,YAAY,SAAU,QAAO,UAAU,YAAY;AAE1E,QAAO;;;;;AC9ET,SAAS,QAAQ,OAAoC;AACnD,QAAO,SAAS,QAAQ,OAAO,UAAU,YAAY,UAAU,SAAS,WAAW;;AAGrF,SAAS,gBAAgB,IAAmC;AAC1D,KAAI,OAAO,OAAO,WAAY,QAAO;CACrC,MAAM,OAAQ,GAAW;AACzB,KAAI,MAAM,cAAe,QAAO,KAAK;AAErC,KAAI,mBAAmB,GAAI,QAAQ,GAAW;;AAIhD,SAAS,gBAAgB,UAAgC;CACvD,MAAM,SAAoB,EAAE;AAC5B,MAAK,MAAM,SAAS,SAClB,KAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,KAAK,GAAG,gBAAgB,MAAM,CAAC;UAC7B,OAAO,UAAU,YAAY;EAEtC,MAAM,WAAW,OAAO;AACxB,MAAI,MAAM,QAAQ,SAAS,CACzB,QAAO,KAAK,GAAG,gBAAgB,SAAS,CAAC;MAEzC,QAAO,KAAK,SAAS;OAGvB,QAAO,KAAK,MAAM;AAGtB,QAAO;;AAGT,SAAS,gBAAgB,UAAqB,SAAqC;CACjF,MAAM,OAAO,gBAAgB,SAAS;CACtC,MAAM,SAAqB,EAAE;AAE7B,MAAK,MAAM,SAAS,MAAM;AACxB,MAAI,SAAS,QAAQ,UAAU,SAAS,UAAU,KAAM;AAExD,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAO,KAAK,MAAM;AAClB;;AAGF,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAO,KAAK,OAAO,MAAM,CAAC;AAC1B;;AAGF,MAAI,QAAQ,MAAM,EAAE;GAClB,MAAM,YAAY,YAAY,OAAO,QAAQ;AAC7C,OAAI,MAAM,QAAQ,UAAU,CAC1B,QAAO,KAAK,GAAG,UAAU;YAChB,aAAa,KACtB,QAAO,KAAK,UAAU;;;AAK5B,QAAO;;AAGT,SAAS,YAAY,OAAkB,SAAsD;CAC3F,MAAM,EAAE,MAAM,OAAO,aAAa;CAClC,MAAM,gBAAgB,QAAQ,kBAAkB;CAChD,MAAM,WAAW,QAAQ,YAAY;CAGrC,MAAM,UAAU,gBAAgB,KAAK;AACrC,KAAI,SAAS;EA6DX,IAAI;EACJ,IAAI,oBAAsC;AAG1C,MAAI,MAAM,kBAAkB,OAAO,MAAM,mBAAmB,SAC1D,eAAc,MAAM;WACX,OAAO,SAAS,YAAY;GAErC,MAAM,cAAc,EAAE,GAAG,OAAO;AAChC,OAAI,YAAY,SAAS,SAAS,EAChC,aAAY,WAAW,SAAS,WAAW,IAAI,SAAS,KAAK;GAE/D,MAAM,SAAU,KAAiD,YAAY;AAC7E,OAAI,QAAQ,OAAO,EAAE;AACnB,wBAAoB;IACpB,MAAM,aAAc,OAA+C;AACnE,QAAI,YAAY,kBAAkB,OAAO,WAAW,mBAAmB,SACrE,eAAc,WAAW;;;EAM/B,MAAM,WAAoC,EAAE;AAC5C,MAAI,YACF,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,YAAY,CACpD,UAAS,OAAO,OAAO,UAAU,aAAc,OAAyB,GAAG;EAO/E,MAAM,eACJ,MAAM,gBACL,mBAAkE,OAAO;EAC5E,MAAM,SACJ,iBAAiB,eACb,cAAc,cAAyC,SAAS,GAChE;EAON,MAAM,OAAgB;GACpB,MAAM;GACN,OAAO;GACP,UALkB,gBAAgB,YAAY,EAAE,EAAE,QAAQ;GAM3D;AAED,MAAI,UAAU,OAAO,KAAK,OAAO,CAAC,SAAS,EACzC,MAAK,SAAS;AAGhB,SAAO;;AAIT,KAAI,OAAO,SAAS,YAAY;EAC9B,MAAM,cAAc,EAAE,GAAG,OAAO;AAChC,MAAI,YAAY,SAAS,SAAS,EAChC,aAAY,WAAW,SAAS,WAAW,IAAI,SAAS,KAAK;EAG/D,MAAM,SAAS,KAAK,YAAY;AAEhC,MAAI,QAAQ,OAAO,CACjB,QAAO,YAAY,QAAQ,QAAQ;AAIrC,MAAI,OAAO,WAAW,SAAU,QAAO,CAAC,OAAO;AAC/C,MAAI,OAAO,WAAW,SAAU,QAAO,CAAC,OAAO,OAAO,CAAC;AACvD,SAAO;;AAIT,KAAI,OAAO,SAAS,UAAU;EAC5B,MAAM,cAAc,gBAAgB,YAAY,EAAE,EAAE,QAAQ;AAE5D,MAAI,YAAY,SAAS,EAAG,QAAO;AACnC,SAAO;;AAGT,QAAO;;;;;;;;;;;;;;AAeT,SAAgB,oBAAoB,OAAgB,UAA0B,EAAE,EAAW;AACzF,KAAI,QAAQ,MAAM,EAAE;EAClB,MAAM,SAAS,YAAY,OAAO,QAAQ;AAC1C,MAAI,UAAU,CAAC,MAAM,QAAQ,OAAO,CAAE,QAAO;AAI7C,SAAO;GAAE,MAAM;GAAY,OAAO,EAAE;GAAE,UADrB,MAAM,QAAQ,OAAO,GAAG,SAAS,EAAE;GACJ;;AAIlD,KAAI,OAAO,UAAU,WAEnB,QAAO,oBADS,OAAyB,EACN,QAAQ;AAG7C,QAAO;EAAE,MAAM;EAAY,OAAO,EAAE;EAAE,UAAU,EAAE;EAAE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/connector-document",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.11",
|
|
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.
|
|
44
|
-
"@pyreon/document": "^0.12.
|
|
45
|
-
"@pyreon/typescript": "^0.12.
|
|
43
|
+
"@pyreon/core": "^0.12.11",
|
|
44
|
+
"@pyreon/document": "^0.12.11",
|
|
45
|
+
"@pyreon/typescript": "^0.12.11",
|
|
46
46
|
"@vitus-labs/tools-rolldown": "^1.15.4"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@pyreon/core": "^0.12.
|
|
50
|
-
"@pyreon/document": "^0.12.
|
|
49
|
+
"@pyreon/core": "^0.12.11",
|
|
50
|
+
"@pyreon/document": "^0.12.11"
|
|
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
|
-
|
|
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
|
-
//
|
|
156
|
+
// Path A: pre-resolved on the JSX vnode (test fixtures)
|
|
96
157
|
if (props._documentProps && typeof props._documentProps === 'object') {
|
|
97
|
-
|
|
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 &&
|
|
103
|
-
? resolveStyles(
|
|
190
|
+
includeStyles && stylesSource
|
|
191
|
+
? resolveStyles(stylesSource as Record<string, unknown>, rootSize)
|
|
104
192
|
: undefined
|
|
105
193
|
|
|
106
|
-
//
|
|
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 = {
|