@formepdf/react 0.3.0 → 0.4.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.
package/dist/expr.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /** Comparison and arithmetic expression helpers. */
2
+ export declare const expr: {
3
+ eq: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
4
+ ne: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
5
+ gt: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
6
+ lt: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
7
+ gte: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
8
+ lte: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
9
+ add: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
10
+ sub: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
11
+ mul: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
12
+ div: (a: unknown, b: unknown) => import("./template-proxy.js").ExprMarkerObject;
13
+ upper: (v: unknown) => import("./template-proxy.js").ExprMarkerObject;
14
+ lower: (v: unknown) => import("./template-proxy.js").ExprMarkerObject;
15
+ concat: (...args: unknown[]) => import("./template-proxy.js").ExprMarkerObject;
16
+ format: (v: unknown, fmt: string) => import("./template-proxy.js").ExprMarkerObject;
17
+ cond: (condition: unknown, ifTrue: unknown, ifFalse: unknown) => import("./template-proxy.js").ExprMarkerObject;
18
+ if: (condition: unknown, then: unknown, elseVal?: unknown) => import("./template-proxy.js").ExprMarkerObject;
19
+ count: (v: unknown) => import("./template-proxy.js").ExprMarkerObject;
20
+ };
package/dist/expr.js ADDED
@@ -0,0 +1,39 @@
1
+ /// Expression helpers for template operations that Proxy can't capture.
2
+ ///
3
+ /// These produce expression marker objects that `serializeTemplate()` detects
4
+ /// and converts to the corresponding `$op` nodes in the template JSON.
5
+ import { createExprMarker, toExprValue } from './template-proxy.js';
6
+ /** Comparison and arithmetic expression helpers. */
7
+ export const expr = {
8
+ // Comparison
9
+ eq: (a, b) => createExprMarker({ $eq: [toExprValue(a), toExprValue(b)] }),
10
+ ne: (a, b) => createExprMarker({ $ne: [toExprValue(a), toExprValue(b)] }),
11
+ gt: (a, b) => createExprMarker({ $gt: [toExprValue(a), toExprValue(b)] }),
12
+ lt: (a, b) => createExprMarker({ $lt: [toExprValue(a), toExprValue(b)] }),
13
+ gte: (a, b) => createExprMarker({ $gte: [toExprValue(a), toExprValue(b)] }),
14
+ lte: (a, b) => createExprMarker({ $lte: [toExprValue(a), toExprValue(b)] }),
15
+ // Arithmetic
16
+ add: (a, b) => createExprMarker({ $add: [toExprValue(a), toExprValue(b)] }),
17
+ sub: (a, b) => createExprMarker({ $sub: [toExprValue(a), toExprValue(b)] }),
18
+ mul: (a, b) => createExprMarker({ $mul: [toExprValue(a), toExprValue(b)] }),
19
+ div: (a, b) => createExprMarker({ $div: [toExprValue(a), toExprValue(b)] }),
20
+ // String transforms
21
+ upper: (v) => createExprMarker({ $upper: toExprValue(v) }),
22
+ lower: (v) => createExprMarker({ $lower: toExprValue(v) }),
23
+ concat: (...args) => createExprMarker({ $concat: args.map(toExprValue) }),
24
+ format: (v, fmt) => createExprMarker({ $format: [toExprValue(v), fmt] }),
25
+ // Conditional
26
+ cond: (condition, ifTrue, ifFalse) => createExprMarker({ $cond: [toExprValue(condition), toExprValue(ifTrue), toExprValue(ifFalse)] }),
27
+ if: (condition, then, elseVal) => {
28
+ const obj = {
29
+ $if: toExprValue(condition),
30
+ then: toExprValue(then),
31
+ };
32
+ if (elseVal !== undefined) {
33
+ obj.else = toExprValue(elseVal);
34
+ }
35
+ return createExprMarker(obj);
36
+ },
37
+ // Array
38
+ count: (v) => createExprMarker({ $count: toExprValue(v) }),
39
+ };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { Document, Page, View, Text, Image, Table, Row, Cell, Fixed, Svg, PageBreak } from './components.js';
2
- export { serialize, mapStyle, mapDimension, parseColor, expandEdges, expandCorners } from './serialize.js';
2
+ export { serialize, serializeTemplate, mapStyle, mapDimension, parseColor, expandEdges, expandCorners } from './serialize.js';
3
3
  export { StyleSheet } from './stylesheet.js';
