@formepdf/react 0.3.0 → 0.4.1
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 +20 -0
- package/dist/expr.js +39 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/serialize.d.ts +5 -0
- package/dist/serialize.js +471 -0
- package/dist/template-proxy.d.ts +38 -0
- package/dist/template-proxy.js +100 -0
- package/package.json +1 -1
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';
|
package/dist/serialize.d.ts
CHANGED
|
@@ -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,473 @@ 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 serializeExprValues(getExpr(child), parent);
|
|
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
|
+
// ─── Expression value serialization ─────────────────────────────────
|
|
1062
|
+
/**
|
|
1063
|
+
* Recursively process an expression object, serializing any React elements
|
|
1064
|
+
* found in its values (e.g. $if then/else branches).
|
|
1065
|
+
*/
|
|
1066
|
+
function serializeExprValues(expr, parent) {
|
|
1067
|
+
const result = {};
|
|
1068
|
+
for (const [key, val] of Object.entries(expr)) {
|
|
1069
|
+
if (isValidElement(val)) {
|
|
1070
|
+
result[key] = serializeTemplateChild(val, parent);
|
|
1071
|
+
}
|
|
1072
|
+
else if (Array.isArray(val)) {
|
|
1073
|
+
result[key] = val.map(v => isValidElement(v) ? serializeTemplateChild(v, parent) : v);
|
|
1074
|
+
}
|
|
1075
|
+
else {
|
|
1076
|
+
result[key] = val;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return result;
|
|
1080
|
+
}
|
|
1081
|
+
// ─── Template value processing ──────────────────────────────────────
|
|
1082
|
+
/**
|
|
1083
|
+
* Process a value that may contain ref markers, expr markers, or proxy objects.
|
|
1084
|
+
* Returns the expression form or the original value.
|
|
1085
|
+
*/
|
|
1086
|
+
function processTemplateValue(v) {
|
|
1087
|
+
if (typeof v === 'string') {
|
|
1088
|
+
if (isRefMarker(v)) {
|
|
1089
|
+
return { $ref: getRefPath(v) };
|
|
1090
|
+
}
|
|
1091
|
+
// Check for embedded sentinels in longer strings
|
|
1092
|
+
if (v.includes(REF_SENTINEL)) {
|
|
1093
|
+
return processTemplateInterpolatedString(v);
|
|
1094
|
+
}
|
|
1095
|
+
return v;
|
|
1096
|
+
}
|
|
1097
|
+
if (isExprMarker(v)) {
|
|
1098
|
+
return getExpr(v);
|
|
1099
|
+
}
|
|
1100
|
+
if (isEachMarker(v)) {
|
|
1101
|
+
return {
|
|
1102
|
+
$each: { $ref: getEachPath(v) },
|
|
1103
|
+
as: '$item',
|
|
1104
|
+
template: processTemplateValue(getEachTemplate(v)),
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
// Proxy objects with toPrimitive
|
|
1108
|
+
if (typeof v === 'object' && v !== null && Symbol.toPrimitive in v) {
|
|
1109
|
+
const str = String(v);
|
|
1110
|
+
if (isRefMarker(str)) {
|
|
1111
|
+
return { $ref: getRefPath(str) };
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return v;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Process a string that contains interpolated ref sentinels.
|
|
1118
|
+
* e.g. "Hello \0FORME_REF:name\0!" → {$concat: ["Hello ", {$ref: "name"}, "!"]}
|
|
1119
|
+
*/
|
|
1120
|
+
function processTemplateInterpolatedString(s) {
|
|
1121
|
+
const parts = [];
|
|
1122
|
+
let remaining = s;
|
|
1123
|
+
while (remaining.length > 0) {
|
|
1124
|
+
const startIdx = remaining.indexOf(REF_SENTINEL);
|
|
1125
|
+
if (startIdx === -1) {
|
|
1126
|
+
parts.push(remaining);
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
if (startIdx > 0) {
|
|
1130
|
+
parts.push(remaining.slice(0, startIdx));
|
|
1131
|
+
}
|
|
1132
|
+
const afterSentinel = remaining.slice(startIdx + REF_SENTINEL.length);
|
|
1133
|
+
const endIdx = afterSentinel.indexOf(REF_SENTINEL_END);
|
|
1134
|
+
if (endIdx === -1) {
|
|
1135
|
+
parts.push(remaining);
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
const path = afterSentinel.slice(0, endIdx);
|
|
1139
|
+
parts.push({ $ref: path });
|
|
1140
|
+
remaining = afterSentinel.slice(endIdx + REF_SENTINEL_END.length);
|
|
1141
|
+
}
|
|
1142
|
+
if (parts.length === 1)
|
|
1143
|
+
return parts[0];
|
|
1144
|
+
return { $concat: parts };
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Process a string that might be a pure ref sentinel.
|
|
1148
|
+
* Returns the $ref node if it's a pure ref, null otherwise.
|
|
1149
|
+
*/
|
|
1150
|
+
function processTemplateString(s) {
|
|
1151
|
+
if (isRefMarker(s)) {
|
|
1152
|
+
return { $ref: getRefPath(s) };
|
|
1153
|
+
}
|
|
1154
|
+
if (s.includes(REF_SENTINEL)) {
|
|
1155
|
+
return processTemplateInterpolatedString(s);
|
|
1156
|
+
}
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Flatten text content within a <Text> element, detecting ref markers.
|
|
1161
|
+
* Returns either a plain string or a $ref/$concat expression.
|
|
1162
|
+
*/
|
|
1163
|
+
function flattenTemplateTextContent(children) {
|
|
1164
|
+
if (children === null || children === undefined)
|
|
1165
|
+
return '';
|
|
1166
|
+
if (typeof children === 'boolean')
|
|
1167
|
+
return '';
|
|
1168
|
+
if (typeof children === 'string') {
|
|
1169
|
+
if (isRefMarker(children))
|
|
1170
|
+
return { $ref: getRefPath(children) };
|
|
1171
|
+
if (children.includes(REF_SENTINEL))
|
|
1172
|
+
return processTemplateInterpolatedString(children);
|
|
1173
|
+
return children;
|
|
1174
|
+
}
|
|
1175
|
+
if (typeof children === 'number')
|
|
1176
|
+
return String(children);
|
|
1177
|
+
if (isExprMarker(children))
|
|
1178
|
+
return getExpr(children);
|
|
1179
|
+
// Proxy with toPrimitive
|
|
1180
|
+
if (typeof children === 'object' && children !== null && Symbol.toPrimitive in children) {
|
|
1181
|
+
const str = String(children);
|
|
1182
|
+
if (isRefMarker(str))
|
|
1183
|
+
return { $ref: getRefPath(str) };
|
|
1184
|
+
return str;
|
|
1185
|
+
}
|
|
1186
|
+
if (Array.isArray(children)) {
|
|
1187
|
+
const parts = children.map(c => flattenTemplateTextContent(c));
|
|
1188
|
+
// If all parts are strings, join them
|
|
1189
|
+
if (parts.every(p => typeof p === 'string')) {
|
|
1190
|
+
return parts.join('');
|
|
1191
|
+
}
|
|
1192
|
+
// Otherwise produce a $concat
|
|
1193
|
+
return { $concat: parts };
|
|
1194
|
+
}
|
|
1195
|
+
if (isValidElement(children)) {
|
|
1196
|
+
const element = children;
|
|
1197
|
+
if (element.type === Text) {
|
|
1198
|
+
const props = element.props;
|
|
1199
|
+
return flattenTemplateTextContent(props.children);
|
|
1200
|
+
}
|
|
1201
|
+
const props = element.props;
|
|
1202
|
+
return flattenTemplateTextContent(props.children);
|
|
1203
|
+
}
|
|
1204
|
+
const arr = [];
|
|
1205
|
+
Children.forEach(children, c => arr.push(c));
|
|
1206
|
+
if (arr.length > 0) {
|
|
1207
|
+
return flattenTemplateTextContent(arr);
|
|
1208
|
+
}
|
|
1209
|
+
return String(children);
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Map style, processing values that may contain template expressions.
|
|
1213
|
+
*/
|
|
1214
|
+
function mapTemplateStyle(style) {
|
|
1215
|
+
if (!style)
|
|
1216
|
+
return {};
|
|
1217
|
+
// Use the regular mapStyle but then post-process values that contain markers
|
|
1218
|
+
const result = mapStyle(style);
|
|
1219
|
+
return processTemplateStyleValues(result);
|
|
1220
|
+
}
|
|
1221
|
+
function processTemplateStyleValues(obj) {
|
|
1222
|
+
const result = {};
|
|
1223
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
1224
|
+
if (typeof val === 'object' && val !== null && !Array.isArray(val)) {
|
|
1225
|
+
result[key] = processTemplateStyleValues(val);
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
result[key] = processTemplateValue(val);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return result;
|
|
1232
|
+
}
|
|
@@ -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 };
|