@formepdf/react 0.4.4 → 0.5.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.
@@ -8,7 +8,7 @@ export declare function serialize(element: ReactElement): FormeDocument;
8
8
  export declare function mapStyle(style?: Style): FormeStyle;
9
9
  export declare function mapDimension(val: number | string): FormeDimension;
10
10
  export declare function parseColor(hex: string): FormeColor;
11
- export declare function expandEdges(val: number | Edges): FormeEdges;
11
+ export declare function expandEdges(val: number | string | number[] | Edges): FormeEdges;
12
12
  export declare function expandCorners(val: number | Corners): FormeCornerValues;
13
13
  /**
14
14
  * Serialize a React element tree into a Forme template JSON document.
package/dist/serialize.js CHANGED
@@ -89,6 +89,8 @@ export function serialize(element) {
89
89
  metadata.subject = props.subject;
90
90
  if (props.creator !== undefined)
91
91
  metadata.creator = props.creator;
92
+ if (props.lang !== undefined)
93
+ metadata.lang = props.lang;
92
94
  // Merge global + document fonts (document fonts override on conflict)
93
95
  const mergedFonts = mergeFonts(Font.getRegistered(), props.fonts);
94
96
  const result = {
@@ -296,12 +298,17 @@ function serializeImage(element) {
296
298
  kind.width = props.width;
297
299
  if (props.height !== undefined)
298
300
  kind.height = props.height;
299
- return {
301
+ const node = {
300
302
  kind,
301
303
  style: mapStyle(props.style),
302
304
  children: [],
303
305
  sourceLocation: extractSourceLocation(element),
304
306
  };
307
+ if (props.href)
308
+ node.href = props.href;
309
+ if (props.alt)
310
+ node.alt = props.alt;
311
+ return node;
305
312
  }
306
313
  function serializeTable(element, _parent = null) {
307
314
  const props = element.props;
@@ -364,12 +371,17 @@ function serializeSvg(element) {
364
371
  };
365
372
  if (props.viewBox)
366
373
  kind.view_box = props.viewBox;
367
- return {
374
+ const node = {
368
375
  kind,
369
376
  style: mapStyle(props.style),
370
377
  children: [],
371
378
  sourceLocation: extractSourceLocation(element),
372
379
  };
380
+ if (props.href)
381
+ node.href = props.href;
382
+ if (props.alt)
383
+ node.alt = props.alt;
384
+ return node;
373
385
  }
374
386
  // ─── Children helpers ────────────────────────────────────────────────
375
387
  function flattenChildren(children) {
@@ -594,9 +606,40 @@ export function mapStyle(style) {
594
606
  result.backgroundColor = parseColor(style.backgroundColor);
595
607
  if (style.opacity !== undefined)
596
608
  result.opacity = style.opacity;
597
- // Border
598
- if (style.borderWidth !== undefined || style.borderTopWidth !== undefined || style.borderRightWidth !== undefined || style.borderBottomWidth !== undefined || style.borderLeftWidth !== undefined) {
599
- const base = style.borderWidth !== undefined ? expandEdgeValues(style.borderWidth) : { top: 0, right: 0, bottom: 0, left: 0 };
609
+ // Border — cascade: border < borderTop/Right/Bottom/Left < borderWidth/borderColor < borderTopWidth/borderTopColor
610
+ // Step 1: Parse string shorthands into intermediate per-side values
611
+ let shortWidth = { top: undefined, right: undefined, bottom: undefined, left: undefined };
612
+ let shortColor = { top: undefined, right: undefined, bottom: undefined, left: undefined };
613
+ if (style.border !== undefined) {
614
+ const parsed = parseBorderString(style.border);
615
+ if (parsed.width !== undefined)
616
+ shortWidth = { top: parsed.width, right: parsed.width, bottom: parsed.width, left: parsed.width };
617
+ if (parsed.color !== undefined)
618
+ shortColor = { top: parsed.color, right: parsed.color, bottom: parsed.color, left: parsed.color };
619
+ }
620
+ // Per-side string shorthands override all-side shorthand
621
+ for (const [side, prop] of [['top', 'borderTop'], ['right', 'borderRight'], ['bottom', 'borderBottom'], ['left', 'borderLeft']]) {
622
+ const val = style[prop];
623
+ if (val === undefined)
624
+ continue;
625
+ if (typeof val === 'number') {
626
+ shortWidth[side] = val;
627
+ }
628
+ else {
629
+ const parsed = parseBorderString(val);
630
+ if (parsed.width !== undefined)
631
+ shortWidth[side] = parsed.width;
632
+ if (parsed.color !== undefined)
633
+ shortColor[side] = parsed.color;
634
+ }
635
+ }
636
+ // Step 2: Build borderWidth — existing borderWidth/borderTopWidth override shorthands
637
+ const hasBorderWidth = style.borderWidth !== undefined || style.borderTopWidth !== undefined || style.borderRightWidth !== undefined || style.borderBottomWidth !== undefined || style.borderLeftWidth !== undefined;
638
+ const hasShortWidth = shortWidth.top !== undefined || shortWidth.right !== undefined || shortWidth.bottom !== undefined || shortWidth.left !== undefined;
639
+ if (hasBorderWidth || hasShortWidth) {
640
+ const base = style.borderWidth !== undefined
641
+ ? expandEdgeValues(style.borderWidth)
642
+ : { top: shortWidth.top ?? 0, right: shortWidth.right ?? 0, bottom: shortWidth.bottom ?? 0, left: shortWidth.left ?? 0 };
600
643
  result.borderWidth = {
601
644
  top: style.borderTopWidth ?? base.top,
602
645
  right: style.borderRightWidth ?? base.right,
@@ -604,14 +647,22 @@ export function mapStyle(style) {
604
647
  left: style.borderLeftWidth ?? base.left,
605
648
  };
606
649
  }
607
- if (style.borderColor !== undefined || style.borderTopColor !== undefined || style.borderRightColor !== undefined || style.borderBottomColor !== undefined || style.borderLeftColor !== undefined) {
650
+ // Step 3: Build borderColor existing borderColor/borderTopColor override shorthands
651
+ const hasBorderColor = style.borderColor !== undefined || style.borderTopColor !== undefined || style.borderRightColor !== undefined || style.borderBottomColor !== undefined || style.borderLeftColor !== undefined;
652
+ const hasShortColor = shortColor.top !== undefined || shortColor.right !== undefined || shortColor.bottom !== undefined || shortColor.left !== undefined;
653
+ if (hasBorderColor || hasShortColor) {
608
654
  const defaultColor = parseColor('#000000');
609
- let base = { top: defaultColor, right: defaultColor, bottom: defaultColor, left: defaultColor };
655
+ let base = {
656
+ top: shortColor.top ?? defaultColor,
657
+ right: shortColor.right ?? defaultColor,
658
+ bottom: shortColor.bottom ?? defaultColor,
659
+ left: shortColor.left ?? defaultColor,
660
+ };
610
661
  if (typeof style.borderColor === 'string') {
611
662
  const c = parseColor(style.borderColor);
612
663
  base = { top: c, right: c, bottom: c, left: c };
613
664
  }
614
- else if (style.borderColor) {
665
+ else if (style.borderColor && typeof style.borderColor === 'object') {
615
666
  base = {
616
667
  top: parseColor(style.borderColor.top),
617
668
  right: parseColor(style.borderColor.right),
@@ -699,10 +750,54 @@ export function parseColor(hex) {
699
750
  // Fallback: black
700
751
  return { r: 0, g: 0, b: 0, a: 1 };
701
752
  }
753
+ /**
754
+ * Parse a CSS-style 1-4 value edge shorthand.
755
+ * Accepts: `"8"`, `"8 16"`, `"8 16 24"`, `"8 16 24 32"` (with optional `px` suffix).
756
+ * Also accepts number arrays: `[8]`, `[8, 16]`, `[8, 16, 24]`, `[8, 16, 24, 32]`.
757
+ */
758
+ function parseCSSEdges(val) {
759
+ const values = Array.isArray(val)
760
+ ? val
761
+ : val.trim().split(/\s+/).map(s => parseFloat(s.replace(/px$/i, '')));
762
+ switch (values.length) {
763
+ case 1: return { top: values[0], right: values[0], bottom: values[0], left: values[0] };
764
+ case 2: return { top: values[0], right: values[1], bottom: values[0], left: values[1] };
765
+ case 3: return { top: values[0], right: values[1], bottom: values[2], left: values[1] };
766
+ default: return { top: values[0], right: values[1], bottom: values[2], left: values[3] };
767
+ }
768
+ }
769
+ const BORDER_STYLE_KEYWORDS = new Set([
770
+ 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'inset', 'outset', 'none', 'hidden',
771
+ ]);
772
+ /**
773
+ * Parse a CSS border shorthand string like `"1px solid #000"`.
774
+ * Returns extracted width and/or color. Style keywords are recognized but ignored.
775
+ */
776
+ function parseBorderString(val) {
777
+ const tokens = val.trim().split(/\s+/);
778
+ let width;
779
+ let color;
780
+ for (const token of tokens) {
781
+ const lower = token.toLowerCase();
782
+ if (BORDER_STYLE_KEYWORDS.has(lower))
783
+ continue;
784
+ const num = parseFloat(lower.replace(/px$/i, ''));
785
+ if (!isNaN(num) && /^[\d.]/.test(lower)) {
786
+ width = num;
787
+ }
788
+ else {
789
+ color = parseColor(token);
790
+ }
791
+ }
792
+ return { width, color };
793
+ }
702
794
  export function expandEdges(val) {
703
795
  if (typeof val === 'number') {
704
796
  return { top: val, right: val, bottom: val, left: val };
705
797
  }
798
+ if (typeof val === 'string' || Array.isArray(val)) {
799
+ return parseCSSEdges(val);
800
+ }
706
801
  return { top: val.top, right: val.right, bottom: val.bottom, left: val.left };
707
802
  }