4
4
  export { Font } from './font.js';
5
5
  export type { FontRegistration } from './font.js';
6
+ export { createDataProxy, isRefMarker, isEachMarker, isExprMarker } from './template-proxy.js';
7
+ export { expr } from './expr.js';
6
8
  export { render, renderToObject } from './render.js';
7
9
  export type { Style, Edges, Corners, EdgeColors, DocumentProps, PageProps, ViewProps, TextProps, ImageProps, ColumnDef, TableProps, RowProps, CellProps, FixedProps, SvgProps, TextRun, FormeDocument, FormeFont, FormeNode, FormeNodeKind, FormeStyle, FormePageConfig, FormePageSize, FormeEdges, FormeMetadata, FormeColumnDef, FormeColumnWidth, FormeDimension, FormeColor, FormeEdgeValues, FormeCornerValues, } from './types.js';
package/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  // Components
2
2
  export { Document, Page, View, Text, Image, Table, Row, Cell, Fixed, Svg, PageBreak } from './components.js';
3
3
  // Serialization
4
- export { serialize, mapStyle, mapDimension, parseColor, expandEdges, expandCorners } from './serialize.js';
4
+ export { serialize, serializeTemplate, mapStyle, mapDimension, parseColor, expandEdges, expandCorners } from './serialize.js';
5
5
  // StyleSheet
6
6
  export { StyleSheet } from './stylesheet.js';
7
7
  // Font registration
8
8
  export { Font } from './font.js';
9
+ // Template compilation
10
+ export { createDataProxy, isRefMarker, isEachMarker, isExprMarker } from './template-proxy.js';
11
+ export { expr } from './expr.js';
9
12
  // Render functions
10
13
  export { render, renderToObject } from './render.js';
@@ -10,3 +10,8 @@ export declare function mapDimension(val: number | string): FormeDimension;
10
10
  export declare function parseColor(hex: string): FormeColor;
11
11
  export declare function expandEdges(val: number | Edges): FormeEdges;
12
12
  export declare function expandCorners(val: number | Corners): FormeCornerValues;
13
+ /**
14
+ * Serialize a React element tree into a Forme template JSON document.
15
+ * Like `serialize()` but with expression marker detection for template compilation.
16
+ */
17
+ export declare function serializeTemplate(element: ReactElement): Record<string, unknown>;
package/dist/serialize.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { isValidElement, Children, Fragment } from 'react';
2
2
  import { Document, Page, View, Text, Image, Table, Row, Cell, Fixed, Svg, PageBreak } from './components.js';
3
3
  import { Font } from './font.js';
