@milaboratories/pl-model-common 1.34.1 → 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dialog/index.d.ts +34 -0
- package/dist/dialog/index.d.ts.map +1 -0
- package/dist/drivers/index.d.ts +1 -1
- package/dist/drivers/pframe/data_types.cjs +1 -0
- package/dist/drivers/pframe/data_types.cjs.map +1 -1
- package/dist/drivers/pframe/data_types.d.ts +27 -1
- package/dist/drivers/pframe/data_types.d.ts.map +1 -1
- package/dist/drivers/pframe/data_types.js +1 -1
- package/dist/drivers/pframe/data_types.js.map +1 -1
- package/dist/drivers/pframe/driver.cjs.map +1 -1
- package/dist/drivers/pframe/driver.d.ts +6 -1
- package/dist/drivers/pframe/driver.d.ts.map +1 -1
- package/dist/drivers/pframe/driver.js.map +1 -1
- package/dist/drivers/pframe/index.d.ts +1 -1
- package/dist/index.cjs +12 -3
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -6
- package/dist/ref.cjs +16 -0
- package/dist/ref.cjs.map +1 -1
- package/dist/ref.d.ts +16 -12
- package/dist/ref.d.ts.map +1 -1
- package/dist/ref.js +15 -1
- package/dist/ref.js.map +1 -1
- package/dist/services/index.cjs +1 -1
- package/dist/services/index.d.ts +3 -3
- package/dist/services/index.js +1 -1
- package/dist/services/{service_injector_factory.js → node_service_handlers.cjs} +6 -5
- package/dist/services/node_service_handlers.cjs.map +1 -0
- package/dist/services/node_service_handlers.d.ts +8 -0
- package/dist/services/node_service_handlers.d.ts.map +1 -0
- package/dist/services/{service_injector_factory.cjs → node_service_handlers.js} +6 -5
- package/dist/services/node_service_handlers.js.map +1 -0
- package/dist/services/service_declarations.cjs +48 -2
- package/dist/services/service_declarations.cjs.map +1 -1
- package/dist/services/service_declarations.d.ts +2 -0
- package/dist/services/service_declarations.d.ts.map +1 -1
- package/dist/services/service_declarations.js +48 -2
- package/dist/services/service_declarations.js.map +1 -1
- package/dist/services/service_injectors.cjs +29 -25
- package/dist/services/service_injectors.cjs.map +1 -1
- package/dist/services/service_injectors.d.ts +21 -10
- package/dist/services/service_injectors.d.ts.map +1 -1
- package/dist/services/service_injectors.js +29 -26
- package/dist/services/service_injectors.js.map +1 -1
- package/dist/services/service_types.cjs +26 -2
- package/dist/services/service_types.cjs.map +1 -1
- package/dist/services/service_types.d.ts +6 -4
- package/dist/services/service_types.d.ts.map +1 -1
- package/dist/services/service_types.js +22 -3
- package/dist/services/service_types.js.map +1 -1
- package/package.json +3 -3
- package/src/dialog/index.ts +33 -0
- package/src/drivers/pframe/data_types.ts +30 -1
- package/src/drivers/pframe/driver.ts +17 -2
- package/src/index.ts +1 -0
- package/src/ref.test.ts +37 -0
- package/src/ref.ts +34 -1
- package/src/services/{service_injector_factory.ts → node_service_handlers.ts} +13 -6
- package/src/services/service_declarations.ts +60 -3
- package/src/services/service_injectors.ts +50 -34
- package/src/services/service_types.ts +38 -4
- package/dist/services/service_injector_factory.cjs.map +0 -1
- package/dist/services/service_injector_factory.d.ts +0 -8
- package/dist/services/service_injector_factory.d.ts.map +0 -1
- package/dist/services/service_injector_factory.js.map +0 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Service declarations — add new services here.
|
|
3
3
|
*
|
|
4
|
+
* `modelMethods` / `uiMethods` lists are the single source of truth for
|
|
5
|
+
* method names exposed on each side. They drive SERVICE_METHOD_MAP (UI
|
|
6
|
+
* ServiceProxy forwarders) and MODEL_SERVICE_METHOD_MAP (workflow VM
|
|
7
|
+
* bridge), so node/main/wasm split no longer needs introspection stubs.
|
|
8
|
+
*
|
|
4
9
|
* After adding a service, fix type errors in these files:
|
|
5
10
|
*
|
|
6
11
|
* Model side:
|
|
@@ -8,18 +13,70 @@
|
|
|
8
13
|
* - lib/node/pl-middle-layer/src/middle_layer/middle_layer.ts — ModelServiceRegistry factory
|
|
9
14
|
*
|
|
10
15
|
* UI side:
|
|
11
|
-
* - lib/model/common/src/services/
|
|
16
|
+
* - lib/model/common/src/services/node_service_handlers.ts — driver method wrappers (node services only)
|
|
12
17
|
* - sdk/ui-vue/src/internal/createAppV3.ts — UiServiceRegistry factory
|
|
13
18
|
*
|
|
19
|
+
* Electron main side (only for `main` services):
|
|
20
|
+
* - platforma-desktop-app/packages/main/src/services/mainServices.ts — main-process dispatcher
|
|
21
|
+
*
|
|
14
22
|
* Optional:
|
|
15
23
|
* - sdk/model/src/services/block_service_flags.ts — only if default-required by all blocks
|
|
16
24
|
*/
|
|
17
25
|
|
|
26
|
+
import type { DialogService } from "../dialog";
|
|
18
27
|
import type { PFrameDriver, PFrameModelDriver } from "../drivers/pframe/driver";
|
|
19
28
|
import type { PFrameSpecDriver } from "../drivers/pframe/spec_driver";
|
|
20
29
|
import { service } from "./service_types";
|
|
21
30
|
|
|
22
31
|
export const Services = {
|
|
23
|
-
PFrameSpec: service<PFrameSpecDriver, PFrameSpecDriver>()({
|
|
24
|
-
|
|
32
|
+
PFrameSpec: service<PFrameSpecDriver, PFrameSpecDriver>()({
|
|
33
|
+
type: "wasm",
|
|
34
|
+
name: "pframeSpec",
|
|
35
|
+
modelMethods: [
|
|
36
|
+
"createSpecFrame",
|
|
37
|
+
"listColumns",
|
|
38
|
+
"discoverColumns",
|
|
39
|
+
"deleteColumn",
|
|
40
|
+
"evaluateQuery",
|
|
41
|
+
"buildQuery",
|
|
42
|
+
"expandAxes",
|
|
43
|
+
"collapseAxes",
|
|
44
|
+
"findAxis",
|
|
45
|
+
"findTableColumn",
|
|
46
|
+
] as const,
|
|
47
|
+
uiMethods: [
|
|
48
|
+
"createSpecFrame",
|
|
49
|
+
"listColumns",
|
|
50
|
+
"discoverColumns",
|
|
51
|
+
"deleteColumn",
|
|
52
|
+
"evaluateQuery",
|
|
53
|
+
"buildQuery",
|
|
54
|
+
"expandAxes",
|
|
55
|
+
"collapseAxes",
|
|
56
|
+
"findAxis",
|
|
57
|
+
"findTableColumn",
|
|
58
|
+
] as const,
|
|
59
|
+
}),
|
|
60
|
+
PFrame: service<PFrameModelDriver, PFrameDriver>()({
|
|
61
|
+
type: "node",
|
|
62
|
+
name: "pframe",
|
|
63
|
+
modelMethods: ["createPFrame", "createPTable", "createPTableV2"] as const,
|
|
64
|
+
uiMethods: [
|
|
65
|
+
"findColumns",
|
|
66
|
+
"getColumnSpec",
|
|
67
|
+
"listColumns",
|
|
68
|
+
"calculateTableData",
|
|
69
|
+
"getUniqueValues",
|
|
70
|
+
"getShape",
|
|
71
|
+
"getSpec",
|
|
72
|
+
"getData",
|
|
73
|
+
"writePTableToFs",
|
|
74
|
+
] as const,
|
|
75
|
+
}),
|
|
76
|
+
Dialog: service<Record<string, never>, DialogService>()({
|
|
77
|
+
type: "main",
|
|
78
|
+
name: "dialog",
|
|
79
|
+
modelMethods: [] as const,
|
|
80
|
+
uiMethods: ["showSaveDialog"] as const,
|
|
81
|
+
}),
|
|
25
82
|
};
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Service dispatch glue — auto-derived types, method maps, and service info builder.
|
|
3
3
|
*
|
|
4
|
-
* The factory (
|
|
5
|
-
* that's the only file to edit when adding a
|
|
4
|
+
* The worker-side handler factory (createNodeServiceHandlers) lives in
|
|
5
|
+
* node_service_handlers.ts — that's the only file to edit when adding a
|
|
6
|
+
* new node service. Method-name registries for UI and model sides are
|
|
7
|
+
* derived directly from the `uiMethods` / `modelMethods` declarations in
|
|
8
|
+
* service_declarations.ts — no factory introspection, no stubs.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import type { InferServiceKind, InferServiceUi, ServiceBrand, ServiceName } from "./service_types";
|
|
9
12
|
import type { DriverKit } from "../driver_kit";
|
|
10
13
|
import { Services } from "./service_declarations";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
14
|
+
import { getServiceModelMethods, getServiceUiMethods } from "./service_types";
|
|
15
|
+
import { resolveRequiredServices } from "./service_capabilities";
|
|
16
|
+
import { createNodeServiceHandlers } from "./node_service_handlers";
|
|
13
17
|
|
|
14
|
-
export {
|
|
18
|
+
export { createNodeServiceHandlers } from "./node_service_handlers";
|
|
15
19
|
|
|
16
20
|
type NodeServiceKeys = {
|
|
17
21
|
[K in keyof typeof Services]: InferServiceKind<ServiceBrand<(typeof Services)[K]>> extends "node"
|
|
@@ -19,54 +23,66 @@ type NodeServiceKeys = {
|
|
|
19
23
|
: never;
|
|
20
24
|
}[keyof typeof Services];
|
|
21
25
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Auto-derived map of node service keys to their UI-facing interfaces.
|
|
28
|
+
* Fulfilled by real driver wrappers in the worker. Main services are
|
|
29
|
+
* dispatched by the Electron main-process router and never reach this
|
|
30
|
+
* map; wasm services are instantiated locally on each consumer.
|
|
31
|
+
*/
|
|
32
|
+
export type NodeServiceHandlerMap = {
|
|
24
33
|
[K in NodeServiceKeys]: InferServiceUi<ServiceBrand<(typeof Services)[K]>>;
|
|
25
34
|
};
|
|
26
35
|
|
|
27
36
|
let cachedKit: DriverKit | undefined;
|
|
28
|
-
let
|
|
37
|
+
let cachedHandlers: NodeServiceHandlerMap | undefined;
|
|
29
38
|
|
|
30
|
-
function
|
|
31
|
-
if (!
|
|
39
|
+
function getOrCreateHandlers(driverKit: DriverKit): NodeServiceHandlerMap {
|
|
40
|
+
if (!cachedHandlers || cachedKit !== driverKit) {
|
|
32
41
|
cachedKit = driverKit;
|
|
33
|
-
|
|
42
|
+
cachedHandlers = createNodeServiceHandlers(driverKit);
|
|
34
43
|
}
|
|
35
|
-
return
|
|
44
|
+
return cachedHandlers;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the worker-side handler object for a given service ID. Caches
|
|
49
|
+
* per DriverKit reference. Called from the worker's IPC dispatch path.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveNodeServiceHandler(
|
|
40
52
|
driverKit: DriverKit,
|
|
41
53
|
serviceId: ServiceName,
|
|
42
|
-
):
|
|
43
|
-
const
|
|
54
|
+
): NodeServiceHandlerMap[keyof NodeServiceHandlerMap] | null {
|
|
55
|
+
const handlers = getOrCreateHandlers(driverKit);
|
|
44
56
|
const key = Object.keys(Services).find(
|
|
45
57
|
(k) => Services[k as keyof typeof Services] === serviceId,
|
|
46
58
|
) as NodeServiceKeys | undefined;
|
|
47
59
|
if (!key) return null;
|
|
48
|
-
return
|
|
60
|
+
return handlers[key];
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* provides a target so the closures in createUiServiceInjectors have own-property
|
|
55
|
-
* keys that getMethodNames can introspect.
|
|
56
|
-
*/
|
|
57
|
-
export const SERVICE_METHOD_MAP: Readonly<Record<string, string[]>> = (() => {
|
|
58
|
-
const stubKit = new Proxy({} as DriverKit, {
|
|
59
|
-
get: () => new Proxy({}, { get: () => () => {} }),
|
|
60
|
-
});
|
|
61
|
-
const injectors = createUiServiceInjectors(stubKit);
|
|
63
|
+
function buildMethodMap(
|
|
64
|
+
pick: (id: ServiceName) => readonly string[],
|
|
65
|
+
): Readonly<Record<string, string[]>> {
|
|
62
66
|
const result: Record<string, string[]> = {};
|
|
63
|
-
for (const
|
|
64
|
-
|
|
65
|
-
const injector = injectors[key as NodeServiceKeys];
|
|
66
|
-
result[serviceId] = injector ? getMethodNames(injector) : [];
|
|
67
|
+
for (const id of Object.values(Services)) {
|
|
68
|
+
result[id as string] = [...pick(id)];
|
|
67
69
|
}
|
|
68
70
|
return result;
|
|
69
|
-
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* UI-side method names per service. Drives ServiceProxy forwarder
|
|
75
|
+
* construction in the renderer.
|
|
76
|
+
*/
|
|
77
|
+
export const SERVICE_METHOD_MAP: Readonly<Record<string, string[]>> =
|
|
78
|
+
buildMethodMap(getServiceUiMethods);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Model-side method names per service. Drives the workflow VM bridge
|
|
82
|
+
* (see pl-middle-layer js_render).
|
|
83
|
+
*/
|
|
84
|
+
export const MODEL_SERVICE_METHOD_MAP: Readonly<Record<string, string[]>> =
|
|
85
|
+
buildMethodMap(getServiceModelMethods);
|
|
70
86
|
|
|
71
87
|
/** Build service info for a block from its feature flags. */
|
|
72
88
|
export function buildServiceInfo(
|
|
@@ -22,19 +22,36 @@ export type InferServiceKind<S extends ServiceTypesLike> =
|
|
|
22
22
|
|
|
23
23
|
export type ServiceName<S extends ServiceTypesLike = ServiceTypesLike> = Branded<string, S>;
|
|
24
24
|
|
|
25
|
-
export type ServiceType = "node" | "wasm";
|
|
25
|
+
export type ServiceType = "node" | "wasm" | "main";
|
|
26
26
|
|
|
27
27
|
const SERVICE_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*$/;
|
|
28
28
|
|
|
29
|
-
export const {
|
|
29
|
+
export const {
|
|
30
|
+
service,
|
|
31
|
+
isNodeService,
|
|
32
|
+
isWasmService,
|
|
33
|
+
isMainService,
|
|
34
|
+
getServiceKind,
|
|
35
|
+
getServiceModelMethods,
|
|
36
|
+
getServiceUiMethods,
|
|
37
|
+
} = (() => {
|
|
30
38
|
const typeMap = new Map<string, ServiceType>();
|
|
39
|
+
const modelMethodsMap = new Map<string, readonly string[]>();
|
|
40
|
+
const uiMethodsMap = new Map<string, readonly string[]>();
|
|
31
41
|
return {
|
|
32
42
|
service<Model, Ui>() {
|
|
33
|
-
return <
|
|
43
|
+
return <
|
|
44
|
+
K extends ServiceType,
|
|
45
|
+
N extends string,
|
|
46
|
+
MM extends readonly (keyof Model & string)[],
|
|
47
|
+
UM extends readonly (keyof Ui & string)[],
|
|
48
|
+
>(options: {
|
|
34
49
|
readonly type: K;
|
|
35
50
|
readonly name: N;
|
|
51
|
+
readonly modelMethods: MM;
|
|
52
|
+
readonly uiMethods: UM;
|
|
36
53
|
}): Branded<N, ServiceTypesLike<Model, Ui, K>> => {
|
|
37
|
-
const { name, type } = options;
|
|
54
|
+
const { name, type, modelMethods, uiMethods } = options;
|
|
38
55
|
if (!SERVICE_ID_PATTERN.test(name)) {
|
|
39
56
|
throw new ServiceInvalidIdError(
|
|
40
57
|
`Invalid service ID "${name}": must match ${SERVICE_ID_PATTERN}`,
|
|
@@ -44,12 +61,29 @@ export const { service, isNodeService } = (() => {
|
|
|
44
61
|
throw new ServiceAlreadyRegisteredError(`Service "${name}" already registered`);
|
|
45
62
|
}
|
|
46
63
|
typeMap.set(name, type);
|
|
64
|
+
modelMethodsMap.set(name, modelMethods);
|
|
65
|
+
uiMethodsMap.set(name, uiMethods);
|
|
47
66
|
return name as Branded<N, ServiceTypesLike<Model, Ui, K>>;
|
|
48
67
|
};
|
|
49
68
|
},
|
|
50
69
|
isNodeService(id: ServiceName): boolean {
|
|
51
70
|
return typeMap.get(id) === "node";
|
|
52
71
|
},
|
|
72
|
+
isWasmService(id: ServiceName): boolean {
|
|
73
|
+
return typeMap.get(id) === "wasm";
|
|
74
|
+
},
|
|
75
|
+
isMainService(id: ServiceName): boolean {
|
|
76
|
+
return typeMap.get(id) === "main";
|
|
77
|
+
},
|
|
78
|
+
getServiceKind(id: ServiceName): ServiceType | undefined {
|
|
79
|
+
return typeMap.get(id);
|
|
80
|
+
},
|
|
81
|
+
getServiceModelMethods(id: ServiceName): readonly string[] {
|
|
82
|
+
return modelMethodsMap.get(id) ?? [];
|
|
83
|
+
},
|
|
84
|
+
getServiceUiMethods(id: ServiceName): readonly string[] {
|
|
85
|
+
return uiMethodsMap.get(id) ?? [];
|
|
86
|
+
},
|
|
53
87
|
};
|
|
54
88
|
})();
|
|
55
89
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"service_injector_factory.cjs","names":[],"sources":["../../src/services/service_injector_factory.ts"],"sourcesContent":["/**\n * UI service injector factory.\n *\n * When adding a new node service, add its method wrappers here.\n * Everything else (SERVICE_METHOD_MAP, buildServiceInfo, UiServiceInjectorMap)\n * auto-derives from this factory + Services in service_declarations.ts.\n */\n\nimport type { DriverKit } from \"../driver_kit\";\nimport type { UiServiceInjectorMap } from \"./service_injectors\";\n\nexport function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjectorMap {\n const { pFrameDriver } = driverKit;\n return {\n PFrame: {\n findColumns: (handle, request) => pFrameDriver.findColumns(handle, request),\n getColumnSpec: (handle, columnId) => pFrameDriver.getColumnSpec(handle, columnId),\n listColumns: (handle) => pFrameDriver.listColumns(handle),\n calculateTableData: (handle, request, range) =>\n pFrameDriver.calculateTableData(handle, request, range),\n getUniqueValues: (handle, request) => pFrameDriver.getUniqueValues(handle, request),\n getShape: (handle) => pFrameDriver.getShape(handle),\n getSpec: (handle) => pFrameDriver.getSpec(handle),\n getData: (handle, columnIndices, range) => pFrameDriver.getData(handle, columnIndices, range),\n },\n };\n}\n"],"mappings":";AAWA,SAAgB,yBAAyB,WAA4C;CACnF,MAAM,EAAE,iBAAiB;AACzB,QAAO,EACL,QAAQ;EACN,cAAc,QAAQ,YAAY,aAAa,YAAY,QAAQ,QAAQ;EAC3E,gBAAgB,QAAQ,aAAa,aAAa,cAAc,QAAQ,SAAS;EACjF,cAAc,WAAW,aAAa,YAAY,OAAO;EACzD,qBAAqB,QAAQ,SAAS,UACpC,aAAa,mBAAmB,QAAQ,SAAS,MAAM;EACzD,kBAAkB,QAAQ,YAAY,aAAa,gBAAgB,QAAQ,QAAQ;EACnF,WAAW,WAAW,aAAa,SAAS,OAAO;EACnD,UAAU,WAAW,aAAa,QAAQ,OAAO;EACjD,UAAU,QAAQ,eAAe,UAAU,aAAa,QAAQ,QAAQ,eAAe,MAAM;EAC9F,EACF"}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { DriverKit } from "../driver_kit.js";
|
|
2
|
-
import { UiServiceInjectorMap } from "./service_injectors.js";
|
|
3
|
-
|
|
4
|
-
//#region src/services/service_injector_factory.d.ts
|
|
5
|
-
declare function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjectorMap;
|
|
6
|
-
//#endregion
|
|
7
|
-
export { createUiServiceInjectors };
|
|
8
|
-
//# sourceMappingURL=service_injector_factory.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"service_injector_factory.d.ts","names":[],"sources":["../../src/services/service_injector_factory.ts"],"mappings":";;;;iBAWgB,wBAAA,CAAyB,SAAA,EAAW,SAAA,GAAY,oBAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"service_injector_factory.js","names":[],"sources":["../../src/services/service_injector_factory.ts"],"sourcesContent":["/**\n * UI service injector factory.\n *\n * When adding a new node service, add its method wrappers here.\n * Everything else (SERVICE_METHOD_MAP, buildServiceInfo, UiServiceInjectorMap)\n * auto-derives from this factory + Services in service_declarations.ts.\n */\n\nimport type { DriverKit } from \"../driver_kit\";\nimport type { UiServiceInjectorMap } from \"./service_injectors\";\n\nexport function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjectorMap {\n const { pFrameDriver } = driverKit;\n return {\n PFrame: {\n findColumns: (handle, request) => pFrameDriver.findColumns(handle, request),\n getColumnSpec: (handle, columnId) => pFrameDriver.getColumnSpec(handle, columnId),\n listColumns: (handle) => pFrameDriver.listColumns(handle),\n calculateTableData: (handle, request, range) =>\n pFrameDriver.calculateTableData(handle, request, range),\n getUniqueValues: (handle, request) => pFrameDriver.getUniqueValues(handle, request),\n getShape: (handle) => pFrameDriver.getShape(handle),\n getSpec: (handle) => pFrameDriver.getSpec(handle),\n getData: (handle, columnIndices, range) => pFrameDriver.getData(handle, columnIndices, range),\n },\n };\n}\n"],"mappings":";AAWA,SAAgB,yBAAyB,WAA4C;CACnF,MAAM,EAAE,iBAAiB;AACzB,QAAO,EACL,QAAQ;EACN,cAAc,QAAQ,YAAY,aAAa,YAAY,QAAQ,QAAQ;EAC3E,gBAAgB,QAAQ,aAAa,aAAa,cAAc,QAAQ,SAAS;EACjF,cAAc,WAAW,aAAa,YAAY,OAAO;EACzD,qBAAqB,QAAQ,SAAS,UACpC,aAAa,mBAAmB,QAAQ,SAAS,MAAM;EACzD,kBAAkB,QAAQ,YAAY,aAAa,gBAAgB,QAAQ,QAAQ;EACnF,WAAW,WAAW,aAAa,SAAS,OAAO;EACnD,UAAU,WAAW,aAAa,QAAQ,OAAO;EACjD,UAAU,QAAQ,eAAe,UAAU,aAAa,QAAQ,QAAQ,eAAe,MAAM;EAC9F,EACF"}
|