@player-ui/check-path-plugin 0.0.1-next.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.
@@ -0,0 +1,220 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var view = require('@player-ui/view');
6
+ var partialMatchRegistry = require('@player-ui/partial-match-registry');
7
+ var dlv = require('dlv');
8
+
9
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
10
+
11
+ var dlv__default = /*#__PURE__*/_interopDefaultLegacy(dlv);
12
+
13
+ function createMatcher(match) {
14
+ if (typeof match === "string" || typeof match === "number") {
15
+ return partialMatchRegistry.createObjectMatcher({ type: match });
16
+ }
17
+ if (typeof match === "function") {
18
+ return match;
19
+ }
20
+ return partialMatchRegistry.createObjectMatcher(match);
21
+ }
22
+ function getParent(node, viewInfo) {
23
+ var _a;
24
+ let working = node;
25
+ while (working.parent && working.parent.type !== view.NodeType.Asset && working.parent.type !== view.NodeType.View) {
26
+ working = working.parent;
27
+ }
28
+ const { parent } = working;
29
+ if (parent && (parent.type === view.NodeType.Asset || parent.type === view.NodeType.View)) {
30
+ return (_a = viewInfo.resolver.getSourceNode(parent)) != null ? _a : parent;
31
+ }
32
+ }
33
+ class CheckPathPlugin {
34
+ constructor() {
35
+ this.name = "check-path";
36
+ }
37
+ apply(player) {
38
+ player.hooks.viewController.tap(this.name, (viewController) => {
39
+ viewController.hooks.view.tap(this.name, (view$1) => {
40
+ view$1.hooks.resolver.tap(this.name, (resolver) => {
41
+ const viewInfo = {
42
+ resolvedMap: new Map(),
43
+ assetIdMap: new Map(),
44
+ resolver
45
+ };
46
+ this.viewInfo = viewInfo;
47
+ resolver.hooks.afterResolve.tap(this.name, (value, node) => {
48
+ const sourceNode = resolver.getSourceNode(node);
49
+ if (sourceNode) {
50
+ viewInfo.resolvedMap.set(sourceNode, {
51
+ resolved: node,
52
+ value
53
+ });
54
+ if (node.type === view.NodeType.Asset || node.type === view.NodeType.View) {
55
+ const id = dlv__default["default"](value, "id");
56
+ if (id) {
57
+ viewInfo.assetIdMap.set(id, node);
58
+ }
59
+ }
60
+ }
61
+ return value;
62
+ });
63
+ });
64
+ });
65
+ });
66
+ }
67
+ getParent(id, query) {
68
+ var _a;
69
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
70
+ if (!assetNode || !this.viewInfo) {
71
+ return void 0;
72
+ }
73
+ let potentialMatch = getParent(assetNode, this.viewInfo);
74
+ if (query === void 0) {
75
+ if (potentialMatch) {
76
+ const resolved = this.viewInfo.resolvedMap.get(potentialMatch);
77
+ return resolved == null ? void 0 : resolved.value;
78
+ }
79
+ return;
80
+ }
81
+ const queryArray = Array.isArray(query) ? [...query] : [query];
82
+ let parentQuery = queryArray.shift();
83
+ let depth = 0;
84
+ while (potentialMatch && parentQuery) {
85
+ if (depth++ >= 50) {
86
+ throw new Error("Recursion depth exceeded. Check for cycles in the AST graph");
87
+ }
88
+ const matcher = createMatcher(parentQuery);
89
+ const resolved = this.viewInfo.resolvedMap.get(potentialMatch);
90
+ if (resolved && matcher(resolved.value)) {
91
+ if (queryArray.length === 0) {
92
+ return resolved.value;
93
+ }
94
+ parentQuery = queryArray.shift();
95
+ }
96
+ potentialMatch = getParent(potentialMatch, this.viewInfo);
97
+ }
98
+ return void 0;
99
+ }
100
+ getParentProp(id) {
101
+ var _a, _b, _c, _d;
102
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
103
+ if (!assetNode || !this.viewInfo) {
104
+ return;
105
+ }
106
+ let working = assetNode;
107
+ let parent;
108
+ while (working) {
109
+ parent = (working == null ? void 0 : working.parent) && ((_b = this.viewInfo.resolvedMap.get(working.parent)) == null ? void 0 : _b.resolved);
110
+ if (parent && (parent.type === view.NodeType.Asset || parent.type === view.NodeType.View)) {
111
+ break;
112
+ }
113
+ working = working == null ? void 0 : working.parent;
114
+ }
115
+ if (parent && "children" in parent) {
116
+ const childProp = (_c = parent.children) == null ? void 0 : _c.find((child) => child.value === working);
117
+ return (_d = childProp == null ? void 0 : childProp.path) == null ? void 0 : _d[0];
118
+ }
119
+ return void 0;
120
+ }
121
+ hasParentContext(id, query) {
122
+ return Boolean(this.getParent(id, query));
123
+ }
124
+ findChildPath(node, query, includeSelfMatch = true) {
125
+ var _a, _b, _c, _d;
126
+ if (query.length === 0) {
127
+ return true;
128
+ }
129
+ const [first, ...rest] = query;
130
+ const matcher = createMatcher(first);
131
+ if (node.type === view.NodeType.Asset || node.type === view.NodeType.View) {
132
+ const resolved = (_a = this.viewInfo) == null ? void 0 : _a.resolvedMap.get(node);
133
+ const includesSelf = (_b = includeSelfMatch && resolved && matcher(resolved.value)) != null ? _b : false;
134
+ const childQuery = includesSelf ? rest : query;
135
+ if (childQuery.length === 0 && includesSelf) {
136
+ return true;
137
+ }
138
+ if (childQuery.length && (!node.children || node.children.length === 0)) {
139
+ return false;
140
+ }
141
+ if ((_c = node.children) == null ? void 0 : _c.some((childNode) => this.findChildPath(childNode.value, childQuery))) {
142
+ return true;
143
+ }
144
+ } else if (node.type === view.NodeType.MultiNode && node.values.some((childNode) => this.findChildPath(childNode, query))) {
145
+ return true;
146
+ } else if ("children" in node && ((_d = node.children) == null ? void 0 : _d.some((childNode) => this.findChildPath(childNode.value, query)))) {
147
+ return true;
148
+ }
149
+ return false;
150
+ }
151
+ hasChildContext(id, query) {
152
+ var _a;
153
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
154
+ const queryArray = Array.isArray(query) ? [...query] : [query];
155
+ if (!assetNode) {
156
+ return false;
157
+ }
158
+ return this.findChildPath(assetNode, queryArray, false);
159
+ }
160
+ getAsset(id) {
161
+ var _a, _b, _c, _d;
162
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
163
+ if (!assetNode)
164
+ return;
165
+ const sourceNode = (_b = this.viewInfo) == null ? void 0 : _b.resolver.getSourceNode(assetNode);
166
+ if (!sourceNode)
167
+ return;
168
+ return (_d = (_c = this.viewInfo) == null ? void 0 : _c.resolvedMap.get(sourceNode)) == null ? void 0 : _d.value;
169
+ }
170
+ getPath(id, query) {
171
+ var _a, _b;
172
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
173
+ if (!assetNode || !this.viewInfo) {
174
+ return;
175
+ }
176
+ let path = [];
177
+ let queryArray = [];
178
+ if (query) {
179
+ queryArray = Array.isArray(query) ? [...query] : [query];
180
+ }
181
+ let parentQuery = queryArray.shift();
182
+ let working = assetNode;
183
+ const findWorkingChild = (parent) => {
184
+ var _a2;
185
+ return (_a2 = parent.children) == null ? void 0 : _a2.find((n) => n.value === working);
186
+ };
187
+ while (working !== void 0) {
188
+ const parent = (working == null ? void 0 : working.parent) && this.viewInfo.resolvedMap.get(working.parent);
189
+ const parentNode = parent == null ? void 0 : parent.resolved;
190
+ if (parentNode) {
191
+ if (parentNode.type === view.NodeType.MultiNode) {
192
+ const index = parentNode.values.indexOf(working);
193
+ if (index !== -1) {
194
+ const actualIndex = index - parentNode.values.slice(0, index).reduce((undefCount, next) => {
195
+ var _a2, _b2;
196
+ return ((_b2 = (_a2 = this.viewInfo) == null ? void 0 : _a2.resolvedMap.get(next)) == null ? void 0 : _b2.value) === void 0 ? undefCount + 1 : undefCount;
197
+ }, 0);
198
+ path = [actualIndex, ...path];
199
+ }
200
+ } else if ("children" in parentNode) {
201
+ const childProp = findWorkingChild(parentNode);
202
+ path = [...(_b = childProp == null ? void 0 : childProp.path) != null ? _b : [], ...path];
203
+ }
204
+ }
205
+ if (parentQuery) {
206
+ const matcher = createMatcher(parentQuery);
207
+ if (matcher(parent == null ? void 0 : parent.value)) {
208
+ parentQuery = queryArray.shift();
209
+ if (!parentQuery)
210
+ return path;
211
+ }
212
+ }
213
+ working = working.parent;
214
+ }
215
+ return queryArray.length === 0 ? path : void 0;
216
+ }
217
+ }
218
+
219
+ exports.CheckPathPlugin = CheckPathPlugin;
220
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1,54 @@
1
+ import { PlayerPlugin, Player } from '@player-ui/player';
2
+ import { Asset } from '@player-ui/types';
3
+
4
+ declare type QueryFunction = (asset: Asset) => boolean;
5
+ declare type Query = QueryFunction | string | object;
6
+ /**
7
+ * The `check-path-plugin` enables developers to query segments of the view tree for contextual rendering or behavior.
8
+ * This is best suited to be referenced during the UI rendering phase, where one can make decisions about the rendering of an asset based on where it lies in the tree.
9
+ */
10
+ declare class CheckPathPlugin implements PlayerPlugin {
11
+ name: string;
12
+ private viewInfo?;
13
+ apply(player: Player): void;
14
+ /**
15
+ * Starts at the asset with the given id, and walks backwards _up_ the tree until it finds a match for the parent
16
+ *
17
+ * @param id - The id of the asset to _start_ at
18
+ * @param query - A means of matching a parent asset
19
+ * @returns - The parent object if a match is found, else undefined
20
+ */
21
+ getParent(id: string, query?: Query | Array<Query>): Asset | undefined;
22
+ /**
23
+ * Returns the property that the asset resides on relative to it's parent
24
+ *
25
+ * @param id - The id of the asset to _start_ at
26
+ * @returns - The property name or undefined if no parent was found
27
+ */
28
+ getParentProp(id: string): string | number | undefined;
29
+ /**
30
+ * Given the starting node, check to verify that the supplied queries are relevant to the current asset's parents.
31
+ *
32
+ * @param id - The id of the asset to _start_ at
33
+ * @returns - true if the context applies, false if it doesn't
34
+ */
35
+ hasParentContext(id: string, query: Query | Array<Query>): boolean;
36
+ /** Search the node for any matching paths in the graph that match the query */
37
+ private findChildPath;
38
+ /**
39
+ * Given the starting node, check to verify that the supplied queries are relevant to the current asset's children.
40
+ *
41
+ * @param id - The id of the asset to _start_ at
42
+ * @returns - true if the context applies, false if it doesn't
43
+ */
44
+ hasChildContext(id: string, query: Query | Array<Query>): boolean;
45
+ /** Get the asset represented by id */
46
+ getAsset(id: string): Asset | undefined;
47
+ /**
48
+ * Get the path of the asset in the view upto
49
+ * the asset that matches the query or to the view if no query is provided
50
+ */
51
+ getPath(id: string, query?: Query | Array<Query>): Array<string | number> | undefined;
52
+ }
53
+
54
+ export { CheckPathPlugin, Query, QueryFunction };
@@ -0,0 +1,212 @@
1
+ import { NodeType } from '@player-ui/view';
2
+ import { createObjectMatcher } from '@player-ui/partial-match-registry';
3
+ import dlv from 'dlv';
4
+
5
+ function createMatcher(match) {
6
+ if (typeof match === "string" || typeof match === "number") {
7
+ return createObjectMatcher({ type: match });
8
+ }
9
+ if (typeof match === "function") {
10
+ return match;
11
+ }
12
+ return createObjectMatcher(match);
13
+ }
14
+ function getParent(node, viewInfo) {
15
+ var _a;
16
+ let working = node;
17
+ while (working.parent && working.parent.type !== NodeType.Asset && working.parent.type !== NodeType.View) {
18
+ working = working.parent;
19
+ }
20
+ const { parent } = working;
21
+ if (parent && (parent.type === NodeType.Asset || parent.type === NodeType.View)) {
22
+ return (_a = viewInfo.resolver.getSourceNode(parent)) != null ? _a : parent;
23
+ }
24
+ }
25
+ class CheckPathPlugin {
26
+ constructor() {
27
+ this.name = "check-path";
28
+ }
29
+ apply(player) {
30
+ player.hooks.viewController.tap(this.name, (viewController) => {
31
+ viewController.hooks.view.tap(this.name, (view) => {
32
+ view.hooks.resolver.tap(this.name, (resolver) => {
33
+ const viewInfo = {
34
+ resolvedMap: new Map(),
35
+ assetIdMap: new Map(),
36
+ resolver
37
+ };
38
+ this.viewInfo = viewInfo;
39
+ resolver.hooks.afterResolve.tap(this.name, (value, node) => {
40
+ const sourceNode = resolver.getSourceNode(node);
41
+ if (sourceNode) {
42
+ viewInfo.resolvedMap.set(sourceNode, {
43
+ resolved: node,
44
+ value
45
+ });
46
+ if (node.type === NodeType.Asset || node.type === NodeType.View) {
47
+ const id = dlv(value, "id");
48
+ if (id) {
49
+ viewInfo.assetIdMap.set(id, node);
50
+ }
51
+ }
52
+ }
53
+ return value;
54
+ });
55
+ });
56
+ });
57
+ });
58
+ }
59
+ getParent(id, query) {
60
+ var _a;
61
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
62
+ if (!assetNode || !this.viewInfo) {
63
+ return void 0;
64
+ }
65
+ let potentialMatch = getParent(assetNode, this.viewInfo);
66
+ if (query === void 0) {
67
+ if (potentialMatch) {
68
+ const resolved = this.viewInfo.resolvedMap.get(potentialMatch);
69
+ return resolved == null ? void 0 : resolved.value;
70
+ }
71
+ return;
72
+ }
73
+ const queryArray = Array.isArray(query) ? [...query] : [query];
74
+ let parentQuery = queryArray.shift();
75
+ let depth = 0;
76
+ while (potentialMatch && parentQuery) {
77
+ if (depth++ >= 50) {
78
+ throw new Error("Recursion depth exceeded. Check for cycles in the AST graph");
79
+ }
80
+ const matcher = createMatcher(parentQuery);
81
+ const resolved = this.viewInfo.resolvedMap.get(potentialMatch);
82
+ if (resolved && matcher(resolved.value)) {
83
+ if (queryArray.length === 0) {
84
+ return resolved.value;
85
+ }
86
+ parentQuery = queryArray.shift();
87
+ }
88
+ potentialMatch = getParent(potentialMatch, this.viewInfo);
89
+ }
90
+ return void 0;
91
+ }
92
+ getParentProp(id) {
93
+ var _a, _b, _c, _d;
94
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
95
+ if (!assetNode || !this.viewInfo) {
96
+ return;
97
+ }
98
+ let working = assetNode;
99
+ let parent;
100
+ while (working) {
101
+ parent = (working == null ? void 0 : working.parent) && ((_b = this.viewInfo.resolvedMap.get(working.parent)) == null ? void 0 : _b.resolved);
102
+ if (parent && (parent.type === NodeType.Asset || parent.type === NodeType.View)) {
103
+ break;
104
+ }
105
+ working = working == null ? void 0 : working.parent;
106
+ }
107
+ if (parent && "children" in parent) {
108
+ const childProp = (_c = parent.children) == null ? void 0 : _c.find((child) => child.value === working);
109
+ return (_d = childProp == null ? void 0 : childProp.path) == null ? void 0 : _d[0];
110
+ }
111
+ return void 0;
112
+ }
113
+ hasParentContext(id, query) {
114
+ return Boolean(this.getParent(id, query));
115
+ }
116
+ findChildPath(node, query, includeSelfMatch = true) {
117
+ var _a, _b, _c, _d;
118
+ if (query.length === 0) {
119
+ return true;
120
+ }
121
+ const [first, ...rest] = query;
122
+ const matcher = createMatcher(first);
123
+ if (node.type === NodeType.Asset || node.type === NodeType.View) {
124
+ const resolved = (_a = this.viewInfo) == null ? void 0 : _a.resolvedMap.get(node);
125
+ const includesSelf = (_b = includeSelfMatch && resolved && matcher(resolved.value)) != null ? _b : false;
126
+ const childQuery = includesSelf ? rest : query;
127
+ if (childQuery.length === 0 && includesSelf) {
128
+ return true;
129
+ }
130
+ if (childQuery.length && (!node.children || node.children.length === 0)) {
131
+ return false;
132
+ }
133
+ if ((_c = node.children) == null ? void 0 : _c.some((childNode) => this.findChildPath(childNode.value, childQuery))) {
134
+ return true;
135
+ }
136
+ } else if (node.type === NodeType.MultiNode && node.values.some((childNode) => this.findChildPath(childNode, query))) {
137
+ return true;
138
+ } else if ("children" in node && ((_d = node.children) == null ? void 0 : _d.some((childNode) => this.findChildPath(childNode.value, query)))) {
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ hasChildContext(id, query) {
144
+ var _a;
145
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
146
+ const queryArray = Array.isArray(query) ? [...query] : [query];
147
+ if (!assetNode) {
148
+ return false;
149
+ }
150
+ return this.findChildPath(assetNode, queryArray, false);
151
+ }
152
+ getAsset(id) {
153
+ var _a, _b, _c, _d;
154
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
155
+ if (!assetNode)
156
+ return;
157
+ const sourceNode = (_b = this.viewInfo) == null ? void 0 : _b.resolver.getSourceNode(assetNode);
158
+ if (!sourceNode)
159
+ return;
160
+ return (_d = (_c = this.viewInfo) == null ? void 0 : _c.resolvedMap.get(sourceNode)) == null ? void 0 : _d.value;
161
+ }
162
+ getPath(id, query) {
163
+ var _a, _b;
164
+ const assetNode = (_a = this.viewInfo) == null ? void 0 : _a.assetIdMap.get(id);
165
+ if (!assetNode || !this.viewInfo) {
166
+ return;
167
+ }
168
+ let path = [];
169
+ let queryArray = [];
170
+ if (query) {
171
+ queryArray = Array.isArray(query) ? [...query] : [query];
172
+ }
173
+ let parentQuery = queryArray.shift();
174
+ let working = assetNode;
175
+ const findWorkingChild = (parent) => {
176
+ var _a2;
177
+ return (_a2 = parent.children) == null ? void 0 : _a2.find((n) => n.value === working);
178
+ };
179
+ while (working !== void 0) {
180
+ const parent = (working == null ? void 0 : working.parent) && this.viewInfo.resolvedMap.get(working.parent);
181
+ const parentNode = parent == null ? void 0 : parent.resolved;
182
+ if (parentNode) {
183
+ if (parentNode.type === NodeType.MultiNode) {
184
+ const index = parentNode.values.indexOf(working);
185
+ if (index !== -1) {
186
+ const actualIndex = index - parentNode.values.slice(0, index).reduce((undefCount, next) => {
187
+ var _a2, _b2;
188
+ return ((_b2 = (_a2 = this.viewInfo) == null ? void 0 : _a2.resolvedMap.get(next)) == null ? void 0 : _b2.value) === void 0 ? undefCount + 1 : undefCount;
189
+ }, 0);
190
+ path = [actualIndex, ...path];
191
+ }
192
+ } else if ("children" in parentNode) {
193
+ const childProp = findWorkingChild(parentNode);
194
+ path = [...(_b = childProp == null ? void 0 : childProp.path) != null ? _b : [], ...path];
195
+ }
196
+ }
197
+ if (parentQuery) {
198
+ const matcher = createMatcher(parentQuery);
199
+ if (matcher(parent == null ? void 0 : parent.value)) {
200
+ parentQuery = queryArray.shift();
201
+ if (!parentQuery)
202
+ return path;
203
+ }
204
+ }
205
+ working = working.parent;
206
+ }
207
+ return queryArray.length === 0 ? path : void 0;
208
+ }
209
+ }
210
+
211
+ export { CheckPathPlugin };
212
+ //# sourceMappingURL=index.esm.js.map
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@player-ui/check-path-plugin",
3
+ "version": "0.0.1-next.1",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "registry": "https://registry.npmjs.org"
7
+ },
8
+ "peerDependencies": {
9
+ "@player-ui/binding-grammar": "0.0.1-next.1"
10
+ },
11
+ "dependencies": {
12
+ "@player-ui/partial-match-registry": "0.0.1-next.1",
13
+ "dlv": "^1.1.3",
14
+ "tapable": "1.1.3",
15
+ "@types/tapable": "^1.0.5",
16
+ "@babel/runtime": "7.15.4"
17
+ },
18
+ "main": "dist/index.cjs.js",
19
+ "module": "dist/index.esm.js",
20
+ "typings": "dist/index.d.ts"
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,392 @@
1
+ import type { Player, PlayerPlugin } from '@player-ui/player';
2
+ import type { Node, Resolver } from '@player-ui/view';
3
+ import { NodeType } from '@player-ui/view';
4
+ import type { Asset } from '@player-ui/types';
5
+ import { createObjectMatcher } from '@player-ui/partial-match-registry';
6
+ import dlv from 'dlv';
7
+
8
+ export type QueryFunction = (asset: Asset) => boolean;
9
+ export type Query = QueryFunction | string | object;
10
+
11
+ /** Generate a function that matches on the given input */
12
+ function createMatcher(
13
+ match: number | string | object | QueryFunction
14
+ ): QueryFunction {
15
+ if (typeof match === 'string' || typeof match === 'number') {
16
+ return createObjectMatcher({ type: match });
17
+ }
18
+
19
+ if (typeof match === 'function') {
20
+ return match as QueryFunction;
21
+ }
22
+
23
+ return createObjectMatcher(match);
24
+ }
25
+
26
+ interface ViewInfo {
27
+ /** The root of the view graph */
28
+ root?: Node.Node;
29
+
30
+ /** A cache of an asset or view's id to it's node */
31
+ assetIdMap: Map<string, Node.Asset | Node.View>;
32
+
33
+ /** A map of a node to it's resolved node and value */
34
+ resolvedMap: Map<
35
+ Node.Node,
36
+ {
37
+ /** The final resolved AST node */
38
+ resolved: Node.Node;
39
+
40
+ /** The final, resolved value of the node */
41
+ value: any;
42
+ }
43
+ >;
44
+
45
+ /** The resolver instance tied to this view. Used to map back to original nodes */
46
+ resolver: Resolver;
47
+ }
48
+
49
+ /**
50
+ * Traverse up the tree until reaching the first asset or view
51
+ * Returns undefined if no matching parent is found
52
+ */
53
+ function getParent(
54
+ node: Node.Node,
55
+ viewInfo: ViewInfo
56
+ ): Node.ViewOrAsset | undefined {
57
+ let working = node;
58
+
59
+ while (
60
+ working.parent &&
61
+ working.parent.type !== NodeType.Asset &&
62
+ working.parent.type !== NodeType.View
63
+ ) {
64
+ working = working.parent;
65
+ }
66
+
67
+ const { parent } = working;
68
+
69
+ if (
70
+ parent &&
71
+ (parent.type === NodeType.Asset || parent.type === NodeType.View)
72
+ ) {
73
+ return (viewInfo.resolver.getSourceNode(parent) ??
74
+ parent) as Node.ViewOrAsset;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * The `check-path-plugin` enables developers to query segments of the view tree for contextual rendering or behavior.
80
+ * This is best suited to be referenced during the UI rendering phase, where one can make decisions about the rendering of an asset based on where it lies in the tree.
81
+ */
82
+ export class CheckPathPlugin implements PlayerPlugin {
83
+ name = 'check-path';
84
+ private viewInfo?: ViewInfo;
85
+
86
+ apply(player: Player) {
87
+ player.hooks.viewController.tap(this.name, (viewController) => {
88
+ viewController.hooks.view.tap(this.name, (view) => {
89
+ view.hooks.resolver.tap(this.name, (resolver: Resolver) => {
90
+ const viewInfo: ViewInfo = {
91
+ resolvedMap: new Map(),
92
+ assetIdMap: new Map(),
93
+ resolver,
94
+ };
95
+ this.viewInfo = viewInfo;
96
+
97
+ resolver.hooks.afterResolve.tap(this.name, (value, node) => {
98
+ const sourceNode = resolver.getSourceNode(node);
99
+
100
+ if (sourceNode) {
101
+ viewInfo.resolvedMap.set(sourceNode, {
102
+ resolved: node,
103
+ value,
104
+ });
105
+
106
+ if (node.type === NodeType.Asset || node.type === NodeType.View) {
107
+ const id = dlv(value, 'id');
108
+
109
+ if (id) {
110
+ viewInfo.assetIdMap.set(id, node);
111
+ }
112
+ }
113
+ }
114
+
115
+ return value;
116
+ });
117
+ });
118
+ });
119
+ });
120
+ }
121
+
122
+ /**
123
+ * Starts at the asset with the given id, and walks backwards _up_ the tree until it finds a match for the parent
124
+ *
125
+ * @param id - The id of the asset to _start_ at
126
+ * @param query - A means of matching a parent asset
127
+ * @returns - The parent object if a match is found, else undefined
128
+ */
129
+ public getParent(
130
+ id: string,
131
+ query?: Query | Array<Query>
132
+ ): Asset | undefined {
133
+ const assetNode = this.viewInfo?.assetIdMap.get(id);
134
+
135
+ if (!assetNode || !this.viewInfo) {
136
+ return undefined;
137
+ }
138
+
139
+ let potentialMatch = getParent(assetNode, this.viewInfo);
140
+
141
+ // Handle the case of an empty query (just get the immediate parent)
142
+ if (query === undefined) {
143
+ if (potentialMatch) {
144
+ const resolved = this.viewInfo.resolvedMap.get(potentialMatch);
145
+
146
+ return resolved?.value;
147
+ }
148
+
149
+ return;
150
+ }
151
+
152
+ const queryArray = Array.isArray(query) ? [...query] : [query];
153
+ let parentQuery = queryArray.shift();
154
+
155
+ // Keep track of the recursive depth in case we loop forever
156
+ let depth = 0;
157
+
158
+ while (potentialMatch && parentQuery) {
159
+ if (depth++ >= 50) {
160
+ throw new Error(
161
+ 'Recursion depth exceeded. Check for cycles in the AST graph'
162
+ );
163
+ }
164
+
165
+ const matcher = createMatcher(parentQuery);
166
+ const resolved = this.viewInfo.resolvedMap.get(potentialMatch);
167
+
168
+ if (resolved && matcher(resolved.value)) {
169
+ // This is the last match.
170
+ if (queryArray.length === 0) {
171
+ return resolved.value;
172
+ }
173
+
174
+ parentQuery = queryArray.shift();
175
+ }
176
+
177
+ potentialMatch = getParent(potentialMatch, this.viewInfo);
178
+ }
179
+
180
+ return undefined;
181
+ }
182
+
183
+ /**
184
+ * Returns the property that the asset resides on relative to it's parent
185
+ *
186
+ * @param id - The id of the asset to _start_ at
187
+ * @returns - The property name or undefined if no parent was found
188
+ */
189
+ public getParentProp(id: string): string | number | undefined {
190
+ const assetNode = this.viewInfo?.assetIdMap.get(id);
191
+
192
+ if (!assetNode || !this.viewInfo) {
193
+ return;
194
+ }
195
+
196
+ let working: Node.Node | undefined = assetNode;
197
+ let parent;
198
+
199
+ while (working) {
200
+ parent =
201
+ working?.parent &&
202
+ this.viewInfo.resolvedMap.get(working.parent)?.resolved;
203
+
204
+ if (
205
+ parent &&
206
+ (parent.type === NodeType.Asset || parent.type === NodeType.View)
207
+ ) {
208
+ break;
209
+ }
210
+
211
+ working = working?.parent;
212
+ }
213
+
214
+ if (parent && 'children' in parent) {
215
+ const childProp = parent.children?.find(
216
+ (child) => child.value === working
217
+ );
218
+
219
+ return childProp?.path?.[0];
220
+ }
221
+
222
+ return undefined;
223
+ }
224
+
225
+ /**
226
+ * Given the starting node, check to verify that the supplied queries are relevant to the current asset's parents.
227
+ *
228
+ * @param id - The id of the asset to _start_ at
229
+ * @returns - true if the context applies, false if it doesn't
230
+ */
231
+ public hasParentContext(id: string, query: Query | Array<Query>): boolean {
232
+ return Boolean(this.getParent(id, query));
233
+ }
234
+
235
+ /** Search the node for any matching paths in the graph that match the query */
236
+ private findChildPath(
237
+ node: Node.Node,
238
+ query: Array<Query>,
239
+ includeSelfMatch = true
240
+ ): boolean {
241
+ if (query.length === 0) {
242
+ return true;
243
+ }
244
+
245
+ const [first, ...rest] = query;
246
+ const matcher = createMatcher(first);
247
+
248
+ if (node.type === NodeType.Asset || node.type === NodeType.View) {
249
+ const resolved = this.viewInfo?.resolvedMap.get(node);
250
+ const includesSelf =
251
+ (includeSelfMatch && resolved && matcher(resolved.value)) ?? false;
252
+ const childQuery = includesSelf ? rest : query;
253
+
254
+ if (childQuery.length === 0 && includesSelf) {
255
+ return true;
256
+ }
257
+
258
+ if (childQuery.length && (!node.children || node.children.length === 0)) {
259
+ return false;
260
+ }
261
+
262
+ if (
263
+ node.children?.some((childNode) =>
264
+ this.findChildPath(childNode.value, childQuery)
265
+ )
266
+ ) {
267
+ return true;
268
+ }
269
+ } else if (
270
+ node.type === NodeType.MultiNode &&
271
+ node.values.some((childNode) => this.findChildPath(childNode, query))
272
+ ) {
273
+ return true;
274
+ } else if (
275
+ 'children' in node &&
276
+ node.children?.some((childNode) =>
277
+ this.findChildPath(childNode.value, query)
278
+ )
279
+ ) {
280
+ return true;
281
+ }
282
+
283
+ return false;
284
+ }
285
+
286
+ /**
287
+ * Given the starting node, check to verify that the supplied queries are relevant to the current asset's children.
288
+ *
289
+ * @param id - The id of the asset to _start_ at
290
+ * @returns - true if the context applies, false if it doesn't
291
+ */
292
+ public hasChildContext(id: string, query: Query | Array<Query>): boolean {
293
+ const assetNode = this.viewInfo?.assetIdMap.get(id);
294
+ const queryArray = Array.isArray(query) ? [...query] : [query];
295
+
296
+ if (!assetNode) {
297
+ return false;
298
+ }
299
+
300
+ return this.findChildPath(assetNode, queryArray, false);
301
+ }
302
+
303
+ /** Get the asset represented by id */
304
+ public getAsset(id: string): Asset | undefined {
305
+ const assetNode = this.viewInfo?.assetIdMap.get(id);
306
+ if (!assetNode) return;
307
+
308
+ const sourceNode = this.viewInfo?.resolver.getSourceNode(assetNode);
309
+ if (!sourceNode) return;
310
+
311
+ return this.viewInfo?.resolvedMap.get(sourceNode)?.value;
312
+ }
313
+
314
+ /**
315
+ * Get the path of the asset in the view upto
316
+ * the asset that matches the query or to the view if no query is provided
317
+ */
318
+ public getPath(
319
+ id: string,
320
+ query?: Query | Array<Query>
321
+ ): Array<string | number> | undefined {
322
+ const assetNode = this.viewInfo?.assetIdMap.get(id);
323
+
324
+ if (!assetNode || !this.viewInfo) {
325
+ return;
326
+ }
327
+
328
+ let path: Array<string | number> = [];
329
+
330
+ let queryArray: Query[] = [];
331
+
332
+ if (query) {
333
+ queryArray = Array.isArray(query) ? [...query] : [query];
334
+ }
335
+
336
+ let parentQuery = queryArray.shift();
337
+
338
+ let working: Node.Node | undefined = assetNode;
339
+
340
+ /** Find the child value for the working value from the given parent */
341
+ const findWorkingChild = (parent: Node.ViewOrAsset | Node.Value) => {
342
+ return parent.children?.find((n) => n.value === working);
343
+ };
344
+
345
+ while (working !== undefined) {
346
+ const parent =
347
+ working?.parent && this.viewInfo.resolvedMap.get(working.parent);
348
+
349
+ const parentNode = parent?.resolved;
350
+
351
+ if (parentNode) {
352
+ if (parentNode.type === NodeType.MultiNode) {
353
+ const index = parentNode.values.indexOf(working);
354
+
355
+ if (index !== -1) {
356
+ const actualIndex =
357
+ index -
358
+ parentNode.values
359
+ .slice(0, index)
360
+ .reduce(
361
+ (undefCount, next) =>
362
+ this.viewInfo?.resolvedMap.get(next)?.value === undefined
363
+ ? undefCount + 1
364
+ : undefCount,
365
+ 0
366
+ );
367
+
368
+ path = [actualIndex, ...path];
369
+ }
370
+ } else if ('children' in parentNode) {
371
+ const childProp = findWorkingChild(parentNode);
372
+ path = [...(childProp?.path ?? []), ...path];
373
+ }
374
+ }
375
+
376
+ if (parentQuery) {
377
+ const matcher = createMatcher(parentQuery);
378
+
379
+ if (matcher(parent?.value)) {
380
+ parentQuery = queryArray.shift();
381
+ if (!parentQuery) return path;
382
+ }
383
+ }
384
+
385
+ working = working.parent;
386
+ }
387
+
388
+ /* if at the end all queries haven't been consumed,
389
+ it means we couldn't find a path till the matching query */
390
+ return queryArray.length === 0 ? path : undefined;
391
+ }
392
+ }