@mmapp/player-core 0.1.0-alpha.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/index.d.mts +1436 -0
- package/dist/index.d.ts +1436 -0
- package/dist/index.js +4828 -0
- package/dist/index.mjs +4762 -0
- package/package.json +35 -0
- package/package.json.backup +35 -0
- package/src/__tests__/actions.test.ts +187 -0
- package/src/__tests__/blueprint-e2e.test.ts +706 -0
- package/src/__tests__/blueprint-test-runner.test.ts +680 -0
- package/src/__tests__/core-functions.test.ts +78 -0
- package/src/__tests__/dsl-compiler.test.ts +1382 -0
- package/src/__tests__/dsl-grammar.test.ts +1682 -0
- package/src/__tests__/events.test.ts +200 -0
- package/src/__tests__/expression.test.ts +296 -0
- package/src/__tests__/failure-policies.test.ts +110 -0
- package/src/__tests__/frontend-context.test.ts +182 -0
- package/src/__tests__/integration.test.ts +256 -0
- package/src/__tests__/security.test.ts +190 -0
- package/src/__tests__/state-machine.test.ts +450 -0
- package/src/__tests__/testing-engine.test.ts +671 -0
- package/src/actions/dispatcher.ts +80 -0
- package/src/actions/index.ts +7 -0
- package/src/actions/types.ts +25 -0
- package/src/dsl/compiler/component-mapper.ts +289 -0
- package/src/dsl/compiler/field-mapper.ts +187 -0
- package/src/dsl/compiler/index.ts +82 -0
- package/src/dsl/compiler/manifest-compiler.ts +76 -0
- package/src/dsl/compiler/symbol-table.ts +214 -0
- package/src/dsl/compiler/utils.ts +48 -0
- package/src/dsl/compiler/view-compiler.ts +286 -0
- package/src/dsl/compiler/workflow-compiler.ts +600 -0
- package/src/dsl/index.ts +66 -0
- package/src/dsl/ir-migration.ts +221 -0
- package/src/dsl/ir-types.ts +416 -0
- package/src/dsl/lexer.ts +579 -0
- package/src/dsl/parser.ts +115 -0
- package/src/dsl/types.ts +256 -0
- package/src/events/event-bus.ts +68 -0
- package/src/events/index.ts +9 -0
- package/src/events/pattern-matcher.ts +61 -0
- package/src/events/types.ts +27 -0
- package/src/expression/evaluator.ts +676 -0
- package/src/expression/functions.ts +214 -0
- package/src/expression/index.ts +13 -0
- package/src/expression/types.ts +64 -0
- package/src/index.ts +61 -0
- package/src/state-machine/index.ts +16 -0
- package/src/state-machine/interpreter.ts +319 -0
- package/src/state-machine/types.ts +89 -0
- package/src/testing/action-trace.ts +209 -0
- package/src/testing/blueprint-test-runner.ts +214 -0
- package/src/testing/graph-walker.ts +249 -0
- package/src/testing/index.ts +69 -0
- package/src/testing/nrt-comparator.ts +199 -0
- package/src/testing/nrt-types.ts +230 -0
- package/src/testing/test-actions.ts +645 -0
- package/src/testing/test-compiler.ts +278 -0
- package/src/testing/test-runner.ts +444 -0
- package/src/testing/types.ts +231 -0
- package/src/validation/definition-validator.ts +812 -0
- package/src/validation/index.ts +13 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest Compiler — Pass 4 of compilation.
|
|
3
|
+
*
|
|
4
|
+
* Transforms space declaration → IRBlueprintManifest.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { IRBlueprintManifest, CompilerError } from '../ir-types';
|
|
8
|
+
import type { SymbolTable } from './symbol-table';
|
|
9
|
+
import { slugify } from './utils';
|
|
10
|
+
|
|
11
|
+
// =============================================================================
|
|
12
|
+
// Public API
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
export function compileManifest(
|
|
16
|
+
symbols: SymbolTable,
|
|
17
|
+
): { manifest?: IRBlueprintManifest; errors: CompilerError[] } {
|
|
18
|
+
const errors: CompilerError[] = [];
|
|
19
|
+
|
|
20
|
+
if (!symbols.space) {
|
|
21
|
+
return { manifest: undefined, errors };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const space = symbols.space;
|
|
25
|
+
|
|
26
|
+
// Workflows from thing refs
|
|
27
|
+
const workflows = space.thingRefs.map(ref => ({
|
|
28
|
+
slug: slugify(ref.name),
|
|
29
|
+
role: (ref.kind ?? 'primary') as 'primary' | 'child' | 'derived' | 'utility',
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// Experience ID from space name
|
|
33
|
+
const experience_id = slugify(space.name);
|
|
34
|
+
|
|
35
|
+
// Routes from paths
|
|
36
|
+
const routes = space.paths.map(p => {
|
|
37
|
+
const route: {
|
|
38
|
+
path: string;
|
|
39
|
+
node: string;
|
|
40
|
+
entityType?: string;
|
|
41
|
+
entityIdSource?: 'user' | 'param';
|
|
42
|
+
} = {
|
|
43
|
+
path: p.path,
|
|
44
|
+
node: slugify(p.view),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (p.context) {
|
|
48
|
+
route.entityType = p.context;
|
|
49
|
+
route.entityIdSource = inferEntityIdSource(p.path, p.context);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return route;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
manifest: {
|
|
57
|
+
workflows,
|
|
58
|
+
experience_id,
|
|
59
|
+
routes: routes.length > 0 ? routes : undefined,
|
|
60
|
+
},
|
|
61
|
+
errors,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Entity ID Source Inference
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
function inferEntityIdSource(
|
|
70
|
+
path: string,
|
|
71
|
+
context: string,
|
|
72
|
+
): 'user' | 'param' {
|
|
73
|
+
if (context === 'user') return 'user';
|
|
74
|
+
if (path.includes(':id')) return 'param';
|
|
75
|
+
return 'user';
|
|
76
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol Table — Pass 1 of compilation.
|
|
3
|
+
*
|
|
4
|
+
* Walks all root-level AST nodes and collects declared names:
|
|
5
|
+
* space, things (workflows), fragments, and views.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ASTNode } from '../types';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Symbol Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export interface SpaceSymbol {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
node: ASTNode;
|
|
18
|
+
thingRefs: Array<{ name: string; kind?: string }>;
|
|
19
|
+
paths: Array<{ path: string; view: string; context?: string }>;
|
|
20
|
+
tags: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ThingSymbol {
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
node: ASTNode;
|
|
27
|
+
fields: ASTNode[];
|
|
28
|
+
states: ASTNode[];
|
|
29
|
+
startsAt?: string;
|
|
30
|
+
tags: string[];
|
|
31
|
+
levels: ASTNode[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FragmentSymbol {
|
|
35
|
+
name: string;
|
|
36
|
+
node: ASTNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ViewSymbol {
|
|
40
|
+
name: string;
|
|
41
|
+
node: ASTNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SymbolTable {
|
|
45
|
+
space?: SpaceSymbol;
|
|
46
|
+
things: Map<string, ThingSymbol>;
|
|
47
|
+
fragments: Map<string, FragmentSymbol>;
|
|
48
|
+
views: Map<string, ViewSymbol>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// View-like child types (used to distinguish root views from states)
|
|
53
|
+
// =============================================================================
|
|
54
|
+
|
|
55
|
+
const VIEW_CHILD_TYPES = new Set([
|
|
56
|
+
'data_source', 'content', 'iteration', 'section',
|
|
57
|
+
'string_literal', 'search', 'grouping', 'navigation', 'pages',
|
|
58
|
+
'qualifier',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Public API
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
export function collectSymbols(nodes: ASTNode[]): SymbolTable {
|
|
66
|
+
const table: SymbolTable = {
|
|
67
|
+
things: new Map(),
|
|
68
|
+
fragments: new Map(),
|
|
69
|
+
views: new Map(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
for (const node of nodes) {
|
|
73
|
+
const { data } = node.token;
|
|
74
|
+
|
|
75
|
+
switch (data.type) {
|
|
76
|
+
case 'space_decl':
|
|
77
|
+
table.space = collectSpace(node);
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'thing_decl':
|
|
81
|
+
table.things.set(data.name, collectThing(node));
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'fragment_def':
|
|
85
|
+
table.fragments.set(data.name, { name: data.name, node });
|
|
86
|
+
break;
|
|
87
|
+
|
|
88
|
+
case 'state_decl':
|
|
89
|
+
// Root-level state_decl with view-like children → it's actually a view
|
|
90
|
+
if (hasViewChildren(node)) {
|
|
91
|
+
table.views.set(data.name, { name: data.name, node });
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
default:
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return table;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// Space Collection
|
|
105
|
+
// =============================================================================
|
|
106
|
+
|
|
107
|
+
function collectSpace(node: ASTNode): SpaceSymbol {
|
|
108
|
+
const data = node.token.data;
|
|
109
|
+
if (data.type !== 'space_decl') throw new Error('Expected space_decl');
|
|
110
|
+
|
|
111
|
+
const space: SpaceSymbol = {
|
|
112
|
+
name: data.name,
|
|
113
|
+
version: data.version,
|
|
114
|
+
node,
|
|
115
|
+
thingRefs: [],
|
|
116
|
+
paths: [],
|
|
117
|
+
tags: [],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (const child of node.children) {
|
|
121
|
+
const cd = child.token.data;
|
|
122
|
+
|
|
123
|
+
if (cd.type === 'tagged') {
|
|
124
|
+
space.tags.push(...cd.tags);
|
|
125
|
+
} else if (cd.type === 'section' && cd.name === 'things') {
|
|
126
|
+
for (const ref of child.children) {
|
|
127
|
+
if (ref.token.data.type === 'thing_ref') {
|
|
128
|
+
space.thingRefs.push({
|
|
129
|
+
name: ref.token.data.name,
|
|
130
|
+
kind: ref.token.data.kind,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else if (cd.type === 'section' && cd.name === 'paths') {
|
|
135
|
+
for (const pm of child.children) {
|
|
136
|
+
if (pm.token.data.type === 'path_mapping') {
|
|
137
|
+
space.paths.push({
|
|
138
|
+
path: pm.token.data.path,
|
|
139
|
+
view: pm.token.data.view,
|
|
140
|
+
context: pm.token.data.context,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return space;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// Thing Collection
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
function collectThing(node: ASTNode): ThingSymbol {
|
|
155
|
+
const data = node.token.data;
|
|
156
|
+
if (data.type !== 'thing_decl') throw new Error('Expected thing_decl');
|
|
157
|
+
|
|
158
|
+
const thing: ThingSymbol = {
|
|
159
|
+
name: data.name,
|
|
160
|
+
version: data.version ?? '1.0.0',
|
|
161
|
+
node,
|
|
162
|
+
fields: [],
|
|
163
|
+
states: [],
|
|
164
|
+
tags: [],
|
|
165
|
+
levels: [],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
for (const child of node.children) {
|
|
169
|
+
const cd = child.token.data;
|
|
170
|
+
|
|
171
|
+
switch (cd.type) {
|
|
172
|
+
case 'field_def':
|
|
173
|
+
thing.fields.push(child);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case 'state_decl':
|
|
177
|
+
thing.states.push(child);
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case 'starts_at':
|
|
181
|
+
thing.startsAt = cd.state;
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case 'tagged':
|
|
185
|
+
thing.tags.push(...cd.tags);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case 'section':
|
|
189
|
+
if (cd.name === 'levels') {
|
|
190
|
+
for (const lvl of child.children) {
|
|
191
|
+
if (lvl.token.data.type === 'level_def') {
|
|
192
|
+
thing.levels.push(lvl);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return thing;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// =============================================================================
|
|
204
|
+
// Heuristic: Does a root node have view-like children?
|
|
205
|
+
// =============================================================================
|
|
206
|
+
|
|
207
|
+
function hasViewChildren(node: ASTNode): boolean {
|
|
208
|
+
for (const child of node.children) {
|
|
209
|
+
if (VIEW_CHILD_TYPES.has(child.token.data.type)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiler Utilities — pure string transformation helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert a name to kebab-case slug.
|
|
7
|
+
* "project management" → "project-management"
|
|
8
|
+
* "user stats" → "user-stats"
|
|
9
|
+
*/
|
|
10
|
+
export function slugify(name: string): string {
|
|
11
|
+
return name
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.trim()
|
|
14
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
15
|
+
.replace(/\s+/g, '-')
|
|
16
|
+
.replace(/-+/g, '-');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert a name to snake_case.
|
|
21
|
+
* "total tasks" → "total_tasks"
|
|
22
|
+
* "started at" → "started_at"
|
|
23
|
+
*/
|
|
24
|
+
export function snakeCase(name: string): string {
|
|
25
|
+
return name
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.trim()
|
|
28
|
+
.replace(/[^a-z0-9\s_]/g, '')
|
|
29
|
+
.replace(/\s+/g, '_')
|
|
30
|
+
.replace(/_+/g, '_');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate a deterministic ID from parts.
|
|
35
|
+
* generateId("project", "draft") → "project--draft"
|
|
36
|
+
* generateId("task", "in progress") → "task--in-progress"
|
|
37
|
+
*/
|
|
38
|
+
export function generateId(...parts: string[]): string {
|
|
39
|
+
return parts.map(p => slugify(p)).join('--');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a deterministic action ID.
|
|
44
|
+
* generateActionId("active-on-enter", 0) → "active-on-enter-0"
|
|
45
|
+
*/
|
|
46
|
+
export function generateActionId(context: string, index: number): string {
|
|
47
|
+
return `${context}-${index}`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View Compiler — Pass 3 of compilation.
|
|
3
|
+
*
|
|
4
|
+
* Transforms fragments and views → IRExperienceDefinition with ExperienceNode trees.
|
|
5
|
+
* Handles data sources, content, iteration, layout, and bindings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ASTNode } from '../types';
|
|
9
|
+
import type {
|
|
10
|
+
IRExperienceDefinition,
|
|
11
|
+
IRExperienceNode,
|
|
12
|
+
IRWorkflowDataSource,
|
|
13
|
+
IRDataSource,
|
|
14
|
+
CompilerError,
|
|
15
|
+
} from '../ir-types';
|
|
16
|
+
import type { SymbolTable, SpaceSymbol } from './symbol-table';
|
|
17
|
+
import { slugify, snakeCase, generateId } from './utils';
|
|
18
|
+
import {
|
|
19
|
+
mapContent,
|
|
20
|
+
mapStringLiteral,
|
|
21
|
+
mapIteration,
|
|
22
|
+
mapSection,
|
|
23
|
+
mapSearch,
|
|
24
|
+
mapNavigation,
|
|
25
|
+
mapPages,
|
|
26
|
+
} from './component-mapper';
|
|
27
|
+
|
|
28
|
+
// =============================================================================
|
|
29
|
+
// Public API
|
|
30
|
+
// =============================================================================
|
|
31
|
+
|
|
32
|
+
export function compileViews(
|
|
33
|
+
symbols: SymbolTable,
|
|
34
|
+
): { experiences: IRExperienceDefinition[]; errors: CompilerError[] } {
|
|
35
|
+
const experiences: IRExperienceDefinition[] = [];
|
|
36
|
+
const errors: CompilerError[] = [];
|
|
37
|
+
|
|
38
|
+
// Compile fragments
|
|
39
|
+
for (const [, fragment] of symbols.fragments) {
|
|
40
|
+
const exp = compileViewNode(
|
|
41
|
+
fragment.name,
|
|
42
|
+
fragment.node,
|
|
43
|
+
symbols.space,
|
|
44
|
+
);
|
|
45
|
+
experiences.push(exp);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Compile views
|
|
49
|
+
for (const [, view] of symbols.views) {
|
|
50
|
+
const exp = compileViewNode(
|
|
51
|
+
view.name,
|
|
52
|
+
view.node,
|
|
53
|
+
symbols.space,
|
|
54
|
+
);
|
|
55
|
+
experiences.push(exp);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { experiences, errors };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// View Compilation
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
function compileViewNode(
|
|
66
|
+
name: string,
|
|
67
|
+
node: ASTNode,
|
|
68
|
+
space?: SpaceSymbol,
|
|
69
|
+
): IRExperienceDefinition {
|
|
70
|
+
const viewSlug = slugify(name);
|
|
71
|
+
const dataSources: IRDataSource[] = [];
|
|
72
|
+
const workflowSlugs: string[] = [];
|
|
73
|
+
|
|
74
|
+
// First pass: collect data sources
|
|
75
|
+
for (const child of node.children) {
|
|
76
|
+
if (child.token.data.type === 'data_source') {
|
|
77
|
+
const ds = compileDataSource(child);
|
|
78
|
+
dataSources.push(ds);
|
|
79
|
+
if (ds.type === 'workflow' && ds.slug) {
|
|
80
|
+
workflowSlugs.push(ds.slug);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Second pass: build ExperienceNode tree from remaining children
|
|
86
|
+
const children = compileChildren(node.children, viewSlug, false);
|
|
87
|
+
|
|
88
|
+
// Build the root node
|
|
89
|
+
const rootNode: IRExperienceNode = {
|
|
90
|
+
id: viewSlug,
|
|
91
|
+
children: children.length > 0 ? children : undefined,
|
|
92
|
+
dataSources: dataSources.length > 0 ? dataSources : undefined,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (children.length > 1) {
|
|
96
|
+
rootNode.layout = 'stack';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
slug: viewSlug,
|
|
101
|
+
version: space?.version ?? '1.0.0',
|
|
102
|
+
name,
|
|
103
|
+
category: 'purpose',
|
|
104
|
+
view_definition: rootNode,
|
|
105
|
+
workflows: workflowSlugs,
|
|
106
|
+
children: [],
|
|
107
|
+
data_bindings: [],
|
|
108
|
+
is_default: false,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// Data Source Compilation
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
function compileDataSource(node: ASTNode): IRWorkflowDataSource {
|
|
117
|
+
const data = node.token.data;
|
|
118
|
+
if (data.type !== 'data_source') throw new Error('Expected data_source');
|
|
119
|
+
|
|
120
|
+
// Determine query type from alias pronoun
|
|
121
|
+
const query = resolveQueryType(data.alias);
|
|
122
|
+
|
|
123
|
+
// Strip pronoun from alias to get the name
|
|
124
|
+
const name = stripPronoun(data.alias);
|
|
125
|
+
|
|
126
|
+
// Slug from source
|
|
127
|
+
const slug = slugify(data.source);
|
|
128
|
+
|
|
129
|
+
const ds: IRWorkflowDataSource = {
|
|
130
|
+
type: 'workflow',
|
|
131
|
+
name,
|
|
132
|
+
slug,
|
|
133
|
+
query,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Scope
|
|
137
|
+
if (data.scope) {
|
|
138
|
+
ds.parentInstanceId = '{{ parent_instance_id }}';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Process qualifier children
|
|
142
|
+
for (const child of node.children) {
|
|
143
|
+
const cd = child.token.data;
|
|
144
|
+
if (cd.type === 'qualifier') {
|
|
145
|
+
applyQualifier(ds, cd);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return ds;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function resolveQueryType(alias: string): 'latest' | 'list' | 'count' {
|
|
153
|
+
const firstWord = alias.split(/\s+/)[0];
|
|
154
|
+
if (firstWord === 'this') return 'latest';
|
|
155
|
+
// my, its, these → list
|
|
156
|
+
return 'list';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function stripPronoun(alias: string): string {
|
|
160
|
+
const pronouns = ['my', 'this', 'its', 'these'];
|
|
161
|
+
const words = alias.split(/\s+/);
|
|
162
|
+
if (words.length > 1 && pronouns.includes(words[0])) {
|
|
163
|
+
return words.slice(1).join(' ');
|
|
164
|
+
}
|
|
165
|
+
return alias;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function applyQualifier(
|
|
169
|
+
ds: IRWorkflowDataSource,
|
|
170
|
+
qualifier: { kind: string; value: string },
|
|
171
|
+
): void {
|
|
172
|
+
switch (qualifier.kind) {
|
|
173
|
+
case 'order':
|
|
174
|
+
if (qualifier.value === 'newest') {
|
|
175
|
+
ds.sort = 'created_at:desc';
|
|
176
|
+
} else if (qualifier.value === 'oldest') {
|
|
177
|
+
ds.sort = 'created_at:asc';
|
|
178
|
+
} else {
|
|
179
|
+
ds.sort = `${snakeCase(qualifier.value)}:desc`;
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case 'pagination':
|
|
184
|
+
ds.paginated = true;
|
|
185
|
+
ds.pageSize = parseInt(qualifier.value, 10);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
case 'searchable':
|
|
189
|
+
ds.searchFields = qualifier.value
|
|
190
|
+
.split(/\s+and\s+/)
|
|
191
|
+
.map(f => snakeCase(f.trim()));
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case 'filterable':
|
|
195
|
+
ds.facets = qualifier.value
|
|
196
|
+
.split(/\s+and\s+/)
|
|
197
|
+
.map(f => snakeCase(f.trim()));
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// =============================================================================
|
|
203
|
+
// Children Compilation
|
|
204
|
+
// =============================================================================
|
|
205
|
+
|
|
206
|
+
function compileChildren(
|
|
207
|
+
children: ASTNode[],
|
|
208
|
+
parentId: string,
|
|
209
|
+
insideEach: boolean,
|
|
210
|
+
): IRExperienceNode[] {
|
|
211
|
+
const nodes: IRExperienceNode[] = [];
|
|
212
|
+
let childIndex = 0;
|
|
213
|
+
|
|
214
|
+
for (const child of children) {
|
|
215
|
+
const cd = child.token.data;
|
|
216
|
+
|
|
217
|
+
switch (cd.type) {
|
|
218
|
+
case 'data_source':
|
|
219
|
+
// Data sources are handled at the root level, skip here
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case 'qualifier':
|
|
223
|
+
// Qualifiers attach to their parent data source, skip here
|
|
224
|
+
break;
|
|
225
|
+
|
|
226
|
+
case 'content':
|
|
227
|
+
nodes.push(mapContent(cd, insideEach, parentId, childIndex++));
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case 'string_literal': {
|
|
231
|
+
const strNode = mapStringLiteral(cd, parentId, childIndex++);
|
|
232
|
+
// If string literal has children, they're nested under it
|
|
233
|
+
if (child.children.length > 0) {
|
|
234
|
+
strNode.children = compileChildren(child.children, strNode.id, insideEach);
|
|
235
|
+
if (strNode.children.length > 1) {
|
|
236
|
+
strNode.layout = 'stack';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
nodes.push(strNode);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case 'iteration': {
|
|
244
|
+
const iterChildren = compileChildren(child.children, generateId(parentId, 'each', cd.subject), true);
|
|
245
|
+
nodes.push(mapIteration(cd, iterChildren, parentId, childIndex++));
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'section': {
|
|
250
|
+
const sectionChildren = compileChildren(child.children, generateId(parentId, cd.name), insideEach);
|
|
251
|
+
nodes.push(mapSection(cd, sectionChildren, parentId));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case 'search':
|
|
256
|
+
nodes.push(mapSearch(cd, parentId, childIndex++));
|
|
257
|
+
break;
|
|
258
|
+
|
|
259
|
+
case 'navigation':
|
|
260
|
+
nodes.push(mapNavigation(cd, insideEach, parentId, childIndex++));
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
case 'pages':
|
|
264
|
+
nodes.push(mapPages(parentId, childIndex++));
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case 'grouping':
|
|
268
|
+
// grouping affects data source rather than creating a node
|
|
269
|
+
// but we record it as a node for structural completeness
|
|
270
|
+
nodes.push({
|
|
271
|
+
id: generateId(parentId, 'group', cd.collection),
|
|
272
|
+
config: {
|
|
273
|
+
groupBy: snakeCase(cd.key),
|
|
274
|
+
collection: cd.collection,
|
|
275
|
+
},
|
|
276
|
+
children: compileChildren(child.children, generateId(parentId, 'group', cd.collection), insideEach),
|
|
277
|
+
});
|
|
278
|
+
break;
|
|
279
|
+
|
|
280
|
+
default:
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return nodes;
|
|
286
|
+
}
|