708
803
  function expandEdgeValues(val) {
@@ -809,6 +904,8 @@ export function serializeTemplate(element) {
809
904
  metadata.subject = processTemplateValue(props.subject);
810
905
  if (props.creator !== undefined)
811
906
  metadata.creator = processTemplateValue(props.creator);
907
+ if (props.lang !== undefined)
908
+ metadata.lang = processTemplateValue(props.lang);
812
909
  const mergedFonts = mergeFonts(Font.getRegistered(), props.fonts);
813
910
  const result = {
814
911
  children,
package/dist/types.d.ts CHANGED
@@ -42,14 +42,14 @@ export interface Style {
42
42
  gap?: number;
43
43
  rowGap?: number;
44
44
  columnGap?: number;
45
- padding?: number | Edges;
45
+ padding?: number | string | number[] | Edges;
46
46
  paddingTop?: number;
47
47
  paddingRight?: number;
48
48
  paddingBottom?: number;
49
49
  paddingLeft?: number;
50
50
  paddingHorizontal?: number;
51
51
  paddingVertical?: number;
52
- margin?: number | Edges;
52
+ margin?: number | string | number[] | Edges;
53
53
  marginTop?: number;
54
54
  marginRight?: number;
55
55
  marginBottom?: number;
@@ -83,6 +83,16 @@ export interface Style {
83
83
  borderTopRightRadius?: number;
84
84
  borderBottomRightRadius?: number;
85
85
  borderBottomLeftRadius?: number;
86
+ /** CSS border shorthand, e.g. `"1px solid #000"` */
87
+ border?: string;
88
+ /** Per-side border shorthand: string parses as CSS, number sets width */
89
+ borderTop?: string | number;
90
+ /** Per-side border shorthand: string parses as CSS, number sets width */
91
+ borderRight?: string | number;
92
+ /** Per-side border shorthand: string parses as CSS, number sets width */
93
+ borderBottom?: string | number;
94
+ /** Per-side border shorthand: string parses as CSS, number sets width */
95
+ borderLeft?: string | number;
86
96
  position?: 'relative' | 'absolute';
87
97
  top?: number;
88
98
  right?: number;
@@ -98,6 +108,8 @@ export interface DocumentProps {
98
108
  author?: string;
99
109
  subject?: string;
100
110
  creator?: string;
111
+ /** Document language (BCP 47 tag, e.g. "en-US"). Emitted as /Lang in the PDF Catalog. */
112
+ lang?: string;
101
113
  fonts?: FontRegistration[];
102
114
  children?: ReactNode;
103
115
  }
@@ -106,7 +118,7 @@ export interface PageProps {
106
118
  width: number;
107
119
  height: number;
108
120
  };
109
- margin?: number | Edges;
121
+ margin?: number | string | number[] | Edges;
110
122
  children?: ReactNode;
111
123
  }
112
124
  export interface ViewProps {
@@ -127,6 +139,10 @@ export interface ImageProps {
127
139
  width?: number;
128
140
  height?: number;
129
141
  style?: Style;
142
+ /** Optional hyperlink URL — makes the image clickable. */
143
+ href?: string;
144
+ /** Alt text for accessibility. */
145
+ alt?: string;
130
146
  }
131
147
  export interface ColumnDef {
132
148
  width: {
@@ -163,6 +179,10 @@ export interface SvgProps {
163
179
  viewBox?: string;
164
180
  content: string;
165
181
  style?: Style;
182
+ /** Optional hyperlink URL — makes the SVG clickable. */
183
+ href?: string;
184
+ /** Alt text for accessibility. */
185
+ alt?: string;
166
186
  }
167
187
  /** A styled text segment within a <Text> element */
168
188
  export interface TextRun {
@@ -187,6 +207,7 @@ export interface FormeMetadata {
187
207
  author?: string;
188
208
  subject?: string;
189
209
  creator?: string;
210
+ lang?: string;
190
211
  }
191
212
  export interface FormePageConfig {
192
213
  size: FormePageSize;
@@ -211,6 +232,7 @@ export interface FormeNode {
211
232
  children: FormeNode[];
212
233
  bookmark?: string;
213
234
  href?: string;
235
+ alt?: string;
214
236
  sourceLocation?: {
215
237
  file: string;
216
238
  line: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formepdf/react",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "JSX-to-JSON serializer for Forme PDF engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",