@player-tools/dsl 0.0.2-next.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/README.md +9 -0
- package/dist/index.cjs.js +865 -0
- package/dist/index.d.ts +324 -0
- package/dist/index.esm.js +818 -0
- package/package.json +51 -0
- package/src/auto-id.tsx +136 -0
- package/src/compiler/compiler.ts +219 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/schema.ts +250 -0
- package/src/compiler/types.ts +18 -0
- package/src/components.tsx +234 -0
- package/src/index.ts +10 -0
- package/src/string-templates/index.ts +172 -0
- package/src/switch.tsx +136 -0
- package/src/template.tsx +186 -0
- package/src/types.ts +81 -0
- package/src/utils.tsx +109 -0
package/src/template.tsx
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ObjectNode, JsonNode } from 'react-json-reconciler';
|
|
3
|
+
import {
|
|
4
|
+
ArrayNode,
|
|
5
|
+
PropertyNode,
|
|
6
|
+
ValueNode,
|
|
7
|
+
createPortal,
|
|
8
|
+
ProxyNode,
|
|
9
|
+
toJSON,
|
|
10
|
+
} from 'react-json-reconciler';
|
|
11
|
+
import { OptionalIDSuffixProvider } from './auto-id';
|
|
12
|
+
import type { BindingTemplateInstance } from './string-templates';
|
|
13
|
+
import type { WithChildren } from './types';
|
|
14
|
+
|
|
15
|
+
export interface TemplateContextType {
|
|
16
|
+
/** The number of nested templates */
|
|
17
|
+
depth: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const TemplateContext = React.createContext<TemplateContextType>({
|
|
21
|
+
depth: 0,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export interface TemplateProps {
|
|
25
|
+
/** The source binding */
|
|
26
|
+
data: BindingTemplateInstance;
|
|
27
|
+
|
|
28
|
+
/** The target property */
|
|
29
|
+
output?: string;
|
|
30
|
+
|
|
31
|
+
/** The template value */
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Add a template instance to the object */
|
|
36
|
+
function addTemplateToObject(
|
|
37
|
+
obj: ObjectNode,
|
|
38
|
+
templateObj: ObjectNode,
|
|
39
|
+
templateParentNodeType: string
|
|
40
|
+
): () => void {
|
|
41
|
+
// find a template property
|
|
42
|
+
// add one if none exists
|
|
43
|
+
|
|
44
|
+
let templateProp = obj.properties.find(
|
|
45
|
+
(p) => p.keyNode.value === 'template' && p.valueNode?.type === 'array'
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (!templateProp) {
|
|
49
|
+
templateProp = new PropertyNode(new ValueNode('template'), new ArrayNode());
|
|
50
|
+
templateProp.parent = obj;
|
|
51
|
+
obj.properties.push(templateProp);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const templateItems = templateProp.valueNode as ArrayNode;
|
|
55
|
+
|
|
56
|
+
templateItems.items.push(templateObj);
|
|
57
|
+
// eslint-disable-next-line no-param-reassign
|
|
58
|
+
templateObj.parent = templateItems;
|
|
59
|
+
|
|
60
|
+
const templateParentProp = obj.properties.find(
|
|
61
|
+
(p) =>
|
|
62
|
+
p.keyNode.value === templateParentNodeType &&
|
|
63
|
+
p.valueNode?.type === 'array'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (templateParentProp) {
|
|
67
|
+
const indexOfTemplateParent = obj.properties.indexOf(templateParentProp, 1);
|
|
68
|
+
const templateParentValueNode =
|
|
69
|
+
obj.properties[indexOfTemplateParent]?.valueNode;
|
|
70
|
+
if (templateParentValueNode) {
|
|
71
|
+
const templateParentArray = toJSON(templateParentValueNode);
|
|
72
|
+
|
|
73
|
+
// Delete the parent of template if it is an empty array
|
|
74
|
+
if (
|
|
75
|
+
Array.isArray(templateParentArray) &&
|
|
76
|
+
templateParentArray.length === 0
|
|
77
|
+
) {
|
|
78
|
+
obj.properties.splice(indexOfTemplateParent, 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
// Remove the template item from the list
|
|
85
|
+
templateItems.items = templateItems.items.filter((t) => t !== templateObj);
|
|
86
|
+
|
|
87
|
+
// Clean up the whole template if it's removed
|
|
88
|
+
if (templateItems.children.length === 0 && templateProp) {
|
|
89
|
+
obj.properties.splice(obj.properties.indexOf(templateProp, 1));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Context provider wrapper to handle nested templates */
|
|
95
|
+
const TemplateProvider = (props: WithChildren) => {
|
|
96
|
+
const baseContext = React.useContext(TemplateContext);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<TemplateContext.Provider value={{ depth: baseContext.depth + 1 }}>
|
|
100
|
+
{props.children}
|
|
101
|
+
</TemplateContext.Provider>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** Find the first object node in the tree */
|
|
106
|
+
const getParentObject = (node: JsonNode): ObjectNode | undefined => {
|
|
107
|
+
if (node.type === 'object') {
|
|
108
|
+
return node;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (node.parent) {
|
|
112
|
+
return getParentObject(node.parent);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Find the property of the node on the parent */
|
|
117
|
+
const getParentProperty = (node: JsonNode): PropertyNode | undefined => {
|
|
118
|
+
if (node.type === 'property') {
|
|
119
|
+
return node;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (node.parent) {
|
|
123
|
+
return getParentProperty(node.parent);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** A template allows users to dynamically map over an array of data */
|
|
128
|
+
export const Template = (props: TemplateProps) => {
|
|
129
|
+
const baseContext = React.useContext(TemplateContext);
|
|
130
|
+
const [outputProp, setOutputProp] = React.useState<string | undefined>(
|
|
131
|
+
props.output
|
|
132
|
+
);
|
|
133
|
+
const proxyRef = React.useRef<ProxyNode>(null);
|
|
134
|
+
const valueRef = React.useRef<ValueNode>(null);
|
|
135
|
+
const outputElement = React.useMemo(() => new ProxyNode(), []);
|
|
136
|
+
|
|
137
|
+
React.useLayoutEffect(() => {
|
|
138
|
+
// Get the output prop
|
|
139
|
+
const propNode = proxyRef.current && getParentProperty(proxyRef.current);
|
|
140
|
+
|
|
141
|
+
if (outputProp === undefined && propNode) {
|
|
142
|
+
setOutputProp(propNode.keyNode.value);
|
|
143
|
+
}
|
|
144
|
+
}, [proxyRef, outputProp]);
|
|
145
|
+
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
const templateObj = outputElement.items[0] as ObjectNode;
|
|
148
|
+
if (proxyRef.current) {
|
|
149
|
+
const parentObject = getParentObject(proxyRef.current);
|
|
150
|
+
|
|
151
|
+
if (!parentObject) {
|
|
152
|
+
throw new Error('Unable to find parent to add template to');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!outputProp) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// remove the template when unmounted
|
|
160
|
+
return addTemplateToObject(parentObject, templateObj, outputProp);
|
|
161
|
+
}
|
|
162
|
+
}, [proxyRef, outputProp, outputElement.items]);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<proxy ref={proxyRef}>
|
|
166
|
+
{createPortal(
|
|
167
|
+
<OptionalIDSuffixProvider
|
|
168
|
+
wrapperRef={valueRef}
|
|
169
|
+
templateIndex={`_index${
|
|
170
|
+
baseContext.depth === 0 ? '' : baseContext.depth
|
|
171
|
+
}_`}
|
|
172
|
+
>
|
|
173
|
+
<TemplateProvider>
|
|
174
|
+
<object>
|
|
175
|
+
<property name="data">{props.data.toValue()}</property>
|
|
176
|
+
<property name="output">{outputProp}</property>
|
|
177
|
+
<property name="value">{props.children}</property>
|
|
178
|
+
</object>
|
|
179
|
+
</TemplateProvider>
|
|
180
|
+
</OptionalIDSuffixProvider>,
|
|
181
|
+
outputElement
|
|
182
|
+
)}
|
|
183
|
+
<value ref={valueRef} value={undefined} />
|
|
184
|
+
</proxy>
|
|
185
|
+
);
|
|
186
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Asset,
|
|
3
|
+
Expression,
|
|
4
|
+
Navigation as PlayerNav,
|
|
5
|
+
} from '@player-ui/types';
|
|
6
|
+
import type {
|
|
7
|
+
BindingTemplateInstance,
|
|
8
|
+
ExpressionTemplateInstance,
|
|
9
|
+
} from './string-templates';
|
|
10
|
+
|
|
11
|
+
export type WithChildren<T = Record<string, unknown>> = T & {
|
|
12
|
+
/** child nodes */
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type RemoveUnknownIndex<T> = {
|
|
17
|
+
[P in keyof T as T[P] extends unknown
|
|
18
|
+
? unknown extends T[P]
|
|
19
|
+
? never
|
|
20
|
+
: P
|
|
21
|
+
: P]: T[P];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AddUnknownIndex<T> = T & {
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Make an ID prop optional an a type */
|
|
29
|
+
export type OmitProp<T, K extends string> = {
|
|
30
|
+
[P in keyof T as P extends K ? never : P]: T[P];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface PlayerApplicability {
|
|
34
|
+
/** An expression to evaluate to determine if this node should appear in a view or not */
|
|
35
|
+
applicability?:
|
|
36
|
+
| BindingTemplateInstance
|
|
37
|
+
| ExpressionTemplateInstance
|
|
38
|
+
| boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type AssetPropsWithChildren<T extends Asset> = WithChildren<
|
|
42
|
+
WithTemplateTypes<
|
|
43
|
+
OmitProp<RemoveUnknownIndex<T>, 'id' | 'type'> & Partial<Pick<Asset, 'id'>>
|
|
44
|
+
> &
|
|
45
|
+
PlayerApplicability
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
export type SwapKeysToType<T, K extends keyof T, NewType> = {
|
|
49
|
+
[P in keyof T]: P extends K ? NewType : T[P];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type WithTemplateTypes<T> = T extends Record<any, any>
|
|
53
|
+
? {
|
|
54
|
+
[P in keyof T]: WithTemplateTypes<T[P]>;
|
|
55
|
+
}
|
|
56
|
+
: T | BindingTemplateInstance | ExpressionTemplateInstance;
|
|
57
|
+
|
|
58
|
+
type ValidKeys = 'exp' | 'onStart' | 'onEnd';
|
|
59
|
+
|
|
60
|
+
type DeepReplace<T, Old, New> = {
|
|
61
|
+
[P in keyof T]: T[P] extends Old
|
|
62
|
+
? P extends ValidKeys
|
|
63
|
+
? New
|
|
64
|
+
: DeepReplace<T[P], Old, New> // Set to new if one of the valid keys: replace with `? New` for all keys
|
|
65
|
+
: T[P] extends (infer R)[] // Is this a Tuple or array
|
|
66
|
+
? DeepReplace<R, Old, New>[] // Replace the type of the tuple/array
|
|
67
|
+
: T[P] extends object
|
|
68
|
+
? DeepReplace<T[P], Old, New>
|
|
69
|
+
: Extract<T[P], Old> extends Old // Is this a union with the searched for type?
|
|
70
|
+
?
|
|
71
|
+
| DeepReplace<Extract<T[P], object>, Old, New> // Replace all object types of the union
|
|
72
|
+
| Exclude<T[P], Old | object> // Get all types that are not objects (handled above) or Old (handled below
|
|
73
|
+
| New // Direct Replacement of Old
|
|
74
|
+
: T[P];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type Navigation = DeepReplace<
|
|
78
|
+
PlayerNav,
|
|
79
|
+
Expression,
|
|
80
|
+
ExpressionTemplateInstance | ExpressionTemplateInstance[] | Expression
|
|
81
|
+
>;
|
package/src/utils.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
isTemplateStringInstance,
|
|
4
|
+
TemplateStringComponent,
|
|
5
|
+
} from './string-templates';
|
|
6
|
+
|
|
7
|
+
/** Get an array version of the value */
|
|
8
|
+
export function toArray<T>(val: T | Array<T>): Array<T> {
|
|
9
|
+
return Array.isArray(val) ? val : [val];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Create a component version */
|
|
13
|
+
export function toJsonElement(value: any, index?: number): React.ReactElement {
|
|
14
|
+
const keyProp = index === undefined ? null : { key: index };
|
|
15
|
+
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return (
|
|
18
|
+
<array {...keyProp}>
|
|
19
|
+
{value.map((item, idx) => toJsonElement(item, idx))}
|
|
20
|
+
</array>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Allow users to pass in BindingTemplateInstance and ExpressionTemplateInstance directly without turning them into strings first */
|
|
25
|
+
if (isTemplateStringInstance(value)) {
|
|
26
|
+
return <value {...keyProp}>{value.toRefString()}</value>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof value === 'object' && value !== null) {
|
|
30
|
+
return (
|
|
31
|
+
<obj {...keyProp}>
|
|
32
|
+
{Object.keys(value).map((key) => (
|
|
33
|
+
<property key={key} name={key}>
|
|
34
|
+
{toJsonElement(value[key])}
|
|
35
|
+
</property>
|
|
36
|
+
))}
|
|
37
|
+
</obj>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return <value {...keyProp} value={value} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Create a fragment for the properties */
|
|
45
|
+
export function toJsonProperties(value: Record<string, any>) {
|
|
46
|
+
return Object.keys(value).map((key) => (
|
|
47
|
+
<property key={key} name={key}>
|
|
48
|
+
{toJsonElement(value[key])}
|
|
49
|
+
</property>
|
|
50
|
+
));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Create a text asset if needed */
|
|
54
|
+
export function normalizeText(options: {
|
|
55
|
+
/** The current node */
|
|
56
|
+
node: React.ReactNode;
|
|
57
|
+
|
|
58
|
+
/** A component to render a text asset */
|
|
59
|
+
TextComp?: React.ComponentType;
|
|
60
|
+
}): React.ReactNode {
|
|
61
|
+
const { node, TextComp } = options;
|
|
62
|
+
|
|
63
|
+
const nodeArr = React.Children.toArray(node);
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
nodeArr.every(
|
|
67
|
+
(n) => React.isValidElement(n) && n.type !== TemplateStringComponent
|
|
68
|
+
)
|
|
69
|
+
) {
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (TextComp) {
|
|
74
|
+
return <TextComp>{nodeArr}</TextComp>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Tried to convert node to Text Asset, but no Component was supplied.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Create a collection if needed */
|
|
83
|
+
export function normalizeToCollection(options: {
|
|
84
|
+
/** the node to look at */
|
|
85
|
+
node: React.ReactNode;
|
|
86
|
+
|
|
87
|
+
/** A Text asset */
|
|
88
|
+
TextComp?: React.ComponentType;
|
|
89
|
+
|
|
90
|
+
/** A collection asset */
|
|
91
|
+
CollectionComp?: React.ComponentType;
|
|
92
|
+
}) {
|
|
93
|
+
const { node, CollectionComp } = options;
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
React.Children.count(node) > 1 &&
|
|
97
|
+
React.Children.toArray(node).every((n) => typeof n !== 'string')
|
|
98
|
+
) {
|
|
99
|
+
if (!CollectionComp) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Tried to convert array to a collection asset, but no Component was given.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return <CollectionComp>{node}</CollectionComp>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return normalizeText({ ...options, node });
|
|
109
|
+
}
|