4
+ import { isRefMarker, getRefPath, isEachMarker, getEachPath, getEachTemplate, isExprMarker, getExpr, REF_SENTINEL, REF_SENTINEL_END, } from './template-proxy.js';
4
5
  const VALID_PARENTS = {
5
6
  Page: {
6
7
  allowed: ['Document'],
@@ -759,3 +760,453 @@ function mergeFonts(globalFonts, docFonts) {
759
760
  }
760
761
  return Array.from(map.values());
761
762
  }
763
+ // ─── Template serialization ─────────────────────────────────────────
764
+ //
765
+ // Parallel to `serialize()` but detects proxy markers and expr markers,
766
+ // converting them to `$ref`, `$each`, `$if`, and operator nodes.
767
+ /**
768
+ * Serialize a React element tree into a Forme template JSON document.
769
+ * Like `serialize()` but with expression marker detection for template compilation.
770
+ */
771
+ export function serializeTemplate(element) {
772
+ if (element.type !== Document) {
773
+ throw new Error('Top-level element must be <Document>');
774
+ }
775
+ const props = element.props;
776
+ const childElements = flattenTemplateChildren(props.children);
777
+ const pageNodes = [];
778
+ const contentNodes = [];
779
+ for (const child of childElements) {
780
+ if (isValidElement(child) && child.type === Page) {
781
+ pageNodes.push(serializeTemplatePage(child));
782
+ }
783
+ else {
784
+ const node = serializeTemplateChild(child, 'Document');
785
+ if (node !== null)
786
+ contentNodes.push(node);
787
+ }
788
+ }
789
+ let children;
790
+ if (pageNodes.length > 0) {
791
+ if (contentNodes.length > 0) {
792
+ const lastPage = pageNodes[pageNodes.length - 1];
793
+ lastPage.children.push(...contentNodes);
794
+ }
795
+ children = pageNodes;
796
+ }
797
+ else if (contentNodes.length > 0) {
798
+ children = contentNodes;
799
+ }
800
+ else {
801
+ children = [];
802
+ }
803
+ const metadata = {};
804
+ if (props.title !== undefined)
805
+ metadata.title = processTemplateValue(props.title);
806
+ if (props.author !== undefined)
807
+ metadata.author = processTemplateValue(props.author);
808
+ if (props.subject !== undefined)
809
+ metadata.subject = processTemplateValue(props.subject);
810
+ if (props.creator !== undefined)
811
+ metadata.creator = processTemplateValue(props.creator);
812
+ const mergedFonts = mergeFonts(Font.getRegistered(), props.fonts);
813
+ const result = {
814
+ children,
815
+ metadata,
816
+ defaultPage: {
817
+ size: 'A4',
818
+ margin: { top: 54, right: 54, bottom: 54, left: 54 },
819
+ wrap: true,
820
+ },
821
+ };
822
+ if (mergedFonts.length > 0) {
823
+ result.fonts = mergedFonts;
824
+ }
825
+ return result;
826
+ }
827
+ function serializeTemplatePage(element) {
828
+ const props = element.props;
829
+ let size = 'A4';
830
+ if (props.size !== undefined) {
831
+ if (typeof props.size === 'string') {
832
+ size = props.size;
833
+ }
834
+ else {
835
+ size = { Custom: { width: props.size.width, height: props.size.height } };
836
+ }
837
+ }
838
+ let margin = { top: 54, right: 54, bottom: 54, left: 54 };
839
+ if (props.margin !== undefined) {
840
+ margin = expandEdges(props.margin);
841
+ }
842
+ const config = { size, margin, wrap: true };
843
+ const childElements = flattenTemplateChildren(props.children);
844
+ const children = serializeTemplateChildren(childElements, 'Page');
845
+ return {
846
+ kind: { type: 'Page', config },
847
+ style: {},
848
+ children,
849
+ };
850
+ }
851
+ function serializeTemplateChild(child, parent = null) {
852
+ if (child === null || child === undefined || typeof child === 'boolean') {
853
+ return null;
854
+ }
855
+ // Check for each marker (from .map() on proxy)
856
+ if (isEachMarker(child)) {
857
+ const path = getEachPath(child);
858
+ const template = getEachTemplate(child);
859
+ // The template is the JSX element returned from the .map() callback
860
+ const serializedTemplate = isValidElement(template)
861
+ ? serializeTemplateChild(template, parent)
862
+ : processTemplateValue(template);
863
+ return {
864
+ $each: { $ref: path },
865
+ as: '$item',
866
+ template: serializedTemplate,
867
+ };
868
+ }
869
+ // Check for expr marker
870
+ if (isExprMarker(child)) {
871
+ return getExpr(child);
872
+ }
873
+ // Check for ref sentinel strings
874
+ if (typeof child === 'string') {
875
+ const processed = processTemplateString(child);
876
+ if (processed !== null)
877
+ return processed;
878
+ return {
879
+ kind: { type: 'Text', content: child },
880
+ style: {},
881
+ children: [],
882
+ };
883
+ }
884
+ if (typeof child === 'number') {
885
+ return {
886
+ kind: { type: 'Text', content: String(child) },
887
+ style: {},
888
+ children: [],
889
+ };
890
+ }
891
+ if (!isValidElement(child))
892
+ return null;
893
+ const element = child;
894
+ if (element.type === View)
895
+ return serializeTemplateView(element, parent);
896
+ if (element.type === Text)
897
+ return serializeTemplateText(element);
898
+ if (element.type === Image)
899
+ return serializeTemplateImage(element);
900
+ if (element.type === Table)
901
+ return serializeTemplateTable(element, parent);
902
+ if (element.type === Row) {
903
+ validateNesting('Row', parent);
904
+ return serializeTemplateRow(element);
905
+ }
906
+ if (element.type === Cell) {
907
+ validateNesting('Cell', parent);
908
+ return serializeTemplateCell(element);
909
+ }
910
+ if (element.type === Fixed)
911
+ return serializeTemplateFixed(element);
912
+ if (element.type === Svg)
913
+ return serializeSvg(element);
914
+ if (element.type === PageBreak) {
915
+ return { kind: { type: 'PageBreak' }, style: {}, children: [] };
916
+ }
917
+ if (element.type === Page) {
918
+ validateNesting('Page', parent);
919
+ return serializeTemplatePage(element);
920
+ }
921
+ // Unknown function component — call it
922
+ if (typeof element.type === 'function') {
923
+ const result = element.type(element.props);
924
+ if (isValidElement(result)) {
925
+ return serializeTemplateChild(result, parent);
926
+ }
927
+ return null;
928
+ }
929
+ return null;
930
+ }
931
+ function serializeTemplateView(element, _parent = null) {
932
+ const props = element.props;
933
+ const style = mapTemplateStyle(props.style);
934
+ if (props.wrap !== undefined)
935
+ style.wrap = props.wrap;
936
+ const childElements = flattenTemplateChildren(props.children);
937
+ const children = serializeTemplateChildren(childElements, 'View');
938
+ const node = { kind: { type: 'View' }, style, children };
939
+ if (props.bookmark)
940
+ node.bookmark = props.bookmark;
941
+ if (props.href)
942
+ node.href = props.href;
943
+ return node;
944
+ }
945
+ function serializeTemplateText(element) {
946
+ const props = element.props;
947
+ const childElements = flattenTemplateChildren(props.children);
948
+ const hasTextChild = childElements.some(c => isValidElement(c) && c.type === Text);
949
+ const kind = { type: 'Text', content: '' };
950
+ if (hasTextChild) {
951
+ const runs = [];
952
+ for (const child of childElements) {
953
+ if (typeof child === 'string' || typeof child === 'number') {
954
+ const processed = typeof child === 'string' ? processTemplateString(child) : null;
955
+ if (processed !== null) {
956
+ runs.push({ content: processed });
957
+ }
958
+ else {
959
+ runs.push({ content: String(child) });
960
+ }
961
+ }
962
+ else if (isValidElement(child) && child.type === Text) {
963
+ const childProps = child.props;
964
+ const run = {
965
+ content: flattenTemplateTextContent(childProps.children),
966
+ };
967
+ if (childProps.style)
968
+ run.style = mapTemplateStyle(childProps.style);
969
+ if (childProps.href)
970
+ run.href = childProps.href;
971
+ runs.push(run);
972
+ }
973
+ }
974
+ kind.runs = runs;
975
+ }
976
+ else {
977
+ kind.content = flattenTemplateTextContent(props.children);
978
+ }
979
+ if (props.href)
980
+ kind.href = props.href;
981
+ const node = {
982
+ kind,
983
+ style: mapTemplateStyle(props.style),
984
+ children: [],
985
+ };
986
+ if (props.bookmark)
987
+ node.bookmark = props.bookmark;
988
+ return node;
989
+ }
990
+ function serializeTemplateImage(element) {
991
+ const props = element.props;
992
+ const kind = { type: 'Image', src: processTemplateValue(props.src) };
993
+ if (props.width !== undefined)
994
+ kind.width = processTemplateValue(props.width);
995
+ if (props.height !== undefined)
996
+ kind.height = processTemplateValue(props.height);
997
+ return { kind, style: mapTemplateStyle(props.style), children: [] };
998
+ }
999
+ function serializeTemplateTable(element, _parent = null) {
1000
+ const props = element.props;
1001
+ const columns = (props.columns ?? []).map(col => ({
1002
+ width: mapColumnWidth(col.width),
1003
+ }));
1004
+ const childElements = flattenTemplateChildren(props.children);
1005
+ const children = serializeTemplateChildren(childElements, 'Table');
1006
+ return { kind: { type: 'Table', columns }, style: mapTemplateStyle(props.style), children };
1007
+ }
1008
+ function serializeTemplateRow(element) {
1009
+ const props = element.props;
1010
+ const childElements = flattenTemplateChildren(props.children);
1011
+ const children = serializeTemplateChildren(childElements, 'Row');
1012
+ return { kind: { type: 'TableRow', is_header: props.header ?? false }, style: mapTemplateStyle(props.style), children };
1013
+ }
1014
+ function serializeTemplateCell(element) {
1015
+ const props = element.props;
1016
+ const childElements = flattenTemplateChildren(props.children);
1017
+ const children = serializeTemplateChildren(childElements, 'Cell');
1018
+ return { kind: { type: 'TableCell', col_span: props.colSpan ?? 1, row_span: props.rowSpan ?? 1 }, style: mapTemplateStyle(props.style), children };
1019
+ }
1020
+ function serializeTemplateFixed(element) {
1021
+ const props = element.props;
1022
+ const position = props.position === 'header' ? 'Header' : 'Footer';
1023
+ const childElements = flattenTemplateChildren(props.children);
1024
+ const children = serializeTemplateChildren(childElements, 'Fixed');
1025
+ const node = { kind: { type: 'Fixed', position }, style: mapTemplateStyle(props.style), children };
1026
+ if (props.bookmark)
1027
+ node.bookmark = props.bookmark;
1028
+ return node;
1029
+ }
1030
+ function serializeTemplateChildren(children, parent = null) {
1031
+ const nodes = [];
1032
+ for (const child of children) {
1033
+ const node = serializeTemplateChild(child, parent);
1034
+ if (node !== null)
1035
+ nodes.push(node);
1036
+ }
1037
+ return nodes;
1038
+ }
1039
+ /**
1040
+ * Flatten children without using React.Children.forEach, which rejects
1041
+ * proxy objects and markers. Handles arrays, Fragments, and raw values.
1042
+ */
1043
+ function flattenTemplateChildren(children) {
1044
+ if (children === null || children === undefined)
1045
+ return [];
1046
+ const result = [];
1047
+ if (Array.isArray(children)) {
1048
+ for (const child of children) {
1049
+ result.push(...flattenTemplateChildren(child));
1050
+ }
1051
+ return result;
1052
+ }
1053
+ // Fragment unwrapping
1054
+ if (isValidElement(children) && children.type === Fragment) {
1055
+ const fragProps = children.props;
1056
+ return flattenTemplateChildren(fragProps.children);
1057
+ }
1058
+ result.push(children);
1059
+ return result;
1060
+ }
1061
+ // ─── Template value processing ──────────────────────────────────────
1062
+ /**
1063
+ * Process a value that may contain ref markers, expr markers, or proxy objects.
1064
+ * Returns the expression form or the original value.
1065
+ */
1066
+ function processTemplateValue(v) {
1067
+ if (typeof v === 'string') {
1068
+ if (isRefMarker(v)) {
1069
+ return { $ref: getRefPath(v) };
1070
+ }
1071
+ // Check for embedded sentinels in longer strings
1072
+ if (v.includes(REF_SENTINEL)) {
1073
+ return processTemplateInterpolatedString(v);
1074
+ }
1075
+ return v;
1076
+ }
1077
+ if (isExprMarker(v)) {
1078
+ return getExpr(v);
1079
+ }
1080
+ if (isEachMarker(v)) {
1081
+ return {
1082
+ $each: { $ref: getEachPath(v) },
1083
+ as: '$item',
1084
+ template: processTemplateValue(getEachTemplate(v)),
1085
+ };
1086
+ }
1087
+ // Proxy objects with toPrimitive
1088
+ if (typeof v === 'object' && v !== null && Symbol.toPrimitive in v) {
1089
+ const str = String(v);
1090
+ if (isRefMarker(str)) {
1091
+ return { $ref: getRefPath(str) };
1092
+ }
1093
+ }
1094
+ return v;
1095
+ }
1096
+ /**
1097
+ * Process a string that contains interpolated ref sentinels.
1098
+ * e.g. "Hello \0FORME_REF:name\0!" → {$concat: ["Hello ", {$ref: "name"}, "!"]}
1099
+ */
1100
+ function processTemplateInterpolatedString(s) {
1101
+ const parts = [];
1102
+ let remaining = s;
1103
+ while (remaining.length > 0) {
1104
+ const startIdx = remaining.indexOf(REF_SENTINEL);
1105
+ if (startIdx === -1) {
1106
+ parts.push(remaining);
1107
+ break;
1108
+ }
1109
+ if (startIdx > 0) {
1110
+ parts.push(remaining.slice(0, startIdx));
1111
+ }
1112
+ const afterSentinel = remaining.slice(startIdx + REF_SENTINEL.length);
1113
+ const endIdx = afterSentinel.indexOf(REF_SENTINEL_END);
1114
+ if (endIdx === -1) {
1115
+ parts.push(remaining);
1116
+ break;
1117
+ }
1118
+ const path = afterSentinel.slice(0, endIdx);
1119
+ parts.push({ $ref: path });
1120
+ remaining = afterSentinel.slice(endIdx + REF_SENTINEL_END.length);
1121
+ }
1122
+ if (parts.length === 1)
1123
+ return parts[0];
1124
+ return { $concat: parts };
1125
+ }
1126
+ /**
1127
+ * Process a string that might be a pure ref sentinel.
1128
+ * Returns the $ref node if it's a pure ref, null otherwise.
1129
+ */
1130
+ function processTemplateString(s) {
1131
+ if (isRefMarker(s)) {
1132
+ return { $ref: getRefPath(s) };
1133
+ }
1134
+ if (s.includes(REF_SENTINEL)) {
1135
+ return processTemplateInterpolatedString(s);
1136
+ }
1137
+ return null;
1138
+ }
1139
+ /**
1140
+ * Flatten text content within a <Text> element, detecting ref markers.
1141
+ * Returns either a plain string or a $ref/$concat expression.
1142
+ */
1143
+ function flattenTemplateTextContent(children) {
1144
+ if (children === null || children === undefined)
1145
+ return '';
1146
+ if (typeof children === 'boolean')
1147
+ return '';
1148
+ if (typeof children === 'string') {
1149
+ if (isRefMarker(children))
1150
+ return { $ref: getRefPath(children) };
1151
+ if (children.includes(REF_SENTINEL))
1152
+ return processTemplateInterpolatedString(children);
1153
+ return children;
1154
+ }
1155
+ if (typeof children === 'number')
1156
+ return String(children);
1157
+ if (isExprMarker(children))
1158
+ return getExpr(children);
1159
+ // Proxy with toPrimitive
1160
+ if (typeof children === 'object' && children !== null && Symbol.toPrimitive in children) {
1161
+ const str = String(children);
1162
+ if (isRefMarker(str))
1163
+ return { $ref: getRefPath(str) };
1164
+ return str;
1165
+ }
1166
+ if (Array.isArray(children)) {
1167
+ const parts = children.map(c => flattenTemplateTextContent(c));
1168
+ // If all parts are strings, join them
1169
+ if (parts.every(p => typeof p === 'string')) {
1170
+ return parts.join('');
1171
+ }
1172
+ // Otherwise produce a $concat
1173
+ return { $concat: parts };
1174
+ }
1175
+ if (isValidElement(children)) {
1176
+ const element = children;
1177
+ if (element.type === Text) {
1178
+ const props = element.props;
1179
+ return flattenTemplateTextContent(props.children);
1180
+ }
1181
+ const props = element.props;
1182
+ return flattenTemplateTextContent(props.children);
1183
+ }
1184
+ const arr = [];
1185
+ Children.forEach(children, c => arr.push(c));
1186
+ if (arr.length > 0) {
1187
+ return flattenTemplateTextContent(arr);
1188
+ }
1189
+ return String(children);
1190
+ }
1191
+ /**
1192
+ * Map style, processing values that may contain template expressions.
1193
+ */
1194
+ function mapTemplateStyle(style) {
1195
+ if (!style)
1196
+ return {};
1197
+ // Use the regular mapStyle but then post-process values that contain markers
1198
+ const result = mapStyle(style);
1199
+ return processTemplateStyleValues(result);
1200
+ }
1201
+ function processTemplateStyleValues(obj) {
1202
+ const result = {};
1203
+ for (const [key, val] of Object.entries(obj)) {
1204
+ if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
1205
+ result[key] = processTemplateStyleValues(val);
1206
+ }
1207
+ else {
1208
+ result[key] = processTemplateValue(val);
1209
+ }
1210
+ }
1211
+ return result;
1212
+ }
@@ -0,0 +1,38 @@
1
+ /** Sentinel prefix/suffix for ref markers embedded in template strings. */
2
+ declare const REF_SENTINEL = "\0FORME_REF:";
3
+ declare const REF_SENTINEL_END = "\0";
4
+ /** Symbol to identify each markers produced by .map() */
5
+ declare const EACH_MARKER: unique symbol;
6
+ /** Symbol to identify expression marker objects */
7
+ declare const EXPR_MARKER: unique symbol;
8
+ /** Create a recording proxy for template data. */
9
+ export declare function createDataProxy(rootPath?: string[]): unknown;
10
+ export declare function isRefMarker(value: unknown): boolean;
11
+ export declare function getRefPath(value: string): string;
12
+ export declare function isEachMarker(value: unknown): value is {
13
+ [EACH_MARKER]: true;
14
+ path: string;
15
+ template: unknown;
16
+ };
17
+ export declare function getEachPath(marker: {
18
+ path: string;
19
+ }): string;
20
+ export declare function getEachTemplate(marker: {
21
+ template: unknown;
22
+ }): unknown;
23
+ export declare function isExprMarker(value: unknown): value is {
24
+ [EXPR_MARKER]: true;
25
+ expr: Record<string, unknown>;
26
+ };
27
+ export declare function getExpr(marker: {
28
+ expr: Record<string, unknown>;
29
+ }): Record<string, unknown>;
30
+ /** Opaque marker type returned by expression helpers. */
31
+ export interface ExprMarkerObject {
32
+ expr: Record<string, unknown>;
33
+ }
34
+ /** Create an expression marker wrapping a template expression object. */
35
+ export declare function createExprMarker(expr: Record<string, unknown>): ExprMarkerObject;
36
+ /** Convert a value that may be a proxy/marker to its expression form. */
37
+ export declare function toExprValue(v: unknown): unknown;
38
+ export { REF_SENTINEL, REF_SENTINEL_END, EACH_MARKER, EXPR_MARKER };
@@ -0,0 +1,100 @@
1
+ /// Recording proxy that traces property access for template compilation.
2
+ ///
3
+ /// When JSX templates use `data.user.name`, the proxy records the access path
4
+ /// and produces `$ref` markers in the serialized output. Array `.map()` calls
5
+ /// produce `$each` markers.
6
+ /** Sentinel prefix/suffix for ref markers embedded in template strings. */
7
+ const REF_SENTINEL = '\0FORME_REF:';
8
+ const REF_SENTINEL_END = '\0';
9
+ /** Symbol to identify each markers produced by .map() */
10
+ const EACH_MARKER = Symbol.for('forme:each');
11
+ /** Symbol to identify expression marker objects */
12
+ const EXPR_MARKER = Symbol.for('forme:expr');
13
+ /** Create a recording proxy for template data. */
14
+ export function createDataProxy(rootPath = []) {
15
+ const handler = {
16
+ get(_target, prop) {
17
+ // String coercion hooks — produce sentinel string for JSX interpolation
18
+ // Must be checked before the generic symbol guard below
19
+ if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') {
20
+ return () => `${REF_SENTINEL}${rootPath.join('.')}${REF_SENTINEL_END}`;
21
+ }
22
+ // .map() on array proxies → produce $each marker
23
+ if (prop === 'map') {
24
+ return (fn) => {
25
+ const itemProxy = createDataProxy(['$item']);
26
+ const indexProxy = createDataProxy(['$index']);
27
+ const template = fn(itemProxy, indexProxy);
28
+ return createEachMarker(rootPath.join('.'), template);
29
+ };
30
+ }
31
+ // Other symbols — not supported
32
+ if (typeof prop === 'symbol') {
33
+ return undefined;
34
+ }
35
+ // Property access → extend path
36
+ return createDataProxy([...rootPath, prop]);
37
+ },
38
+ // Support `Symbol.toPrimitive in proxy` checks
39
+ has(_target, prop) {
40
+ if (prop === Symbol.toPrimitive)
41
+ return true;
42
+ return prop in _target;
43
+ },
44
+ };
45
+ return new Proxy(Object.create(null), handler);
46
+ }
47
+ // ─── Marker detection ────────────────────────────────────────────────
48
+ export function isRefMarker(value) {
49
+ if (typeof value !== 'string')
50
+ return false;
51
+ return value.startsWith(REF_SENTINEL) && value.endsWith(REF_SENTINEL_END);
52
+ }
53
+ export function getRefPath(value) {
54
+ return value.slice(REF_SENTINEL.length, -REF_SENTINEL_END.length);
55
+ }
56
+ export function isEachMarker(value) {
57
+ return (typeof value === 'object' &&
58
+ value !== null &&
59
+ value[EACH_MARKER] === true);
60
+ }
61
+ export function getEachPath(marker) {
62
+ return marker.path;
63
+ }
64
+ export function getEachTemplate(marker) {
65
+ return marker.template;
66
+ }
67
+ export function isExprMarker(value) {
68
+ return (typeof value === 'object' &&
69
+ value !== null &&
70
+ value[EXPR_MARKER] === true);
71
+ }
72
+ export function getExpr(marker) {
73
+ return marker.expr;
74
+ }
75
+ // ─── Internal helpers ────────────────────────────────────────────────
76
+ function createEachMarker(path, template) {
77
+ return Object.defineProperty({ path, template, [EACH_MARKER]: true }, EACH_MARKER, { enumerable: false, value: true });
78
+ }
79
+ /** Create an expression marker wrapping a template expression object. */
80
+ export function createExprMarker(expr) {
81
+ return Object.defineProperty({ expr, [EXPR_MARKER]: true }, EXPR_MARKER, { enumerable: false, value: true });
82
+ }
83
+ /** Convert a value that may be a proxy/marker to its expression form. */
84
+ export function toExprValue(v) {
85
+ if (typeof v === 'string' && isRefMarker(v)) {
86
+ return { $ref: getRefPath(v) };
87
+ }
88
+ if (isExprMarker(v)) {
89
+ return v.expr;
90
+ }
91
+ // Proxy objects will coerce to string via toPrimitive when used in expressions
92
+ if (typeof v === 'object' && v !== null && Symbol.toPrimitive in v) {
93
+ const str = String(v);
94
+ if (isRefMarker(str)) {
95
+ return { $ref: getRefPath(str) };
96
+ }
97
+ }
98
+ return v;
99
+ }
100
+ export { REF_SENTINEL, REF_SENTINEL_END, EACH_MARKER, EXPR_MARKER };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formepdf/react",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "JSX-to-JSON serializer for Forme PDF engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",