@formepdf/react 0.1.0

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.
@@ -0,0 +1,715 @@
1
+ import { isValidElement, Children, Fragment } from 'react';
2
+ import { Document, Page, View, Text, Image, Table, Row, Cell, Fixed, Svg, PageBreak } from './components.js';
3
+ const VALID_PARENTS = {
4
+ Page: {
5
+ allowed: ['Document'],
6
+ suggestion: '<Page> must be a direct child of <Document>.',
7
+ },
8
+ Row: {
9
+ allowed: ['Table'],
10
+ suggestion: '<Row> must be inside a <Table>. Wrap it: <Table><Row>...</Row></Table>',
11
+ },
12
+ Cell: {
13
+ allowed: ['Row'],
14
+ suggestion: '<Cell> must be inside a <Row>. Wrap it: <Row><Cell>...</Cell></Row>',
15
+ },
16
+ };
17
+ function validateNesting(componentName, parent) {
18
+ const rule = VALID_PARENTS[componentName];
19
+ if (!rule)
20
+ return;
21
+ if (parent !== null && !rule.allowed.includes(parent)) {
22
+ throw new Error(`Invalid nesting: <${componentName}> found inside <${parent}>. ${rule.suggestion}`);
23
+ }
24
+ }
25
+ // ─── Source location extraction ─────────────────────────────────────
26
+ function extractSourceLocation(element) {
27
+ // Check globalThis.__formeSourceMap (populated by CLI dev server's JSX shim for React 19+)
28
+ const map = globalThis.__formeSourceMap;
29
+ if (map) {
30
+ const source = map.get(element);
31
+ if (source)
32
+ return source;
33
+ }
34
+ // Fallback to _source for React 18 and earlier
35
+ const s = element._source;
36
+ if (s && s.fileName) {
37
+ return { file: s.fileName, line: s.lineNumber, column: s.columnNumber };
38
+ }
39
+ return undefined;
40
+ }
41
+ // ─── Public API ──────────────────────────────────────────────────────
42
+ /**
43
+ * Serialize a React element tree into a Forme JSON document object.
44
+ * The top-level element must be a <Document>.
45
+ */
46
+ export function serialize(element) {
47
+ if (element.type !== Document) {
48
+ throw new Error('Top-level element must be <Document>');
49
+ }
50
+ const props = element.props;
51
+ const childElements = flattenChildren(props.children);
52
+ // Separate Page children from content children
53
+ const pageNodes = [];
54
+ const contentNodes = [];
55
+ for (const child of childElements) {
56
+ if (isValidElement(child) && child.type === Page) {
57
+ pageNodes.push(serializePage(child));
58
+ }
59
+ else {
60
+ const node = serializeChild(child, 'Document');
61
+ if (node)
62
+ contentNodes.push(node);
63
+ }
64
+ }
65
+ // If there are page nodes, use them. Otherwise wrap content in a default page.
66
+ let children;
67
+ if (pageNodes.length > 0) {
68
+ // Any loose content nodes get added to the last page's children
69
+ if (contentNodes.length > 0) {
70
+ const lastPage = pageNodes[pageNodes.length - 1];
71
+ lastPage.children.push(...contentNodes);
72
+ }
73
+ children = pageNodes;
74
+ }
75
+ else if (contentNodes.length > 0) {
76
+ children = contentNodes;
77
+ }
78
+ else {
79
+ children = [];
80
+ }
81
+ const metadata = {};
82
+ if (props.title !== undefined)
83
+ metadata.title = props.title;
84
+ if (props.author !== undefined)
85
+ metadata.author = props.author;
86
+ if (props.subject !== undefined)
87
+ metadata.subject = props.subject;
88
+ if (props.creator !== undefined)
89
+ metadata.creator = props.creator;
90
+ return {
91
+ children,
92
+ metadata,
93
+ defaultPage: {
94
+ size: 'A4',
95
+ margin: { top: 54, right: 54, bottom: 54, left: 54 },
96
+ wrap: true,
97
+ },
98
+ };
99
+ }
100
+ // ─── Page serialization ──────────────────────────────────────────────
101
+ function serializePage(element) {
102
+ const props = element.props;
103
+ let size = 'A4';
104
+ if (props.size !== undefined) {
105
+ if (typeof props.size === 'string') {
106
+ size = props.size;
107
+ }
108
+ else {
109
+ size = { Custom: { width: props.size.width, height: props.size.height } };
110
+ }
111
+ }
112
+ let margin = { top: 54, right: 54, bottom: 54, left: 54 };
113
+ if (props.margin !== undefined) {
114
+ margin = expandEdges(props.margin);
115
+ }
116
+ const config = { size, margin, wrap: true };
117
+ const childElements = flattenChildren(props.children);
118
+ const children = serializeChildren(childElements, 'Page');
119
+ return {
120
+ kind: { type: 'Page', config },
121
+ style: {},
122
+ children,
123
+ sourceLocation: extractSourceLocation(element),
124
+ };
125
+ }
126
+ // ─── Node serialization ─────────────────────────────────────────────
127
+ function serializeChild(child, parent = null) {
128
+ if (child === null || child === undefined || typeof child === 'boolean') {
129
+ return null;
130
+ }
131
+ if (typeof child === 'string') {
132
+ return {
133
+ kind: { type: 'Text', content: child },
134
+ style: {},
135
+ children: [],
136
+ };
137
+ }
138
+ if (typeof child === 'number') {
139
+ return {
140
+ kind: { type: 'Text', content: String(child) },
141
+ style: {},
142
+ children: [],
143
+ };
144
+ }
145
+ if (!isValidElement(child)) {
146
+ // Detect HTML elements and give helpful suggestion
147
+ if (typeof child === 'object' && child !== null && 'type' in child) {
148
+ const t = child.type;
149
+ if (typeof t === 'string') {
150
+ const suggestions = {
151
+ div: 'View', span: 'Text', p: 'Text', h1: 'Text', h2: 'Text',
152
+ h3: 'Text', img: 'Image', table: 'Table', tr: 'Row', td: 'Cell',
153
+ };
154
+ const suggestion = suggestions[t];
155
+ if (suggestion) {
156
+ throw new Error(`HTML element <${t}> is not supported. Use <${suggestion}> instead.`);
157
+ }
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+ const element = child;
163
+ if (element.type === View) {
164
+ return serializeView(element, parent);
165
+ }
166
+ if (element.type === Text) {
167
+ return serializeText(element);
168
+ }
169
+ if (element.type === Image) {
170
+ return serializeImage(element);
171
+ }
172
+ if (element.type === Table) {
173
+ return serializeTable(element, parent);
174
+ }
175
+ if (element.type === Row) {
176
+ validateNesting('Row', parent);
177
+ return serializeRow(element);
178
+ }
179
+ if (element.type === Cell) {
180
+ validateNesting('Cell', parent);
181
+ return serializeCell(element);
182
+ }
183
+ if (element.type === Fixed) {
184
+ return serializeFixed(element);
185
+ }
186
+ if (element.type === Svg) {
187
+ return serializeSvg(element);
188
+ }
189
+ if (element.type === PageBreak) {
190
+ return {
191
+ kind: { type: 'PageBreak' },
192
+ style: {},
193
+ children: [],
194
+ sourceLocation: extractSourceLocation(element),
195
+ };
196
+ }
197
+ if (element.type === Page) {
198
+ validateNesting('Page', parent);
199
+ return serializePage(element);
200
+ }
201
+ if (element.type === Document) {
202
+ // Nested Document — just serialize its children
203
+ const props = element.props;
204
+ const childElements = flattenChildren(props.children);
205
+ const nodes = serializeChildren(childElements, parent);
206
+ return nodes.length === 1 ? nodes[0] : {
207
+ kind: { type: 'View' },
208
+ style: {},
209
+ children: nodes,
210
+ };
211
+ }
212
+ // Unknown component — try to call it as a function component
213
+ if (typeof element.type === 'function') {
214
+ const result = element.type(element.props);
215
+ if (isValidElement(result)) {
216
+ return serializeChild(result, parent);
217
+ }
218
+ return null;
219
+ }
220
+ return null;
221
+ }
222
+ function serializeView(element, _parent = null) {
223
+ const props = element.props;
224
+ const style = mapStyle(props.style);
225
+ if (props.wrap !== undefined) {
226
+ style.wrap = props.wrap;
227
+ }
228
+ const childElements = flattenChildren(props.children);
229
+ const children = serializeChildren(childElements, 'View');
230
+ const node = {
231
+ kind: { type: 'View' },
232
+ style,
233
+ children,
234
+ sourceLocation: extractSourceLocation(element),
235
+ };
236
+ if (props.bookmark)
237
+ node.bookmark = props.bookmark;
238
+ if (props.href)
239
+ node.href = props.href;
240
+ return node;
241
+ }
242
+ function serializeText(element) {
243
+ const props = element.props;
244
+ const childElements = flattenChildren(props.children);
245
+ // Check if any child is a <Text> element (inline runs)
246
+ const hasTextChild = childElements.some(c => isValidElement(c) && c.type === Text);
247
+ const kind = { type: 'Text', content: '' };
248
+ if (hasTextChild) {
249
+ // Build runs from children
250
+ const runs = [];
251
+ for (const child of childElements) {
252
+ if (typeof child === 'string' || typeof child === 'number') {
253
+ runs.push({ content: String(child) });
254
+ }
255
+ else if (isValidElement(child) && child.type === Text) {
256
+ const childProps = child.props;
257
+ const run = {
258
+ content: flattenTextContent(childProps.children),
259
+ };
260
+ if (childProps.style)
261
+ run.style = mapStyle(childProps.style);
262
+ if (childProps.href)
263
+ run.href = childProps.href;
264
+ runs.push(run);
265
+ }
266
+ }
267
+ kind.runs = runs;
268
+ }
269
+ else {
270
+ kind.content = flattenTextContent(props.children);
271
+ }
272
+ if (props.href)
273
+ kind.href = props.href;
274
+ const node = {
275
+ kind,
276
+ style: mapStyle(props.style),
277
+ children: [],
278
+ sourceLocation: extractSourceLocation(element),
279
+ };
280
+ if (props.bookmark)
281
+ node.bookmark = props.bookmark;
282
+ return node;
283
+ }
284
+ function serializeImage(element) {
285
+ const props = element.props;
286
+ const kind = { type: 'Image', src: props.src };
287
+ if (props.width !== undefined)
288
+ kind.width = props.width;
289
+ if (props.height !== undefined)
290
+ kind.height = props.height;
291
+ return {
292
+ kind,
293
+ style: mapStyle(props.style),
294
+ children: [],
295
+ sourceLocation: extractSourceLocation(element),
296
+ };
297
+ }
298
+ function serializeTable(element, _parent = null) {
299
+ const props = element.props;
300
+ const columns = (props.columns ?? []).map(col => ({
301
+ width: mapColumnWidth(col.width),
302
+ }));
303
+ const childElements = flattenChildren(props.children);
304
+ const children = serializeChildren(childElements, 'Table');
305
+ return {
306
+ kind: { type: 'Table', columns },
307
+ style: mapStyle(props.style),
308
+ children,
309
+ sourceLocation: extractSourceLocation(element),
310
+ };
311
+ }
312
+ function serializeRow(element) {
313
+ const props = element.props;
314
+ const childElements = flattenChildren(props.children);
315
+ const children = serializeChildren(childElements, 'Row');
316
+ return {
317
+ kind: { type: 'TableRow', is_header: props.header ?? false },
318
+ style: mapStyle(props.style),
319
+ children,
320
+ sourceLocation: extractSourceLocation(element),
321
+ };
322
+ }
323
+ function serializeCell(element) {
324
+ const props = element.props;
325
+ const childElements = flattenChildren(props.children);
326
+ const children = serializeChildren(childElements, 'Cell');
327
+ return {
328
+ kind: { type: 'TableCell', col_span: props.colSpan ?? 1, row_span: props.rowSpan ?? 1 },
329
+ style: mapStyle(props.style),
330
+ children,
331
+ sourceLocation: extractSourceLocation(element),
332
+ };
333
+ }
334
+ function serializeFixed(element) {
335
+ const props = element.props;
336
+ const position = props.position === 'header' ? 'Header' : 'Footer';
337
+ const childElements = flattenChildren(props.children);
338
+ const children = serializeChildren(childElements, 'Fixed');
339
+ const node = {
340
+ kind: { type: 'Fixed', position },
341
+ style: mapStyle(props.style),
342
+ children,
343
+ sourceLocation: extractSourceLocation(element),
344
+ };
345
+ if (props.bookmark)
346
+ node.bookmark = props.bookmark;
347
+ return node;
348
+ }
349
+ function serializeSvg(element) {
350
+ const props = element.props;
351
+ const kind = {
352
+ type: 'Svg',
353
+ width: props.width,
354
+ height: props.height,
355
+ content: props.content,
356
+ };
357
+ if (props.viewBox)
358
+ kind.view_box = props.viewBox;
359
+ return {
360
+ kind,
361
+ style: mapStyle(props.style),
362
+ children: [],
363
+ sourceLocation: extractSourceLocation(element),
364
+ };
365
+ }
366
+ // ─── Children helpers ────────────────────────────────────────────────
367
+ function flattenChildren(children) {
368
+ const result = [];
369
+ Children.forEach(children, child => {
370
+ if (Array.isArray(child)) {
371
+ result.push(...child.flatMap(c => flattenChildren(c)));
372
+ }
373
+ else if (isValidElement(child) && child.type === Fragment) {
374
+ const fragProps = child.props;
375
+ result.push(...flattenChildren(fragProps.children));
376
+ }
377
+ else {
378
+ result.push(child);
379
+ }
380
+ });
381
+ return result;
382
+ }
383
+ function serializeChildren(children, parent = null) {
384
+ const nodes = [];
385
+ for (const child of children) {
386
+ const node = serializeChild(child, parent);
387
+ if (node)
388
+ nodes.push(node);
389
+ }
390
+ return nodes;
391
+ }
392
+ /**
393
+ * Flatten all text content within a <Text> element to a single string.
394
+ * Nested <Text> children have their content extracted and concatenated.
395
+ */
396
+ function flattenTextContent(children) {
397
+ if (children === null || children === undefined)
398
+ return '';
399
+ if (typeof children === 'string')
400
+ return children;
401
+ if (typeof children === 'number')
402
+ return String(children);
403
+ if (typeof children === 'boolean')
404
+ return '';
405
+ if (Array.isArray(children)) {
406
+ return children.map(c => flattenTextContent(c)).join('');
407
+ }
408
+ if (isValidElement(children)) {
409
+ const element = children;
410
+ if (element.type === Text) {
411
+ const props = element.props;
412
+ return flattenTextContent(props.children);
413
+ }
414
+ // For other elements inside Text, try to extract text content
415
+ const props = element.props;
416
+ return flattenTextContent(props.children);
417
+ }
418
+ // React.Children.toArray for iterables
419
+ const arr = [];
420
+ Children.forEach(children, c => arr.push(c));
421
+ if (arr.length > 0) {
422
+ return arr.map(c => flattenTextContent(c)).join('');
423
+ }
424
+ return String(children);
425
+ }
426
+ // ─── Style mapping ──────────────────────────────────────────────────
427
+ const FLEX_DIRECTION_MAP = {
428
+ 'row': 'Row',
429
+ 'column': 'Column',
430
+ 'row-reverse': 'RowReverse',
431
+ 'column-reverse': 'ColumnReverse',
432
+ };
433
+ const JUSTIFY_CONTENT_MAP = {
434
+ 'flex-start': 'FlexStart',
435
+ 'flex-end': 'FlexEnd',
436
+ 'center': 'Center',
437
+ 'space-between': 'SpaceBetween',
438
+ 'space-around': 'SpaceAround',
439
+ 'space-evenly': 'SpaceEvenly',
440
+ };
441
+ const ALIGN_ITEMS_MAP = {
442
+ 'flex-start': 'FlexStart',
443
+ 'flex-end': 'FlexEnd',
444
+ 'center': 'Center',
445
+ 'stretch': 'Stretch',
446
+ 'baseline': 'Baseline',
447
+ };
448
+ const FLEX_WRAP_MAP = {
449
+ 'nowrap': 'NoWrap',
450
+ 'wrap': 'Wrap',
451
+ 'wrap-reverse': 'WrapReverse',
452
+ };
453
+ const ALIGN_CONTENT_MAP = {
454
+ 'flex-start': 'FlexStart',
455
+ 'flex-end': 'FlexEnd',
456
+ 'center': 'Center',
457
+ 'space-between': 'SpaceBetween',
458
+ 'space-around': 'SpaceAround',
459
+ 'space-evenly': 'SpaceEvenly',
460
+ 'stretch': 'Stretch',
461
+ };
462
+ const FONT_STYLE_MAP = {
463
+ 'normal': 'Normal',
464
+ 'italic': 'Italic',
465
+ 'oblique': 'Oblique',
466
+ };
467
+ const TEXT_ALIGN_MAP = {
468
+ 'left': 'Left',
469
+ 'right': 'Right',
470
+ 'center': 'Center',
471
+ 'justify': 'Justify',
472
+ };
473
+ const TEXT_DECORATION_MAP = {
474
+ 'none': 'None',
475
+ 'underline': 'Underline',
476
+ 'line-through': 'LineThrough',
477
+ };
478
+ const TEXT_TRANSFORM_MAP = {
479
+ 'none': 'None',
480
+ 'uppercase': 'Uppercase',
481
+ 'lowercase': 'Lowercase',
482
+ 'capitalize': 'Capitalize',
483
+ };
484
+ export function mapStyle(style) {
485
+ if (!style)
486
+ return {};
487
+ const result = {};
488
+ // Dimensions
489
+ if (style.width !== undefined)
490
+ result.width = mapDimension(style.width);
491
+ if (style.height !== undefined)
492
+ result.height = mapDimension(style.height);
493
+ if (style.minWidth !== undefined)
494
+ result.minWidth = mapDimension(style.minWidth);
495
+ if (style.minHeight !== undefined)
496
+ result.minHeight = mapDimension(style.minHeight);
497
+ if (style.maxWidth !== undefined)
498
+ result.maxWidth = mapDimension(style.maxWidth);
499
+ if (style.maxHeight !== undefined)
500
+ result.maxHeight = mapDimension(style.maxHeight);
501
+ // Edges (individual > axis > base)
502
+ if (style.padding !== undefined || style.paddingTop !== undefined || style.paddingRight !== undefined || style.paddingBottom !== undefined || style.paddingLeft !== undefined || style.paddingHorizontal !== undefined || style.paddingVertical !== undefined) {
503
+ const base = style.padding !== undefined ? expandEdges(style.padding) : { top: 0, right: 0, bottom: 0, left: 0 };
504
+ const vt = style.paddingVertical ?? base.top;
505
+ const vb = style.paddingVertical ?? base.bottom;
506
+ const hl = style.paddingHorizontal ?? base.left;
507
+ const hr = style.paddingHorizontal ?? base.right;
508
+ result.padding = {
509
+ top: style.paddingTop ?? vt,
510
+ right: style.paddingRight ?? hr,
511
+ bottom: style.paddingBottom ?? vb,
512
+ left: style.paddingLeft ?? hl,
513
+ };
514
+ }
515
+ if (style.margin !== undefined || style.marginTop !== undefined || style.marginRight !== undefined || style.marginBottom !== undefined || style.marginLeft !== undefined || style.marginHorizontal !== undefined || style.marginVertical !== undefined) {
516
+ const base = style.margin !== undefined ? expandEdges(style.margin) : { top: 0, right: 0, bottom: 0, left: 0 };
517
+ const vt = style.marginVertical ?? base.top;
518
+ const vb = style.marginVertical ?? base.bottom;
519
+ const hl = style.marginHorizontal ?? base.left;
520
+ const hr = style.marginHorizontal ?? base.right;
521
+ result.margin = {
522
+ top: style.marginTop ?? vt,
523
+ right: style.marginRight ?? hr,
524
+ bottom: style.marginBottom ?? vb,
525
+ left: style.marginLeft ?? hl,
526
+ };
527
+ }
528
+ // Flex
529
+ if (style.flexDirection !== undefined)
530
+ result.flexDirection = FLEX_DIRECTION_MAP[style.flexDirection];
531
+ if (style.justifyContent !== undefined)
532
+ result.justifyContent = JUSTIFY_CONTENT_MAP[style.justifyContent];
533
+ if (style.alignItems !== undefined)
534
+ result.alignItems = ALIGN_ITEMS_MAP[style.alignItems];
535
+ if (style.alignSelf !== undefined)
536
+ result.alignSelf = ALIGN_ITEMS_MAP[style.alignSelf];
537
+ if (style.flexWrap !== undefined)
538
+ result.flexWrap = FLEX_WRAP_MAP[style.flexWrap];
539
+ if (style.alignContent !== undefined)
540
+ result.alignContent = ALIGN_CONTENT_MAP[style.alignContent];
541
+ if (style.flexGrow !== undefined)
542
+ result.flexGrow = style.flexGrow;
543
+ if (style.flexShrink !== undefined)
544
+ result.flexShrink = style.flexShrink;
545
+ if (style.flexBasis !== undefined)
546
+ result.flexBasis = mapDimension(style.flexBasis);
547
+ if (style.gap !== undefined)
548
+ result.gap = style.gap;
549
+ if (style.rowGap !== undefined)
550
+ result.rowGap = style.rowGap;
551
+ if (style.columnGap !== undefined)
552
+ result.columnGap = style.columnGap;
553
+ // Typography
554
+ if (style.fontFamily !== undefined)
555
+ result.fontFamily = style.fontFamily;
556
+ if (style.fontSize !== undefined)
557
+ result.fontSize = style.fontSize;
558
+ if (style.fontWeight !== undefined) {
559
+ result.fontWeight = style.fontWeight === 'bold' ? 700 : style.fontWeight === 'normal' ? 400 : style.fontWeight;
560
+ }
561
+ if (style.fontStyle !== undefined)
562
+ result.fontStyle = FONT_STYLE_MAP[style.fontStyle];
563
+ if (style.lineHeight !== undefined)
564
+ result.lineHeight = style.lineHeight;
565
+ if (style.textAlign !== undefined)
566
+ result.textAlign = TEXT_ALIGN_MAP[style.textAlign];
567
+ if (style.letterSpacing !== undefined)
568
+ result.letterSpacing = style.letterSpacing;
569
+ if (style.textDecoration !== undefined)
570
+ result.textDecoration = TEXT_DECORATION_MAP[style.textDecoration];
571
+ if (style.textTransform !== undefined)
572
+ result.textTransform = TEXT_TRANSFORM_MAP[style.textTransform];
573
+ // Color
574
+ if (style.color !== undefined)
575
+ result.color = parseColor(style.color);
576
+ if (style.backgroundColor !== undefined)
577
+ result.backgroundColor = parseColor(style.backgroundColor);
578
+ if (style.opacity !== undefined)
579
+ result.opacity = style.opacity;
580
+ // Border
581
+ if (style.borderWidth !== undefined || style.borderTopWidth !== undefined || style.borderRightWidth !== undefined || style.borderBottomWidth !== undefined || style.borderLeftWidth !== undefined) {
582
+ const base = style.borderWidth !== undefined ? expandEdgeValues(style.borderWidth) : { top: 0, right: 0, bottom: 0, left: 0 };
583
+ result.borderWidth = {
584
+ top: style.borderTopWidth ?? base.top,
585
+ right: style.borderRightWidth ?? base.right,
586
+ bottom: style.borderBottomWidth ?? base.bottom,
587
+ left: style.borderLeftWidth ?? base.left,
588
+ };
589
+ }
590
+ if (style.borderColor !== undefined || style.borderTopColor !== undefined || style.borderRightColor !== undefined || style.borderBottomColor !== undefined || style.borderLeftColor !== undefined) {
591
+ let base = {};
592
+ if (typeof style.borderColor === 'string') {
593
+ const c = parseColor(style.borderColor);
594
+ base = { top: c, right: c, bottom: c, left: c };
595
+ }
596
+ else if (style.borderColor) {
597
+ base = {
598
+ top: parseColor(style.borderColor.top),
599
+ right: parseColor(style.borderColor.right),
600
+ bottom: parseColor(style.borderColor.bottom),
601
+ left: parseColor(style.borderColor.left),
602
+ };
603
+ }
604
+ result.borderColor = {
605
+ top: (style.borderTopColor ? parseColor(style.borderTopColor) : base.top),
606
+ right: (style.borderRightColor ? parseColor(style.borderRightColor) : base.right),
607
+ bottom: (style.borderBottomColor ? parseColor(style.borderBottomColor) : base.bottom),
608
+ left: (style.borderLeftColor ? parseColor(style.borderLeftColor) : base.left),
609
+ };
610
+ }
611
+ if (style.borderRadius !== undefined || style.borderTopLeftRadius !== undefined || style.borderTopRightRadius !== undefined || style.borderBottomRightRadius !== undefined || style.borderBottomLeftRadius !== undefined) {
612
+ const base = style.borderRadius !== undefined ? expandCorners(style.borderRadius) : { top_left: 0, top_right: 0, bottom_right: 0, bottom_left: 0 };
613
+ result.borderRadius = {
614
+ top_left: style.borderTopLeftRadius ?? base.top_left,
615
+ top_right: style.borderTopRightRadius ?? base.top_right,
616
+ bottom_right: style.borderBottomRightRadius ?? base.bottom_right,
617
+ bottom_left: style.borderBottomLeftRadius ?? base.bottom_left,
618
+ };
619
+ }
620
+ // Positioning
621
+ if (style.position !== undefined) {
622
+ result.position = style.position === 'absolute' ? 'Absolute' : 'Relative';
623
+ }
624
+ if (style.top !== undefined)
625
+ result.top = style.top;
626
+ if (style.right !== undefined)
627
+ result.right = style.right;
628
+ if (style.bottom !== undefined)
629
+ result.bottom = style.bottom;
630
+ if (style.left !== undefined)
631
+ result.left = style.left;
632
+ // Page behavior
633
+ if (style.wrap !== undefined)
634
+ result.wrap = style.wrap;
635
+ if (style.breakBefore !== undefined)
636
+ result.breakBefore = style.breakBefore;
637
+ if (style.minWidowLines !== undefined)
638
+ result.minWidowLines = style.minWidowLines;
639
+ if (style.minOrphanLines !== undefined)
640
+ result.minOrphanLines = style.minOrphanLines;
641
+ return result;
642
+ }
643
+ export function mapDimension(val) {
644
+ if (typeof val === 'number') {
645
+ return { Pt: val };
646
+ }
647
+ if (val === 'auto')
648
+ return 'Auto';
649
+ const match = val.match(/^([0-9.]+)%$/);
650
+ if (match) {
651
+ return { Percent: parseFloat(match[1]) };
652
+ }
653
+ // Try to parse as a number (e.g. "100" without units)
654
+ const num = parseFloat(val);
655
+ if (!isNaN(num)) {
656
+ return { Pt: num };
657
+ }
658
+ return 'Auto';
659
+ }
660
+ export function parseColor(hex) {
661
+ const h = hex.replace(/^#/, '');
662
+ if (h.length === 3) {
663
+ const r = parseInt(h[0] + h[0], 16) / 255;
664
+ const g = parseInt(h[1] + h[1], 16) / 255;
665
+ const b = parseInt(h[2] + h[2], 16) / 255;
666
+ return { r, g, b, a: 1 };
667
+ }
668
+ if (h.length === 6) {
669
+ const r = parseInt(h.slice(0, 2), 16) / 255;
670
+ const g = parseInt(h.slice(2, 4), 16) / 255;
671
+ const b = parseInt(h.slice(4, 6), 16) / 255;
672
+ return { r, g, b, a: 1 };
673
+ }
674
+ if (h.length === 8) {
675
+ const r = parseInt(h.slice(0, 2), 16) / 255;
676
+ const g = parseInt(h.slice(2, 4), 16) / 255;
677
+ const b = parseInt(h.slice(4, 6), 16) / 255;
678
+ const a = parseInt(h.slice(6, 8), 16) / 255;
679
+ return { r, g, b, a };
680
+ }
681
+ // Fallback: black
682
+ return { r: 0, g: 0, b: 0, a: 1 };
683
+ }
684
+ export function expandEdges(val) {
685
+ if (typeof val === 'number') {
686
+ return { top: val, right: val, bottom: val, left: val };
687
+ }
688
+ return { top: val.top, right: val.right, bottom: val.bottom, left: val.left };
689
+ }
690
+ function expandEdgeValues(val) {
691
+ if (typeof val === 'number') {
692
+ return { top: val, right: val, bottom: val, left: val };
693
+ }
694
+ return { top: val.top, right: val.right, bottom: val.bottom, left: val.left };
695
+ }
696
+ export function expandCorners(val) {
697
+ if (typeof val === 'number') {
698
+ return { top_left: val, top_right: val, bottom_right: val, bottom_left: val };
699
+ }
700
+ return {
701
+ top_left: val.topLeft,
702
+ top_right: val.topRight,
703
+ bottom_right: val.bottomRight,
704
+ bottom_left: val.bottomLeft,
705
+ };
706
+ }
707
+ function mapColumnWidth(w) {
708
+ if (w === 'auto')
709
+ return 'Auto';
710
+ if ('fraction' in w)
711
+ return { Fraction: w.fraction };
712
+ if ('fixed' in w)
713
+ return { Fixed: w.fixed };
714
+ return 'Auto';
715
+ }