@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.
@@ -0,0 +1,234 @@
1
+ import React from 'react';
2
+ import flattenChildren from 'react-flatten-children';
3
+ import type { ObjectNode, PropertyNode } from 'react-json-reconciler';
4
+ import mergeRefs from 'react-merge-refs';
5
+ import type { PlayerApplicability, WithChildren } from './types';
6
+ import {
7
+ IDProvider,
8
+ IDSuffixProvider,
9
+ IndexSuffixStopContext,
10
+ OptionalIDSuffixProvider,
11
+ useGetIdPrefix,
12
+ } from './auto-id';
13
+ import {
14
+ normalizeText,
15
+ normalizeToCollection,
16
+ toJsonProperties,
17
+ } from './utils';
18
+
19
+ export const SlotContext = React.createContext<
20
+ | {
21
+ /** The property name for the slot */
22
+ propertyName: string;
23
+ /** If the slot represents an array */
24
+ isArray: boolean;
25
+ /** If the items in the slot should be wrapped in an "asset" object */
26
+ wrapInAsset: boolean;
27
+ /** Other props to add to the slot */
28
+ additionalProperties?: any;
29
+ /** The ref to the property node */
30
+ ref: React.RefObject<PropertyNode>;
31
+ /** A text component if we hit a string but expect an asset */
32
+ TextComp?: React.ComponentType;
33
+ /** A component to create a collection asset is we get an array but need a single element */
34
+ CollectionComp?: React.ComponentType;
35
+ }
36
+ | undefined
37
+ >(undefined);
38
+
39
+ /**
40
+ * Wraps the children in an `asset` object.
41
+ * Additional props are added to the top level object
42
+ */
43
+ export const AssetWrapper = React.forwardRef<
44
+ ObjectNode,
45
+ WithChildren<{ [key: string]: any }>
46
+ >((props, ref) => {
47
+ const { children, ...rest } = props;
48
+
49
+ return (
50
+ <obj ref={ref}>
51
+ {toJsonProperties(rest)}
52
+ <property name="asset">{children}</property>
53
+ </obj>
54
+ );
55
+ });
56
+
57
+ /** Create a ID property for a node */
58
+ export const GeneratedIDProperty = (props: {
59
+ /** the id to use if supplied by the user */
60
+ id?: string;
61
+ }) => {
62
+ const currentPrefixId = useGetIdPrefix();
63
+ return <property name="id">{props.id ?? currentPrefixId}</property>;
64
+ };
65
+
66
+ /** An asset */
67
+ export const Asset = React.forwardRef<
68
+ ObjectNode,
69
+ {
70
+ /** id of the asset */
71
+ id?: string;
72
+
73
+ /** the asset type */
74
+ type: string;
75
+
76
+ /** Any other properties on the asset */
77
+ children?: React.ReactNode;
78
+
79
+ /** other things that we don't know about */
80
+ [key: string]: unknown;
81
+ } & PlayerApplicability
82
+ >((props, ref) => {
83
+ const { id, type, applicability, children, ...rest } = props;
84
+ const slotContext = React.useContext(SlotContext);
85
+ const localRef = React.useRef<ObjectNode>(null);
86
+ const Wrapper = slotContext?.wrapInAsset ? AssetWrapper : React.Fragment;
87
+
88
+ return (
89
+ <Wrapper
90
+ ref={slotContext?.wrapInAsset ? mergeRefs([ref, localRef]) : undefined}
91
+ {...(slotContext?.wrapInAsset && slotContext?.additionalProperties
92
+ ? slotContext?.additionalProperties
93
+ : {})}
94
+ >
95
+ <OptionalIDSuffixProvider wrapperRef={localRef}>
96
+ <SlotContext.Provider value={undefined}>
97
+ <IDProvider id={id}>
98
+ <obj
99
+ ref={
100
+ slotContext?.wrapInAsset
101
+ ? undefined
102
+ : mergeRefs([ref, localRef])
103
+ }
104
+ >
105
+ <GeneratedIDProperty id={id} />
106
+ <property name="type">{type}</property>
107
+ {applicability !== undefined && (
108
+ <property name="applicability">
109
+ <value
110
+ value={
111
+ typeof applicability === 'boolean'
112
+ ? applicability
113
+ : applicability.toRefString()
114
+ }
115
+ />
116
+ </property>
117
+ )}
118
+ {toJsonProperties(rest)}
119
+ {children}
120
+ </obj>
121
+ </IDProvider>
122
+ </SlotContext.Provider>
123
+ </OptionalIDSuffixProvider>
124
+ </Wrapper>
125
+ );
126
+ });
127
+
128
+ Asset.defaultProps = {
129
+ id: undefined,
130
+ children: undefined,
131
+ };
132
+
133
+ /** A component to generate a named property slot */
134
+ export const Slot = (props: {
135
+ /** The name of the slot */
136
+ name: string;
137
+
138
+ /** if the slot is an array or single object */
139
+ isArray?: boolean;
140
+
141
+ /** if each item should be wrapped in an asset */
142
+ wrapInAsset?: boolean;
143
+
144
+ /** Any children to render in the slot */
145
+ children?: React.ReactNode;
146
+
147
+ /** Other properties to add to the slot */
148
+ additionalProperties?: any;
149
+
150
+ /** A text component if we hit a string but expect an asset */
151
+ TextComp?: React.ComponentType;
152
+
153
+ /** A component to create a collection asset is we get an array but need a single element */
154
+ CollectionComp?: React.ComponentType;
155
+ }) => {
156
+ const { TextComp, CollectionComp } = props;
157
+ const children = flattenChildren(props.children);
158
+ const propRef = React.useRef<PropertyNode>(null);
159
+
160
+ return (
161
+ <property ref={propRef} name={props.name}>
162
+ <IDSuffixProvider suffix={props.name}>
163
+ <IndexSuffixStopContext.Provider value={false}>
164
+ <SlotContext.Provider
165
+ value={{
166
+ ref: propRef,
167
+ propertyName: props.name,
168
+ wrapInAsset: props.wrapInAsset ?? false,
169
+ isArray: props.isArray ?? false,
170
+ additionalProperties: props.additionalProperties,
171
+ TextComp,
172
+ CollectionComp,
173
+ }}
174
+ >
175
+ {props.isArray && (
176
+ <array>
177
+ {React.Children.map(children, (child, index) => {
178
+ return (
179
+ // eslint-disable-next-line react/no-array-index-key
180
+ <React.Fragment key={`${props.name}-${index}`}>
181
+ {normalizeText({ node: child, TextComp })}
182
+ </React.Fragment>
183
+ );
184
+ })}
185
+ </array>
186
+ )}
187
+
188
+ {!props.isArray &&
189
+ normalizeToCollection({
190
+ node: children,
191
+ TextComp,
192
+ CollectionComp,
193
+ })}
194
+ </SlotContext.Provider>
195
+ </IndexSuffixStopContext.Provider>
196
+ </IDSuffixProvider>
197
+ </property>
198
+ );
199
+ };
200
+
201
+ /** Create a slot for a given property */
202
+ export function createSlot<SlotProps = unknown>(options: {
203
+ /** The name of the slot */
204
+ name: string;
205
+
206
+ /** if the slot is an array or single object */
207
+ isArray?: boolean;
208
+
209
+ /** if each item should be wrapped in an asset */
210
+ wrapInAsset?: boolean;
211
+
212
+ /** Any children to render in the slot */
213
+ children?: React.ReactNode;
214
+
215
+ /** A text component if we hit a string but expect an asset */
216
+ TextComp?: React.ComponentType;
217
+
218
+ /** A component to create a collection asset is we get an array but need a single element */
219
+ CollectionComp?: React.ComponentType;
220
+ }) {
221
+ return (
222
+ props: {
223
+ /** An object to include in this property */
224
+ children?: React.ReactNode;
225
+ } & SlotProps
226
+ ) => {
227
+ const { children, ...other } = props;
228
+ return (
229
+ <Slot {...options} additionalProperties={other}>
230
+ {children}
231
+ </Slot>
232
+ );
233
+ };
234
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from './components';
2
+ export * from './auto-id';
3
+ export * from './types';
4
+ export * from './string-templates';
5
+ export * from './utils';
6
+ export * from './switch';
7
+ export * from './template';
8
+ export * from 'react-json-reconciler';
9
+ export * from './compiler/schema';
10
+ export * from './compiler';
@@ -0,0 +1,172 @@
1
+ import React from 'react';
2
+
3
+ export type TemplateInstanceRefStringContext = 'binding' | 'expression';
4
+ export interface TemplateRefStringOptions {
5
+ /** If this template string is inside of another binding or expression */
6
+ nestedContext?: TemplateInstanceRefStringContext;
7
+ }
8
+ export interface TemplateInstanceRefStringOptions {
9
+ /** The array of strings for the template */
10
+ strings: TemplateStringsArray;
11
+ /** the other data that's present in the template */
12
+ other: Array<string | TemplateStringType>;
13
+
14
+ /** If this template string is inside of another binding or expression */
15
+ nestedContext?: TemplateInstanceRefStringContext;
16
+
17
+ /** Convert the value to a reference nested in the given context */
18
+ toRefString: (
19
+ options: TemplateRefStringOptions | undefined,
20
+ value: string
21
+ ) => string;
22
+ }
23
+
24
+ const OpaqueIdentifier = Symbol('TemplateStringType');
25
+
26
+ export type TemplateStringType = React.ReactElement & {
27
+ /** An identifier to show that this is a template type */
28
+ [OpaqueIdentifier]: true;
29
+ /** The value of the template string when in another string */
30
+ toString: () => string;
31
+ /** the raw value of the template string */
32
+ toValue: () => string;
33
+ /** the dereferenced value when used in another */
34
+ toRefString: (options?: TemplateRefStringOptions) => string;
35
+ };
36
+
37
+ export type BindingTemplateInstance = TemplateStringType & {
38
+ /** An identifier for a binding instance */
39
+ __type: 'binding';
40
+ };
41
+
42
+ export type ExpressionTemplateInstance = TemplateStringType & {
43
+ /** The identifier for an expression instance */
44
+ __type: 'expression';
45
+ };
46
+
47
+ /** A react component for rendering a template string type */
48
+ export const TemplateStringComponent = (props: {
49
+ /** The string value of the child template string */
50
+ value: string;
51
+ }) => {
52
+ return React.createElement(
53
+ 'value',
54
+ {
55
+ value: props.value,
56
+ },
57
+ null
58
+ );
59
+ };
60
+
61
+ /** The generic template string handler */
62
+ const createTemplateInstance = (
63
+ options: TemplateInstanceRefStringOptions
64
+ ): TemplateStringType => {
65
+ const value = options.strings.reduce((sum, next, i) => {
66
+ const element = options.other[i];
67
+ if (typeof element === 'string') {
68
+ return sum + next + element;
69
+ }
70
+
71
+ return sum + next + (element?.toRefString(options) ?? '');
72
+ }, '');
73
+
74
+ /** get the unwrapped version */
75
+ const toString = () => {
76
+ return options.toRefString({}, value);
77
+ };
78
+
79
+ /** get the raw value of the template */
80
+ const toValue = () => {
81
+ return value;
82
+ };
83
+
84
+ /** This lets us use it directly as a child element in React */
85
+ const element = React.createElement(
86
+ TemplateStringComponent,
87
+ {
88
+ value: toString(),
89
+ },
90
+ null
91
+ ) as TemplateStringType;
92
+
93
+ return {
94
+ ...element,
95
+ [OpaqueIdentifier]: true,
96
+ toString,
97
+ toValue,
98
+ toRefString: (refStringOptions?: TemplateRefStringOptions) => {
99
+ return options.toRefString(refStringOptions, value);
100
+ },
101
+ };
102
+ };
103
+
104
+ /** Creating an instance of a handler for bindings */
105
+ const createBindingTemplateInstance = (
106
+ options: Omit<TemplateInstanceRefStringOptions, 'toRefString'>
107
+ ): BindingTemplateInstance => {
108
+ const templateInstance = createTemplateInstance({
109
+ ...options,
110
+ toRefString: (context, value) => {
111
+ return `{{${value}}}`;
112
+ },
113
+ }) as BindingTemplateInstance;
114
+
115
+ templateInstance.__type = 'binding';
116
+
117
+ return templateInstance;
118
+ };
119
+
120
+ /** Creating an instance of a handler for bindings */
121
+ const createExpressionTemplateInstance = (
122
+ options: Omit<TemplateInstanceRefStringOptions, 'toRefString'>
123
+ ) => {
124
+ const templateInstance = createTemplateInstance({
125
+ ...options,
126
+ toRefString: (contextOptions, value) => {
127
+ if (contextOptions?.nestedContext === 'expression') {
128
+ return value;
129
+ }
130
+
131
+ const inBinding = contextOptions?.nestedContext === 'binding';
132
+ return `${inBinding ? '`' : '@['}${value}${inBinding ? '`' : ']@'}`;
133
+ },
134
+ }) as ExpressionTemplateInstance;
135
+
136
+ templateInstance.__type = 'expression';
137
+
138
+ return templateInstance;
139
+ };
140
+
141
+ /** A tagged-template constructor for a binding */
142
+ export const binding = (
143
+ strings: TemplateStringsArray,
144
+ ...nested: Array<TemplateStringType | string>
145
+ ): BindingTemplateInstance => {
146
+ return createBindingTemplateInstance({
147
+ strings,
148
+ other: nested,
149
+ nestedContext: 'binding',
150
+ });
151
+ };
152
+
153
+ /** A tagged-template constructor for an expression */
154
+ export const expression = (
155
+ strings: TemplateStringsArray,
156
+ ...nested: Array<
157
+ ExpressionTemplateInstance | BindingTemplateInstance | string
158
+ >
159
+ ): ExpressionTemplateInstance => {
160
+ return createExpressionTemplateInstance({
161
+ strings,
162
+ other: nested,
163
+ nestedContext: 'expression',
164
+ });
165
+ };
166
+
167
+ /** Check if a value is a template string */
168
+ export const isTemplateStringInstance = (
169
+ val: unknown
170
+ ): val is ExpressionTemplateInstance | BindingTemplateInstance => {
171
+ return typeof val === 'object' && (val as any)[OpaqueIdentifier] === true;
172
+ };
package/src/switch.tsx ADDED
@@ -0,0 +1,136 @@
1
+ import type { PropsWithChildren } from 'react';
2
+ import React from 'react';
3
+ import type { ArrayNode, JsonNode, ObjectNode } from 'react-json-reconciler';
4
+ import { flattenNodes, PropertyNode } from 'react-json-reconciler';
5
+ import { SlotContext } from '.';
6
+ import { IDSuffixProvider, OptionalIDSuffixProvider } from './auto-id';
7
+ import type {
8
+ BindingTemplateInstance,
9
+ ExpressionTemplateInstance,
10
+ } from './string-templates';
11
+ import { isTemplateStringInstance } from './string-templates';
12
+ import { normalizeToCollection, toJsonProperties } from './utils';
13
+
14
+ export interface SwitchProps {
15
+ /** defaults to a staticSwitch */
16
+ isDynamic?: boolean;
17
+ }
18
+
19
+ const SwitchContext = React.createContext<
20
+ SwitchProps & {
21
+ /** A text component if we hit a string but expect an asset */
22
+ TextComp?: React.ComponentType;
23
+
24
+ /** A component to create a collection asset is we get an array but need a single element */
25
+ CollectionComp?: React.ComponentType;
26
+ }
27
+ >({});
28
+
29
+ /**
30
+ * Switches allow users to fork content between 1 or more assets
31
+ */
32
+ export const Switch = (props: PropsWithChildren<SwitchProps>) => {
33
+ const slotContext = React.useContext(SlotContext);
34
+ const propertyNode = React.useRef<ObjectNode>(null);
35
+
36
+ return (
37
+ <obj ref={propertyNode}>
38
+ <SwitchContext.Provider
39
+ value={{
40
+ ...props,
41
+ TextComp: slotContext?.TextComp,
42
+ CollectionComp: slotContext?.CollectionComp,
43
+ }}
44
+ >
45
+ <OptionalIDSuffixProvider wrapperRef={propertyNode}>
46
+ <property name={props.isDynamic ? 'dynamicSwitch' : 'staticSwitch'}>
47
+ <SlotContext.Provider value={undefined}>
48
+ <array>{props.children}</array>
49
+ </SlotContext.Provider>
50
+ </property>
51
+ </OptionalIDSuffixProvider>
52
+ </SwitchContext.Provider>
53
+ {slotContext?.additionalProperties &&
54
+ toJsonProperties(slotContext.additionalProperties)}
55
+ </obj>
56
+ );
57
+ };
58
+
59
+ export interface CaseProps {
60
+ /** the test for this case statement */
61
+ exp?: ExpressionTemplateInstance | BindingTemplateInstance | boolean;
62
+ }
63
+
64
+ /** Find the first parent array */
65
+ const findParentArray = (node: JsonNode): ArrayNode => {
66
+ if (node.type === 'array') {
67
+ return node;
68
+ }
69
+
70
+ if (node.parent) {
71
+ return findParentArray(node.parent);
72
+ }
73
+
74
+ throw new Error("can't find parent array");
75
+ };
76
+
77
+ /** Find the index of the item in an array */
78
+ const findArrayIndex = (node: JsonNode): number => {
79
+ const parentArray = findParentArray(node);
80
+ const allSearch = flattenNodes(parentArray.children);
81
+ return allSearch.indexOf(node);
82
+ };
83
+
84
+ /** A case for a switch */
85
+ const Case = (props: PropsWithChildren<CaseProps>) => {
86
+ const slotContext = React.useContext(SlotContext);
87
+ const switchContext = React.useContext(SwitchContext);
88
+ const [caseIndex, setCaseIndex] = React.useState(-1);
89
+ const caseNode = React.useRef<ObjectNode>(null);
90
+
91
+ React.useLayoutEffect(() => {
92
+ if (caseNode.current) {
93
+ const index = findArrayIndex(caseNode.current);
94
+ if (index !== caseIndex) {
95
+ setCaseIndex(index);
96
+ }
97
+ }
98
+ }, [caseIndex]);
99
+
100
+ let expValue: string | boolean = true;
101
+
102
+ if (props.exp !== undefined) {
103
+ expValue = isTemplateStringInstance(props.exp)
104
+ ? props.exp.toValue()
105
+ : props.exp;
106
+ }
107
+
108
+ return (
109
+ <obj ref={caseNode}>
110
+ <property name="case">
111
+ <value value={expValue} />
112
+ </property>
113
+ <IDSuffixProvider
114
+ suffix={`${
115
+ switchContext.isDynamic ? 'dynamicSwitch' : 'staticSwitch'
116
+ }-${caseIndex}`}
117
+ >
118
+ <SlotContext.Provider
119
+ value={
120
+ slotContext ? { ...slotContext, wrapInAsset: false } : undefined
121
+ }
122
+ >
123
+ <property name="asset">
124
+ {normalizeToCollection({
125
+ node: props.children,
126
+ TextComp: switchContext?.TextComp,
127
+ CollectionComp: switchContext?.CollectionComp,
128
+ })}
129
+ </property>
130
+ </SlotContext.Provider>
131
+ </IDSuffixProvider>
132
+ </obj>
133
+ );
134
+ };
135
+
136
+ Switch.Case = Case;