@pyreon/connector-document 0.24.4 → 0.24.6

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,110 +0,0 @@
1
- const PX_RE = /^(-?\d+(?:\.\d+)?)px$/
2
- const REM_RE = /^(-?\d+(?:\.\d+)?)rem$/
3
- const EM_RE = /^(-?\d+(?:\.\d+)?)em$/
4
- const PT_RE = /^(-?\d+(?:\.\d+)?)pt$/
5
- const NUMBER_RE = /^-?\d+(?:\.\d+)?$/
6
-
7
- const DEFAULT_ROOT_SIZE = 16
8
-
9
- /**
10
- * Parse a CSS dimension value to a number.
11
- *
12
- * - `14` → `14`
13
- * - `'14px'` → `14`
14
- * - `'1.5rem'` → `24` (with rootSize=16)
15
- * - `'12pt'` → `16` (pt × 1.333)
16
- * - `'auto'` → `undefined`
17
- */
18
- export function parseCssDimension(
19
- value: string | number | null | undefined,
20
- rootSize = DEFAULT_ROOT_SIZE,
21
- ): number | undefined {
22
- if (value == null) return undefined
23
- if (typeof value === 'number') return value
24
- if (typeof value !== 'string') return undefined
25
-
26
- const trimmed = value.trim()
27
-
28
- const pxMatch = PX_RE.exec(trimmed)
29
- if (pxMatch?.[1]) return Number.parseFloat(pxMatch[1])
30
-
31
- const remMatch = REM_RE.exec(trimmed)
32
- if (remMatch?.[1]) return Number.parseFloat(remMatch[1]) * rootSize
33
-
34
- const emMatch = EM_RE.exec(trimmed)
35
- if (emMatch?.[1]) return Number.parseFloat(emMatch[1]) * rootSize
36
-
37
- const ptMatch = PT_RE.exec(trimmed)
38
- if (ptMatch?.[1]) return Number.parseFloat(ptMatch[1]) * (4 / 3)
39
-
40
- if (NUMBER_RE.test(trimmed)) return Number.parseFloat(trimmed)
41
-
42
- return undefined
43
- }
44
-
45
- type BoxModelResult = number | [number, number] | [number, number, number, number] | undefined
46
-
47
- /**
48
- * Parse a CSS padding/margin shorthand to document tuple format.
49
- *
50
- * - `8` → `8`
51
- * - `'8px'` → `8`
52
- * - `'8px 16px'` → `[8, 16]`
53
- * - `'8px 16px 8px 16px'` → `[8, 16, 8, 16]`
54
- * - `'8px 16px 12px'` → `[8, 16, 12, 16]` (CSS 3-value shorthand)
55
- */
56
- export function parseBoxModel(
57
- value: string | number | undefined,
58
- rootSize = DEFAULT_ROOT_SIZE,
59
- ): BoxModelResult {
60
- if (value == null) return undefined
61
- if (typeof value === 'number') return value
62
-
63
- const parts = value
64
- .trim()
65
- .split(/\s+/)
66
- .map((p) => parseCssDimension(p, rootSize))
67
-
68
- const nums = parts.filter((p): p is number => p != null)
69
- if (nums.length !== parts.length) return undefined
70
-
71
- if (nums.length === 1) return nums[0]
72
- if (nums.length === 2) return [nums[0], nums[1]] as [number, number]
73
- if (nums.length === 3)
74
- return [nums[0], nums[1], nums[2], nums[1]] as [number, number, number, number]
75
- if (nums.length === 4)
76
- return [nums[0], nums[1], nums[2], nums[3]] as [number, number, number, number]
77
-
78
- return undefined
79
- }
80
-
81
- /**
82
- * Parse a CSS font-weight value.
83
- */
84
- export function parseFontWeight(
85
- value: string | number | undefined,
86
- ): 'normal' | 'bold' | number | undefined {
87
- if (value == null) return undefined
88
- if (typeof value === 'number') return value
89
- if (value === 'normal' || value === 'bold') return value
90
- const num = Number.parseInt(value, 10)
91
- if (!Number.isNaN(num)) return num
92
- return undefined
93
- }
94
-
95
- /**
96
- * Parse a CSS line-height value to a unitless number.
97
- */
98
- export function parseLineHeight(
99
- value: string | number | undefined,
100
- rootSize = DEFAULT_ROOT_SIZE,
101
- ): number | undefined {
102
- if (value == null) return undefined
103
- if (typeof value === 'number') return value
104
- if (value === 'normal') return undefined
105
-
106
- const dim = parseCssDimension(value, rootSize)
107
- if (dim != null) return dim
108
-
109
- return undefined
110
- }
@@ -1,280 +0,0 @@
1
- import { resolveStyles } from './resolveStyles'
2
- import type { DocChild, DocNode, NodeType } from './types'
3
-
4
- /** Marker interface: components with _documentType are extractable. */
5
- export interface DocumentMarker {
6
- _documentType: NodeType
7
- }
8
-
9
- export interface ExtractOptions {
10
- /** Root font size for rem→px conversion. Default: 16. */
11
- rootSize?: number
12
- /** Include resolved styles from $rocketstyle. Default: true. */
13
- includeStyles?: boolean
14
- }
15
-
16
- type VNodeLike = {
17
- type: string | ((...args: any[]) => any)
18
- props: Record<string, any>
19
- children: unknown[]
20
- }
21
-
22
- function isVNode(value: unknown): value is VNodeLike {
23
- return value != null && typeof value === 'object' && 'type' in value && 'props' in value
24
- }
25
-
26
- function getDocumentType(fn: unknown): NodeType | undefined {
27
- if (typeof fn !== 'function') return undefined
28
- const meta = (fn as any).meta
29
- if (meta?._documentType) return meta._documentType as NodeType
30
- // Fallback: check directly on function (non-rocketstyle components)
31
- if ('_documentType' in fn) return (fn as any)._documentType as NodeType
32
- return undefined
33
- }
34
-
35
- function flattenChildren(children: unknown[]): unknown[] {
36
- const result: unknown[] = []
37
- for (const child of children) {
38
- if (Array.isArray(child)) {
39
- result.push(...flattenChildren(child))
40
- } else if (typeof child === 'function') {
41
- // Reactive getter — call to resolve
42
- const resolved = child()
43
- if (Array.isArray(resolved)) {
44
- result.push(...flattenChildren(resolved))
45
- } else {
46
- result.push(resolved)
47
- }
48
- } else {
49
- result.push(child)
50
- }
51
- }
52
- return result
53
- }
54
-
55
- function extractChildren(children: unknown[], options: ExtractOptions): DocChild[] {
56
- const flat = flattenChildren(children)
57
- const result: DocChild[] = []
58
-
59
- for (const child of flat) {
60
- if (child == null || child === false || child === true) continue
61
-
62
- if (typeof child === 'string') {
63
- result.push(child)
64
- continue
65
- }
66
-
67
- if (typeof child === 'number') {
68
- result.push(String(child))
69
- continue
70
- }
71
-
72
- if (isVNode(child)) {
73
- const extracted = extractNode(child, options)
74
- if (Array.isArray(extracted)) {
75
- result.push(...extracted)
76
- } else if (extracted != null) {
77
- result.push(extracted)
78
- }
79
- }
80
- }
81
-
82
- return result
83
- }
84
-
85
- function extractNode(vnode: VNodeLike, options: ExtractOptions): DocNode | DocChild[] | null {
86
- const { type, props, children } = vnode
87
- const includeStyles = options.includeStyles !== false
88
- const rootSize = options.rootSize ?? 16
89
-
90
- // Component function with _documentType marker (via .statics() or direct)
91
- const docType = getDocumentType(type)
92
- if (docType) {
93
- // ── _documentProps resolution ────────────────────────────────────
94
- //
95
- // Three paths to find `_documentProps` on a documentType vnode,
96
- // tried in order:
97
- //
98
- // (A) **Pre-resolved on the JSX vnode itself** — used by test
99
- // fixtures that hand-construct vnodes with `_documentProps`
100
- // baked in. Cheapest path; tried first.
101
- //
102
- // (C) **Hoisted-attrs fast path (T3.1, PR #321)** — when the
103
- // component is a real rocketstyle primitive, it exposes
104
- // `__rs_attrs` (the accumulated `.attrs()` callback chain)
105
- // as a typed static. We run the chain DIRECTLY with the
106
- // JSX vnode's props — `chain.reduce(Object.assign, {})` —
107
- // and read `_documentProps` from the result. No styled
108
- // wrapper invocation, no JSX tree creation, no dimension
109
- // resolution. This is the production path for every real
110
- // Pyreon doc-primitive (DocDocument, DocHeading, etc.).
111
- //
112
- // (B) **Full component invocation (legacy fallback)** — only
113
- // fires when neither A nor C applies. Used by hand-rolled
114
- // test fixtures that mark a function with `_documentType`
115
- // but don't go through rocketstyle (so `__rs_attrs` is
116
- // absent). Calls the component with the JSX props and
117
- // reads `_documentProps` from the post-call vnode.
118
- //
119
- // Why three paths instead of one: (A) is for test fixtures that
120
- // hardcode `_documentProps` directly on the JSX vnode — a pattern
121
- // that pre-dates the attrs HOC. (C) is the real-world path. (B)
122
- // is what (C) replaced — kept so non-rocketstyle fixtures still
123
- // work. See PR #197 for the original metadata-drop bug and
124
- // PR #321 (T3.1) for the architectural fast path.
125
- //
126
- // **Function values in _documentProps are resolved at this
127
- // point** — primitives like DocDocument can store accessor
128
- // thunks (`() => string`) for reactive metadata, and the
129
- // export pipeline reads the LIVE value on each extraction.
130
-
131
- let rawDocProps: Record<string, unknown> | undefined
132
- let extractedFromCall: VNodeLike | null = null
133
-
134
- // Path A: pre-resolved on the JSX vnode (test fixtures)
135
- if (props._documentProps && typeof props._documentProps === 'object') {
136
- rawDocProps = props._documentProps as Record<string, unknown>
137
- } else if (typeof type === 'function') {
138
- // ── Path C (T3.1 fast path) ─────────────────────────────────────
139
- //
140
- // Rocketstyle exposes the accumulated `.attrs()` callback chain
141
- // as `__rs_attrs` on the component function. Run the chain
142
- // directly with the JSX vnode's props to get the post-attrs
143
- // result — no full component invocation, no styling work, no
144
- // wrapped JSX tree creation. Just the user-supplied attrs
145
- // callback(s) folded into a single props object.
146
- //
147
- // This eliminates the per-export cost of Path B for every real
148
- // rocketstyle primitive (DocDocument, DocHeading, etc.). The
149
- // idempotence assumption is now structural rather than implicit:
150
- // we never call the component, so it cannot have side effects
151
- // that affect the second extraction.
152
- const rsAttrs = (type as { __rs_attrs?: Array<(p: Record<string, unknown>) => Record<string, unknown>> }).__rs_attrs
153
- if (rsAttrs && rsAttrs.length > 0) {
154
- const mergedProps = { ...props }
155
- if (children && children.length > 0) {
156
- mergedProps.children = children.length === 1 ? children[0] : children
157
- }
158
- const attrsResult = rsAttrs.reduce<Record<string, unknown>>(
159
- (acc, fn) => Object.assign(acc, fn(mergedProps)),
160
- {},
161
- )
162
- if (attrsResult._documentProps && typeof attrsResult._documentProps === 'object') {
163
- rawDocProps = attrsResult._documentProps as Record<string, unknown>
164
- }
165
- } else {
166
- // Path B (fallback for non-rocketstyle docComponents):
167
- // invoke the component to get the post-attrs vnode. Used by
168
- // hand-rolled test fixtures that don't go through rocketstyle.
169
- const mergedProps = { ...props }
170
- if (children && children.length > 0) {
171
- mergedProps.children = children.length === 1 ? children[0] : children
172
- }
173
- const result = (type as (p: Record<string, unknown>) => unknown)(mergedProps)
174
- if (isVNode(result)) {
175
- extractedFromCall = result
176
- const innerProps = (result as { props?: Record<string, unknown> }).props
177
- if (innerProps?._documentProps && typeof innerProps._documentProps === 'object') {
178
- rawDocProps = innerProps._documentProps as Record<string, unknown>
179
- }
180
- }
181
- }
182
- }
183
-
184
- // Resolve function values (accessors) at extraction time
185
- const docProps: Record<string, unknown> = {}
186
- if (rawDocProps) {
187
- for (const [key, value] of Object.entries(rawDocProps)) {
188
- docProps[key] = typeof value === 'function' ? (value as () => unknown)() : value
189
- }
190
- }
191
-
192
- // Resolve styles from $rocketstyle. Look on the JSX vnode props
193
- // first; if the call result has its own $rocketstyle (because the
194
- // post-attrs vnode carries it down), use that as a fallback.
195
- const stylesSource =
196
- props.$rocketstyle ??
197
- (extractedFromCall as { props?: Record<string, unknown> } | null)?.props?.$rocketstyle
198
- const styles =
199
- includeStyles && stylesSource
200
- ? resolveStyles(stylesSource as Record<string, unknown>, rootSize)
201
- : undefined
202
-
203
- // Children: prefer the JSX vnode's children (the user-supplied
204
- // tree). The post-attrs call might wrap children in additional
205
- // styled elements that aren't part of the document tree.
206
- const docChildren = extractChildren(children ?? [], options)
207
-
208
- const node: DocNode = {
209
- type: docType,
210
- props: docProps,
211
- children: docChildren,
212
- }
213
-
214
- if (styles && Object.keys(styles).length > 0) {
215
- node.styles = styles
216
- }
217
-
218
- return node
219
- }
220
-
221
- // Component function WITHOUT _documentType — call it to get its VNode output
222
- if (typeof type === 'function') {
223
- const mergedProps = { ...props }
224
- if (children && children.length > 0) {
225
- mergedProps.children = children.length === 1 ? children[0] : children
226
- }
227
-
228
- const result = type(mergedProps)
229
-
230
- if (isVNode(result)) {
231
- return extractNode(result, options)
232
- }
233
-
234
- // The component returned a primitive or null
235
- if (typeof result === 'string') return [result]
236
- if (typeof result === 'number') return [String(result)]
237
- return null
238
- }
239
-
240
- // DOM element (string type like 'div', 'span') — transparent, extract children
241
- if (typeof type === 'string') {
242
- const docChildren = extractChildren(children ?? [], options)
243
- // If there's text content in the DOM element, collect it
244
- if (docChildren.length > 0) return docChildren
245
- return null
246
- }
247
-
248
- return null
249
- }
250
-
251
- /**
252
- * Walk a Pyreon VNode tree and extract a `DocNode` tree for `@pyreon/document`.
253
- *
254
- * For each VNode whose component has a `_documentType` marker:
255
- * 1. Read `_documentType` → `DocNode.type`
256
- * 2. Read `_documentProps` → `DocNode.props`
257
- * 3. Read `$rocketstyle` → `resolveStyles()` → `DocNode.styles`
258
- * 4. Recurse into children
259
- *
260
- * VNodes without `_documentType` are transparent — their children
261
- * are flattened into the parent's children list.
262
- */
263
- export function extractDocumentTree(vnode: unknown, options: ExtractOptions = {}): DocNode {
264
- if (isVNode(vnode)) {
265
- const result = extractNode(vnode, options)
266
- if (result && !Array.isArray(result)) return result
267
-
268
- // Wrap loose children in a document node
269
- const children = Array.isArray(result) ? result : []
270
- return { type: 'document', props: {}, children }
271
- }
272
-
273
- // If passed a component function directly, call it
274
- if (typeof vnode === 'function') {
275
- const result = (vnode as () => unknown)()
276
- return extractDocumentTree(result, options)
277
- }
278
-
279
- return { type: 'document', props: {}, children: [] }
280
- }
package/src/index.ts DELETED
@@ -1,10 +0,0 @@
1
- export {
2
- parseBoxModel,
3
- parseCssDimension,
4
- parseFontWeight,
5
- parseLineHeight,
6
- } from './cssValueParser'
7
- export type { DocumentMarker, ExtractOptions } from './extractDocumentTree'
8
- export { extractDocumentTree } from './extractDocumentTree'
9
- export { resolveStyles } from './resolveStyles'
10
- export type { DocChild, DocNode, NodeType, ResolvedStyles } from './types'
@@ -1,101 +0,0 @@
1
- import {
2
- parseBoxModel,
3
- parseCssDimension,
4
- parseFontWeight,
5
- parseLineHeight,
6
- } from './cssValueParser'
7
- import type { ResolvedStyles } from './types'
8
-
9
- const TEXT_ALIGN_VALUES = new Set(['left', 'center', 'right', 'justify'])
10
- const FONT_STYLE_VALUES = new Set(['normal', 'italic'])
11
- const TEXT_DECORATION_VALUES = new Set(['none', 'underline', 'line-through'])
12
- const BORDER_STYLE_VALUES = new Set(['solid', 'dashed', 'dotted'])
13
-
14
- /**
15
- * Convert a rocketstyle `$rocketstyle` theme object into a `ResolvedStyles`
16
- * object compatible with `@pyreon/document`.
17
- *
18
- * Only extracts properties that `ResolvedStyles` supports — everything else
19
- * (transitions, cursor, display, etc.) is silently ignored.
20
- */
21
- export function resolveStyles(rocketstyle: Record<string, unknown>, rootSize = 16): ResolvedStyles {
22
- const styles: ResolvedStyles = {}
23
-
24
- // Typography
25
- const fontSize = parseCssDimension(rocketstyle.fontSize as string | number, rootSize)
26
- if (fontSize != null) styles.fontSize = fontSize
27
-
28
- if (typeof rocketstyle.fontFamily === 'string') styles.fontFamily = rocketstyle.fontFamily
29
-
30
- const fontWeight = parseFontWeight(rocketstyle.fontWeight as string | number | undefined)
31
- if (fontWeight != null) styles.fontWeight = fontWeight
32
-
33
- if (typeof rocketstyle.fontStyle === 'string' && FONT_STYLE_VALUES.has(rocketstyle.fontStyle))
34
- styles.fontStyle = rocketstyle.fontStyle as 'normal' | 'italic'
35
-
36
- if (
37
- typeof rocketstyle.textDecoration === 'string' &&
38
- TEXT_DECORATION_VALUES.has(rocketstyle.textDecoration)
39
- )
40
- styles.textDecoration = rocketstyle.textDecoration as 'none' | 'underline' | 'line-through'
41
-
42
- if (typeof rocketstyle.color === 'string') styles.color = rocketstyle.color
43
-
44
- if (typeof rocketstyle.backgroundColor === 'string')
45
- styles.backgroundColor = rocketstyle.backgroundColor
46
-
47
- if (typeof rocketstyle.textAlign === 'string' && TEXT_ALIGN_VALUES.has(rocketstyle.textAlign))
48
- styles.textAlign = rocketstyle.textAlign as 'left' | 'center' | 'right' | 'justify'
49
-
50
- const lineHeight = parseLineHeight(
51
- rocketstyle.lineHeight as string | number | undefined,
52
- rootSize,
53
- )
54
- if (lineHeight != null) styles.lineHeight = lineHeight
55
-
56
- const letterSpacing = parseCssDimension(rocketstyle.letterSpacing as string | number, rootSize)
57
- if (letterSpacing != null) styles.letterSpacing = letterSpacing
58
-
59
- // Box model
60
- const padding = parseBoxModel(rocketstyle.padding as string | number | undefined, rootSize)
61
- if (padding != null) styles.padding = padding
62
-
63
- const margin = parseBoxModel(rocketstyle.margin as string | number | undefined, rootSize)
64
- if (margin != null) styles.margin = margin
65
-
66
- // Border
67
- const borderRadius = parseCssDimension(rocketstyle.borderRadius as string | number, rootSize)
68
- if (borderRadius != null) styles.borderRadius = borderRadius
69
-
70
- const borderWidth = parseCssDimension(rocketstyle.borderWidth as string | number, rootSize)
71
- if (borderWidth != null) styles.borderWidth = borderWidth
72
-
73
- if (typeof rocketstyle.borderColor === 'string') styles.borderColor = rocketstyle.borderColor
74
-
75
- if (
76
- typeof rocketstyle.borderStyle === 'string' &&
77
- BORDER_STYLE_VALUES.has(rocketstyle.borderStyle)
78
- )
79
- styles.borderStyle = rocketstyle.borderStyle as 'solid' | 'dashed' | 'dotted'
80
-
81
- // Sizing
82
- if (rocketstyle.width != null) {
83
- const w = parseCssDimension(rocketstyle.width as string | number, rootSize)
84
- styles.width = w ?? (rocketstyle.width as string)
85
- }
86
-
87
- if (rocketstyle.height != null) {
88
- const h = parseCssDimension(rocketstyle.height as string | number, rootSize)
89
- styles.height = h ?? (rocketstyle.height as string)
90
- }
91
-
92
- if (rocketstyle.maxWidth != null) {
93
- const mw = parseCssDimension(rocketstyle.maxWidth as string | number, rootSize)
94
- styles.maxWidth = mw ?? (rocketstyle.maxWidth as string)
95
- }
96
-
97
- // Opacity
98
- if (typeof rocketstyle.opacity === 'number') styles.opacity = rocketstyle.opacity
99
-
100
- return styles
101
- }
package/src/types.ts DELETED
@@ -1 +0,0 @@
1
- export type { DocChild, DocNode, NodeType, ResolvedStyles } from '@pyreon/document'