@player-tools/fluent 0.12.1--canary.241.6077
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/cjs/index.cjs +2396 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/index.legacy-esm.js +2276 -0
- package/dist/index.mjs +2276 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -0
- package/src/core/base-builder/__tests__/fluent-builder-base.test.ts +2423 -0
- package/src/core/base-builder/__tests__/fluent-partial.test.ts +179 -0
- package/src/core/base-builder/__tests__/id-generator.test.ts +658 -0
- package/src/core/base-builder/__tests__/registry.test.ts +534 -0
- package/src/core/base-builder/__tests__/resolution-mixed-arrays.test.ts +319 -0
- package/src/core/base-builder/__tests__/resolution-pipeline.test.ts +416 -0
- package/src/core/base-builder/__tests__/resolution-switches.test.ts +468 -0
- package/src/core/base-builder/__tests__/resolution-templates.test.ts +255 -0
- package/src/core/base-builder/__tests__/switch.test.ts +815 -0
- package/src/core/base-builder/__tests__/template.test.ts +596 -0
- package/src/core/base-builder/__tests__/value-extraction.test.ts +200 -0
- package/src/core/base-builder/__tests__/value-storage.test.ts +459 -0
- package/src/core/base-builder/conditional/index.ts +64 -0
- package/src/core/base-builder/context.ts +152 -0
- package/src/core/base-builder/errors.ts +69 -0
- package/src/core/base-builder/fluent-builder-base.ts +308 -0
- package/src/core/base-builder/guards.ts +137 -0
- package/src/core/base-builder/id/generator.ts +290 -0
- package/src/core/base-builder/id/registry.ts +152 -0
- package/src/core/base-builder/index.ts +72 -0
- package/src/core/base-builder/resolution/path-resolver.ts +116 -0
- package/src/core/base-builder/resolution/pipeline.ts +103 -0
- package/src/core/base-builder/resolution/steps/__tests__/nested-asset-wrappers.test.ts +206 -0
- package/src/core/base-builder/resolution/steps/asset-id.ts +77 -0
- package/src/core/base-builder/resolution/steps/asset-wrappers.ts +64 -0
- package/src/core/base-builder/resolution/steps/builders.ts +84 -0
- package/src/core/base-builder/resolution/steps/mixed-arrays.ts +95 -0
- package/src/core/base-builder/resolution/steps/nested-asset-wrappers.ts +124 -0
- package/src/core/base-builder/resolution/steps/static-values.ts +35 -0
- package/src/core/base-builder/resolution/steps/switches.ts +71 -0
- package/src/core/base-builder/resolution/steps/templates.ts +40 -0
- package/src/core/base-builder/resolution/value-resolver.ts +333 -0
- package/src/core/base-builder/storage/auxiliary-storage.ts +82 -0
- package/src/core/base-builder/storage/value-storage.ts +282 -0
- package/src/core/base-builder/types.ts +266 -0
- package/src/core/base-builder/utils.ts +10 -0
- package/src/core/flow/__tests__/index.test.ts +292 -0
- package/src/core/flow/index.ts +118 -0
- package/src/core/index.ts +8 -0
- package/src/core/mocks/generated/action.builder.ts +92 -0
- package/src/core/mocks/generated/choice-item.builder.ts +120 -0
- package/src/core/mocks/generated/choice.builder.ts +134 -0
- package/src/core/mocks/generated/collection.builder.ts +93 -0
- package/src/core/mocks/generated/field-collection.builder.ts +86 -0
- package/src/core/mocks/generated/index.ts +10 -0
- package/src/core/mocks/generated/info.builder.ts +64 -0
- package/src/core/mocks/generated/input.builder.ts +63 -0
- package/src/core/mocks/generated/overview-collection.builder.ts +65 -0
- package/src/core/mocks/generated/splash-collection.builder.ts +93 -0
- package/src/core/mocks/generated/text.builder.ts +47 -0
- package/src/core/mocks/index.ts +1 -0
- package/src/core/mocks/types/action.ts +92 -0
- package/src/core/mocks/types/choice.ts +129 -0
- package/src/core/mocks/types/collection.ts +140 -0
- package/src/core/mocks/types/info.ts +7 -0
- package/src/core/mocks/types/input.ts +7 -0
- package/src/core/mocks/types/text.ts +5 -0
- package/src/core/schema/__tests__/index.test.ts +127 -0
- package/src/core/schema/index.ts +195 -0
- package/src/core/schema/types.ts +7 -0
- package/src/core/switch/__tests__/index.test.ts +156 -0
- package/src/core/switch/index.ts +81 -0
- package/src/core/tagged-template/README.md +448 -0
- package/src/core/tagged-template/__tests__/extract-bindings-from-schema.test.ts +207 -0
- package/src/core/tagged-template/__tests__/index.test.ts +190 -0
- package/src/core/tagged-template/__tests__/schema-std-integration.test.ts +580 -0
- package/src/core/tagged-template/binding.ts +95 -0
- package/src/core/tagged-template/expression.ts +92 -0
- package/src/core/tagged-template/extract-bindings-from-schema.ts +120 -0
- package/src/core/tagged-template/index.ts +5 -0
- package/src/core/tagged-template/std.ts +472 -0
- package/src/core/tagged-template/types.ts +123 -0
- package/src/core/template/__tests__/index.test.ts +380 -0
- package/src/core/template/index.ts +196 -0
- package/src/core/utils/index.ts +160 -0
- package/src/fp/README.md +411 -0
- package/src/fp/__tests__/index.test.ts +1178 -0
- package/src/fp/index.ts +386 -0
- package/src/gen/common.ts +15 -0
- package/src/index.ts +5 -0
- package/src/types.ts +203 -0
- package/types/core/base-builder/conditional/index.d.ts +21 -0
- package/types/core/base-builder/context.d.ts +39 -0
- package/types/core/base-builder/errors.d.ts +45 -0
- package/types/core/base-builder/fluent-builder-base.d.ts +147 -0
- package/types/core/base-builder/guards.d.ts +58 -0
- package/types/core/base-builder/id/generator.d.ts +69 -0
- package/types/core/base-builder/id/registry.d.ts +93 -0
- package/types/core/base-builder/index.d.ts +9 -0
- package/types/core/base-builder/resolution/path-resolver.d.ts +15 -0
- package/types/core/base-builder/resolution/pipeline.d.ts +27 -0
- package/types/core/base-builder/resolution/steps/asset-id.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/builders.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/mixed-arrays.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/nested-asset-wrappers.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/static-values.d.ts +14 -0
- package/types/core/base-builder/resolution/steps/switches.d.ts +15 -0
- package/types/core/base-builder/resolution/steps/templates.d.ts +14 -0
- package/types/core/base-builder/resolution/value-resolver.d.ts +62 -0
- package/types/core/base-builder/storage/auxiliary-storage.d.ts +50 -0
- package/types/core/base-builder/storage/value-storage.d.ts +82 -0
- package/types/core/base-builder/types.d.ts +183 -0
- package/types/core/base-builder/utils.d.ts +2 -0
- package/types/core/flow/index.d.ts +23 -0
- package/types/core/index.d.ts +8 -0
- package/types/core/mocks/index.d.ts +2 -0
- package/types/core/mocks/types/action.d.ts +58 -0
- package/types/core/mocks/types/choice.d.ts +95 -0
- package/types/core/mocks/types/collection.d.ts +102 -0
- package/types/core/mocks/types/info.d.ts +7 -0
- package/types/core/mocks/types/input.d.ts +7 -0
- package/types/core/mocks/types/text.d.ts +5 -0
- package/types/core/schema/index.d.ts +34 -0
- package/types/core/schema/types.d.ts +5 -0
- package/types/core/switch/index.d.ts +21 -0
- package/types/core/tagged-template/binding.d.ts +19 -0
- package/types/core/tagged-template/expression.d.ts +11 -0
- package/types/core/tagged-template/extract-bindings-from-schema.d.ts +7 -0
- package/types/core/tagged-template/index.d.ts +6 -0
- package/types/core/tagged-template/std.d.ts +174 -0
- package/types/core/tagged-template/types.d.ts +69 -0
- package/types/core/template/index.d.ts +97 -0
- package/types/core/utils/index.d.ts +47 -0
- package/types/fp/index.d.ts +149 -0
- package/types/gen/common.d.ts +6 -0
- package/types/index.d.ts +3 -0
- package/types/types.d.ts +163 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Schema } from "@player-ui/types";
|
|
2
|
+
|
|
3
|
+
export const TaggedTemplateValueSymbol: unique symbol = Symbol(
|
|
4
|
+
"TaggedTemplateValue",
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
export interface TemplateRefOptions {
|
|
8
|
+
nestedContext?: "binding" | "expression";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TaggedTemplateValue<T = unknown> {
|
|
12
|
+
[TaggedTemplateValueSymbol]: true;
|
|
13
|
+
/** Phantom type marker - not available at runtime */
|
|
14
|
+
readonly _phantomType?: T;
|
|
15
|
+
toValue(): string;
|
|
16
|
+
toRefString(options?: TemplateRefOptions): string;
|
|
17
|
+
toString(): string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isTaggedTemplateValue(
|
|
21
|
+
value: unknown,
|
|
22
|
+
): value is TaggedTemplateValue {
|
|
23
|
+
return (
|
|
24
|
+
typeof value === "object" &&
|
|
25
|
+
value !== null &&
|
|
26
|
+
TaggedTemplateValueSymbol in value
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Type that converts an object type to a bindable proxy type
|
|
32
|
+
* Each property access returns a TaggedTemplateValue with the correct type
|
|
33
|
+
*/
|
|
34
|
+
export type BindableProxy<T> = {
|
|
35
|
+
readonly [K in keyof T]: T[K] extends unknown[]
|
|
36
|
+
? TaggedTemplateValue<T[K]> // Arrays become bindings directly
|
|
37
|
+
: T[K] extends object
|
|
38
|
+
? BindableProxy<T[K]> // Objects become nested proxies
|
|
39
|
+
: TaggedTemplateValue<T[K]>; // Primitives become bindings
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maps primitive Schema types to their corresponding TypeScript types
|
|
44
|
+
*/
|
|
45
|
+
type PrimitiveTypeMap = {
|
|
46
|
+
StringType: string;
|
|
47
|
+
NumberType: number;
|
|
48
|
+
BooleanType: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Utility to force TypeScript to fully evaluate a type
|
|
53
|
+
*/
|
|
54
|
+
type Evaluate<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a type name is primitive
|
|
58
|
+
*/
|
|
59
|
+
type IsPrimitiveType<T extends string> = T extends keyof PrimitiveTypeMap
|
|
60
|
+
? true
|
|
61
|
+
: false;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert a primitive type name to its TaggedTemplateValue
|
|
65
|
+
*/
|
|
66
|
+
type PrimitiveToBinding<T extends string> = T extends keyof PrimitiveTypeMap
|
|
67
|
+
? TaggedTemplateValue<PrimitiveTypeMap[T]>
|
|
68
|
+
: TaggedTemplateValue<string>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Process a single field from a node into its binding representation
|
|
72
|
+
*/
|
|
73
|
+
type ProcessField<
|
|
74
|
+
Field extends Schema.DataTypes,
|
|
75
|
+
S extends Schema.Schema,
|
|
76
|
+
CurrentPath extends string,
|
|
77
|
+
> = Field extends { type: infer TypeName extends string }
|
|
78
|
+
? Field extends { isArray: true }
|
|
79
|
+
? // Handle array fields
|
|
80
|
+
IsPrimitiveType<TypeName> extends true
|
|
81
|
+
? TypeName extends "StringType"
|
|
82
|
+
? { name: PrimitiveToBinding<TypeName> }
|
|
83
|
+
: { value: PrimitiveToBinding<TypeName> }
|
|
84
|
+
: TypeName extends keyof S
|
|
85
|
+
? S[TypeName] extends Schema.Node
|
|
86
|
+
? ProcessNodeFields<S[TypeName], S, `${CurrentPath}._current_`>
|
|
87
|
+
: TaggedTemplateValue<string>
|
|
88
|
+
: TaggedTemplateValue<string>
|
|
89
|
+
: // Handle non-array fields
|
|
90
|
+
IsPrimitiveType<TypeName> extends true
|
|
91
|
+
? PrimitiveToBinding<TypeName>
|
|
92
|
+
: TypeName extends keyof S
|
|
93
|
+
? S[TypeName] extends Schema.Node
|
|
94
|
+
? ProcessNodeFields<S[TypeName], S, CurrentPath>
|
|
95
|
+
: TaggedTemplateValue<string>
|
|
96
|
+
: TaggedTemplateValue<string>
|
|
97
|
+
: TaggedTemplateValue<string>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Process all fields in a node
|
|
101
|
+
*/
|
|
102
|
+
type ProcessNodeFields<
|
|
103
|
+
Node extends Schema.Node,
|
|
104
|
+
S extends Schema.Schema,
|
|
105
|
+
BasePath extends string,
|
|
106
|
+
> = Evaluate<{
|
|
107
|
+
[K in keyof Node]: ProcessField<
|
|
108
|
+
Node[K],
|
|
109
|
+
S,
|
|
110
|
+
BasePath extends "" ? K & string : `${BasePath}.${K & string}`
|
|
111
|
+
>;
|
|
112
|
+
}>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Main type to extract bindings from a schema
|
|
116
|
+
*/
|
|
117
|
+
export type ExtractedBindings<S extends Schema.Schema> = S extends {
|
|
118
|
+
ROOT: infer RootNode;
|
|
119
|
+
}
|
|
120
|
+
? RootNode extends Schema.Node
|
|
121
|
+
? ProcessNodeFields<RootNode, S, "">
|
|
122
|
+
: never
|
|
123
|
+
: never;
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "vitest";
|
|
2
|
+
import type { Template, Asset } from "@player-ui/types";
|
|
3
|
+
import { template, isTemplate, TEMPLATE_MARKER } from "../index";
|
|
4
|
+
import {
|
|
5
|
+
type BaseBuildContext,
|
|
6
|
+
type SlotBranch,
|
|
7
|
+
resetGlobalIdSet,
|
|
8
|
+
} from "../../base-builder";
|
|
9
|
+
import { text } from "../../mocks";
|
|
10
|
+
import { binding as b } from "../../tagged-template";
|
|
11
|
+
|
|
12
|
+
// Mock BaseBuildContext
|
|
13
|
+
const mockBaseBuildContext: BaseBuildContext = {
|
|
14
|
+
parentId: "parent-1",
|
|
15
|
+
branch: {
|
|
16
|
+
type: "slot",
|
|
17
|
+
name: "test",
|
|
18
|
+
} as SlotBranch,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("template", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
resetGlobalIdSet();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("create a basic template configuration", () => {
|
|
27
|
+
const result = template({
|
|
28
|
+
data: "list.of.names",
|
|
29
|
+
output: "values",
|
|
30
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
31
|
+
})(mockBaseBuildContext);
|
|
32
|
+
|
|
33
|
+
const expected: Template<{ asset: Asset<"text"> }> = {
|
|
34
|
+
data: "list.of.names",
|
|
35
|
+
output: "values",
|
|
36
|
+
value: {
|
|
37
|
+
asset: {
|
|
38
|
+
id: "parent-1-test-_index_-text",
|
|
39
|
+
type: "text",
|
|
40
|
+
value: "{{list.of.names._index_}}",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(result).toEqual(expected);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("create a template with tagged template binding", () => {
|
|
49
|
+
const result = template({
|
|
50
|
+
data: b`list.of.names`,
|
|
51
|
+
output: "values",
|
|
52
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
53
|
+
})(mockBaseBuildContext);
|
|
54
|
+
|
|
55
|
+
const expected: Template<{ asset: Asset<"text"> }> = {
|
|
56
|
+
data: "{{list.of.names}}",
|
|
57
|
+
output: "values",
|
|
58
|
+
value: {
|
|
59
|
+
asset: {
|
|
60
|
+
id: "parent-1-test-_index_-text",
|
|
61
|
+
type: "text",
|
|
62
|
+
value: "{{list.of.names._index_}}",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
expect(result).toEqual(expected);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("include dynamic flag when specified", () => {
|
|
71
|
+
const result = template({
|
|
72
|
+
data: "list.of.names",
|
|
73
|
+
output: "values",
|
|
74
|
+
dynamic: true,
|
|
75
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
76
|
+
})(mockBaseBuildContext);
|
|
77
|
+
|
|
78
|
+
const expected: Template<{ asset: Asset<"text"> }> = {
|
|
79
|
+
data: "list.of.names",
|
|
80
|
+
output: "values",
|
|
81
|
+
dynamic: true,
|
|
82
|
+
value: {
|
|
83
|
+
asset: {
|
|
84
|
+
id: "parent-1-test-_index_-text",
|
|
85
|
+
type: "text",
|
|
86
|
+
value: "{{list.of.names._index_}}",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
expect(result).toEqual(expected);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("not include dynamic flag when it is false", () => {
|
|
95
|
+
const result = template({
|
|
96
|
+
data: "list.of.names",
|
|
97
|
+
output: "values",
|
|
98
|
+
dynamic: false,
|
|
99
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
100
|
+
})(mockBaseBuildContext);
|
|
101
|
+
|
|
102
|
+
const expected: Template<{ asset: Asset<"text"> }> = {
|
|
103
|
+
data: "list.of.names",
|
|
104
|
+
output: "values",
|
|
105
|
+
value: {
|
|
106
|
+
asset: {
|
|
107
|
+
id: "parent-1-test-_index_-text",
|
|
108
|
+
type: "text",
|
|
109
|
+
value: "{{list.of.names._index_}}",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
expect(result).toEqual(expected);
|
|
115
|
+
expect(result).not.toHaveProperty("dynamic");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("pass the correct parent context to the value function", () => {
|
|
119
|
+
let capturedBaseBuildContext: BaseBuildContext | null = null;
|
|
120
|
+
|
|
121
|
+
const valueWithCapture = (parentCtx: BaseBuildContext): Asset<"text"> => {
|
|
122
|
+
capturedBaseBuildContext = parentCtx;
|
|
123
|
+
return {
|
|
124
|
+
id: "test",
|
|
125
|
+
type: "text",
|
|
126
|
+
value: "test",
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
template({
|
|
131
|
+
data: "list.of.names",
|
|
132
|
+
output: "values",
|
|
133
|
+
value: valueWithCapture,
|
|
134
|
+
})(mockBaseBuildContext);
|
|
135
|
+
|
|
136
|
+
expect(capturedBaseBuildContext).toEqual({
|
|
137
|
+
parentId: "parent-1-test",
|
|
138
|
+
branch: {
|
|
139
|
+
type: "template",
|
|
140
|
+
depth: 0,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Simulation of a multiple templates scenario (we test the output structure, not runtime behavior)
|
|
146
|
+
test("support the structure for multiple templates with the same output property", () => {
|
|
147
|
+
const template1 = template({
|
|
148
|
+
data: "list.of.names",
|
|
149
|
+
output: "values",
|
|
150
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
151
|
+
})(mockBaseBuildContext);
|
|
152
|
+
|
|
153
|
+
const template2 = template({
|
|
154
|
+
data: "list.of.other-names",
|
|
155
|
+
output: "values",
|
|
156
|
+
value: text().withValue(b`list.of.other-names._index_`),
|
|
157
|
+
})(mockBaseBuildContext);
|
|
158
|
+
|
|
159
|
+
expect(template1.output).toEqual(template2.output);
|
|
160
|
+
expect(template1.output).toBe("values");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Test complex nested asset structures
|
|
164
|
+
test("handle complex nested asset structures", () => {
|
|
165
|
+
interface CollectionAsset extends Asset<"collection"> {
|
|
166
|
+
items: Array<{ asset: Asset<"text"> }>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const collectionAsset = (): CollectionAsset => ({
|
|
170
|
+
id: "collection-_index_",
|
|
171
|
+
type: "collection",
|
|
172
|
+
items: [
|
|
173
|
+
{
|
|
174
|
+
asset: {
|
|
175
|
+
id: "item-_index_-0",
|
|
176
|
+
type: "text",
|
|
177
|
+
value: "{{list.of.names._index_.first}}",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
asset: {
|
|
182
|
+
id: "item-_index_-1",
|
|
183
|
+
type: "text",
|
|
184
|
+
value: "{{list.of.names._index_.last}}",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const result = template({
|
|
191
|
+
data: "list.of.names",
|
|
192
|
+
output: "collections",
|
|
193
|
+
value: collectionAsset,
|
|
194
|
+
})(mockBaseBuildContext);
|
|
195
|
+
|
|
196
|
+
const expected: Template<{ asset: CollectionAsset }> = {
|
|
197
|
+
data: "list.of.names",
|
|
198
|
+
output: "collections",
|
|
199
|
+
value: {
|
|
200
|
+
asset: {
|
|
201
|
+
id: "collection-_index_",
|
|
202
|
+
type: "collection",
|
|
203
|
+
items: [
|
|
204
|
+
{
|
|
205
|
+
asset: {
|
|
206
|
+
id: "item-_index_-0",
|
|
207
|
+
type: "text",
|
|
208
|
+
value: "{{list.of.names._index_.first}}",
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
asset: {
|
|
213
|
+
id: "item-_index_-1",
|
|
214
|
+
type: "text",
|
|
215
|
+
value: "{{list.of.names._index_.last}}",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
expect(result).toEqual(expected);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Test the structure for dynamic template functionality
|
|
227
|
+
test("create a dynamic template that updates when data changes", () => {
|
|
228
|
+
const result = template({
|
|
229
|
+
data: "list.of.names",
|
|
230
|
+
output: "values",
|
|
231
|
+
dynamic: true,
|
|
232
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
233
|
+
})(mockBaseBuildContext);
|
|
234
|
+
|
|
235
|
+
expect(result.dynamic).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("isTemplate type guard", () => {
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
resetGlobalIdSet();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("should return true for template functions", () => {
|
|
245
|
+
const templateFn = template({
|
|
246
|
+
data: "list.of.names",
|
|
247
|
+
output: "values",
|
|
248
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(isTemplate(templateFn)).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("should return false for non-template functions", () => {
|
|
255
|
+
const regularFunction = () => ({});
|
|
256
|
+
const arrowFunction = () => "test";
|
|
257
|
+
const objectWithFunction = { fn: () => {} };
|
|
258
|
+
|
|
259
|
+
expect(isTemplate(regularFunction)).toBe(false);
|
|
260
|
+
expect(isTemplate(arrowFunction)).toBe(false);
|
|
261
|
+
expect(isTemplate(objectWithFunction.fn)).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("should return false for non-functions", () => {
|
|
265
|
+
const string = "not a function";
|
|
266
|
+
const number = 42;
|
|
267
|
+
const object = { type: "test" };
|
|
268
|
+
const nullValue = null;
|
|
269
|
+
const undefinedValue = undefined;
|
|
270
|
+
|
|
271
|
+
expect(isTemplate(string)).toBe(false);
|
|
272
|
+
expect(isTemplate(number)).toBe(false);
|
|
273
|
+
expect(isTemplate(object)).toBe(false);
|
|
274
|
+
expect(isTemplate(nullValue)).toBe(false);
|
|
275
|
+
expect(isTemplate(undefinedValue)).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("should have TEMPLATE_MARKER symbol on template functions", () => {
|
|
279
|
+
const templateFn = template({
|
|
280
|
+
data: "list.of.names",
|
|
281
|
+
output: "values",
|
|
282
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(TEMPLATE_MARKER in templateFn).toBe(true);
|
|
286
|
+
expect(
|
|
287
|
+
(templateFn as unknown as { [TEMPLATE_MARKER]: unknown })[
|
|
288
|
+
TEMPLATE_MARKER
|
|
289
|
+
],
|
|
290
|
+
).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("should not have TEMPLATE_MARKER symbol on regular functions", () => {
|
|
294
|
+
const regularFunction = () => ({});
|
|
295
|
+
|
|
296
|
+
expect(TEMPLATE_MARKER in regularFunction).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("should work with template functions that have been called", () => {
|
|
300
|
+
const templateFn = template({
|
|
301
|
+
data: "list.of.names",
|
|
302
|
+
output: "values",
|
|
303
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Call the template function
|
|
307
|
+
templateFn(mockBaseBuildContext);
|
|
308
|
+
|
|
309
|
+
// The original function should still be identifiable as a template
|
|
310
|
+
expect(isTemplate(templateFn)).toBe(true);
|
|
311
|
+
expect(TEMPLATE_MARKER in templateFn).toBe(true);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("template with optional output", () => {
|
|
316
|
+
beforeEach(() => {
|
|
317
|
+
resetGlobalIdSet();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("should infer output from slot context", () => {
|
|
321
|
+
const contextWithSlot: BaseBuildContext = {
|
|
322
|
+
parentId: "parent-1",
|
|
323
|
+
branch: {
|
|
324
|
+
type: "slot",
|
|
325
|
+
name: "values-0", // This should infer "values" as the output
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const templateFn = template({
|
|
330
|
+
data: b`list.of.names`,
|
|
331
|
+
// No output provided - should be inferred
|
|
332
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const result = templateFn(contextWithSlot);
|
|
336
|
+
|
|
337
|
+
expect(result.output).toBe("values");
|
|
338
|
+
expect(result.data).toBe("{{list.of.names}}");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("should throw error when output cannot be inferred", () => {
|
|
342
|
+
const contextWithoutSlot: BaseBuildContext = {
|
|
343
|
+
parentId: "parent-1",
|
|
344
|
+
branch: {
|
|
345
|
+
type: "template",
|
|
346
|
+
depth: 0,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const templateFn = template({
|
|
351
|
+
data: b`list.of.names`,
|
|
352
|
+
// No output provided and context doesn't allow inference
|
|
353
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(() => templateFn(contextWithoutSlot)).toThrow(
|
|
357
|
+
"Template output must be provided or inferrable from context",
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("should use explicit output when provided", () => {
|
|
362
|
+
const contextWithSlot: BaseBuildContext = {
|
|
363
|
+
parentId: "parent-1",
|
|
364
|
+
branch: {
|
|
365
|
+
type: "slot",
|
|
366
|
+
name: "values-0",
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const templateFn = template({
|
|
371
|
+
data: b`list.of.names`,
|
|
372
|
+
output: "customOutput", // Explicit output should override inference
|
|
373
|
+
value: text().withValue(b`list.of.names._index_`),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const result = templateFn(contextWithSlot);
|
|
377
|
+
|
|
378
|
+
expect(result.output).toBe("customOutput");
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Asset, Template } from "@player-ui/types";
|
|
2
|
+
import {
|
|
3
|
+
type BaseBuildContext,
|
|
4
|
+
genId,
|
|
5
|
+
isFluentBuilder,
|
|
6
|
+
BranchTypes,
|
|
7
|
+
} from "../base-builder";
|
|
8
|
+
import {
|
|
9
|
+
isTaggedTemplateValue,
|
|
10
|
+
type TaggedTemplateValue,
|
|
11
|
+
} from "../tagged-template";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Symbol marker to identify template functions
|
|
15
|
+
*/
|
|
16
|
+
export const TEMPLATE_MARKER = Symbol.for("fluent-builder-template");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Type guard to check if a function is a template function
|
|
20
|
+
*/
|
|
21
|
+
export function isTemplate(fn: unknown): fn is ReturnType<typeof template> {
|
|
22
|
+
return (
|
|
23
|
+
typeof fn === "function" &&
|
|
24
|
+
TEMPLATE_MARKER in (fn as { [TEMPLATE_MARKER]?: unknown })
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Arguments for creating a template configuration
|
|
30
|
+
* @template T - The type of asset that will be created for each item in the array
|
|
31
|
+
*/
|
|
32
|
+
interface TemplateArgs<T extends Asset<string>> {
|
|
33
|
+
/** A binding that points to an array in the model */
|
|
34
|
+
readonly data: string | TaggedTemplateValue;
|
|
35
|
+
/** A property to put the mapped objects. If not provided, will be inferred from context */
|
|
36
|
+
readonly output?: string;
|
|
37
|
+
/** The asset creator - can be a static asset, a FluentBuilder, or a builder function that returns an asset */
|
|
38
|
+
readonly value:
|
|
39
|
+
| T
|
|
40
|
+
| { build(context?: BaseBuildContext): T }
|
|
41
|
+
| (<K extends BaseBuildContext>(ctx: K) => T);
|
|
42
|
+
/** Whether template should be recomputed when data changes */
|
|
43
|
+
readonly dynamic?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a template configuration for dynamically creating a list of assets based on array data.
|
|
48
|
+
* Templates provide a way to dynamically create a list of assets, or any object, based on data from the model.
|
|
49
|
+
* All of the templating semantics are removed by the time it reaches an asset's transform or UI layer.
|
|
50
|
+
*
|
|
51
|
+
* Within a template, the `_index_` string can be used to substitute the array-index of the item being mapped.
|
|
52
|
+
*
|
|
53
|
+
* Multiple templates:
|
|
54
|
+
* - Templates can be nested. Use `_index_` for the outer loop, `_index1_` for the inner loop, and so on.
|
|
55
|
+
* - Multiple templates can output to the same property using the same output name. Items will be appended.
|
|
56
|
+
* - Templates can append to existing arrays by using the same output property name.
|
|
57
|
+
*
|
|
58
|
+
* Dynamic vs Static Templates:
|
|
59
|
+
* - If dynamic is false (default), the template will be parsed when a view first renders and won't update as data changes.
|
|
60
|
+
* - If dynamic is true, template will be updated whenever data changes while a view is still showing.
|
|
61
|
+
*
|
|
62
|
+
* @param args - The template configuration arguments
|
|
63
|
+
* @returns A function that takes parent context and returns a Template configuration
|
|
64
|
+
* @see https://player-ui.github.io/next/content/assets-views/#templates
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* // Using a static asset
|
|
68
|
+
* template({
|
|
69
|
+
* data: binding`users`,
|
|
70
|
+
* output: "items",
|
|
71
|
+
* value: text({ value: binding`users._index_.name` })
|
|
72
|
+
* })(parentCtx)
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* // Using a builder function
|
|
78
|
+
* template({
|
|
79
|
+
* data: binding`users`,
|
|
80
|
+
* output: "items",
|
|
81
|
+
* value: (ctx) => text({ value: binding`users._index_.name` }).withId(genId(ctx))
|
|
82
|
+
* })(parentCtx)
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @example Multiple templates with the same output
|
|
86
|
+
* ```ts
|
|
87
|
+
* [
|
|
88
|
+
* template({
|
|
89
|
+
* data: binding`names`,
|
|
90
|
+
* output: "values",
|
|
91
|
+
* value: text({ id: `name-_index_`, value: binding`names._index_` })
|
|
92
|
+
* })(parentCtx),
|
|
93
|
+
* template({
|
|
94
|
+
* data: binding`otherNames`,
|
|
95
|
+
* output: "values",
|
|
96
|
+
* value: text({ id: `other-name-_index_`, value: binding`otherNames._index_` })
|
|
97
|
+
* })(parentCtx)
|
|
98
|
+
* ]
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @example Dynamic template that updates when data changes
|
|
102
|
+
* ```ts
|
|
103
|
+
* template({
|
|
104
|
+
* data: binding`users`,
|
|
105
|
+
* output: "items",
|
|
106
|
+
* dynamic: true,
|
|
107
|
+
* value: text({ value: binding`users._index_.name` })
|
|
108
|
+
* })(parentCtx)
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export const template = <T extends Asset<string>>({
|
|
112
|
+
data,
|
|
113
|
+
output,
|
|
114
|
+
value,
|
|
115
|
+
dynamic = false,
|
|
116
|
+
}: TemplateArgs<T>) => {
|
|
117
|
+
const templateFn = (parentCtx: BaseBuildContext): Template<{ asset: T }> => {
|
|
118
|
+
// If output is not provided, try to infer from context
|
|
119
|
+
const resolvedOutput = output || inferOutputFromContext(parentCtx);
|
|
120
|
+
|
|
121
|
+
if (!resolvedOutput) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"Template output must be provided or inferrable from context. " +
|
|
124
|
+
"When using template in asset arrays, ensure the array property can be inferred " +
|
|
125
|
+
"(e.g., collection().withValues([template(...)]) infers 'values' as output).",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create template context for the value - use the generated parent ID to include slot context
|
|
130
|
+
const templateValueCtx: BaseBuildContext = {
|
|
131
|
+
parentId: genId(parentCtx),
|
|
132
|
+
branch: {
|
|
133
|
+
type: BranchTypes.TEMPLATE,
|
|
134
|
+
depth: 0,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
let resolvedAsset: T;
|
|
139
|
+
if (isFluentBuilder(value)) {
|
|
140
|
+
resolvedAsset = value.build(templateValueCtx) as T;
|
|
141
|
+
} else if (typeof value === "function") {
|
|
142
|
+
const builderResult = value(templateValueCtx);
|
|
143
|
+
if (typeof builderResult === "function") {
|
|
144
|
+
resolvedAsset = (builderResult as (ctx: BaseBuildContext) => T)(
|
|
145
|
+
templateValueCtx,
|
|
146
|
+
);
|
|
147
|
+
} else {
|
|
148
|
+
resolvedAsset = builderResult;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
if (typeof value === "object" && value !== null) {
|
|
152
|
+
resolvedAsset = { ...value } as T;
|
|
153
|
+
} else {
|
|
154
|
+
resolvedAsset = value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
data: isTaggedTemplateValue(data) ? data.toString() : data,
|
|
160
|
+
output: resolvedOutput,
|
|
161
|
+
value: {
|
|
162
|
+
asset: resolvedAsset,
|
|
163
|
+
},
|
|
164
|
+
...(dynamic && { dynamic }),
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Add the marker symbol to the function
|
|
169
|
+
(templateFn as typeof templateFn & { [TEMPLATE_MARKER]: true })[
|
|
170
|
+
TEMPLATE_MARKER
|
|
171
|
+
] = true;
|
|
172
|
+
|
|
173
|
+
return templateFn;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Helper function to infer output property name from BaseBuildContext
|
|
178
|
+
*/
|
|
179
|
+
function inferOutputFromContext(
|
|
180
|
+
parentCtx: BaseBuildContext,
|
|
181
|
+
): string | undefined {
|
|
182
|
+
// Check if we're in a slot context that can give us the property name
|
|
183
|
+
if (parentCtx.branch?.type === "slot") {
|
|
184
|
+
const slotName = parentCtx.branch.name;
|
|
185
|
+
// Extract property name from slot names like "values-0", "items-2", etc.
|
|
186
|
+
const match = slotName.match(/^([a-zA-Z_][a-zA-Z0-9_]*)-\d+$/);
|
|
187
|
+
if (match) {
|
|
188
|
+
return match[1];
|
|
189
|
+
}
|
|
190
|
+
// If it's just a property name without index, use it directly
|
|
191
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(slotName)) {
|
|
192
|
+
return slotName;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|