@cocreate/plugins 1.2.1 → 1.4.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/src/index.js CHANGED
@@ -1,592 +1,254 @@
1
- import Observer from "@cocreate/observer";
2
- import {dotNotationToObject} from "@cocreate/utils";
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ ********************************************************************************/
3
18
 
4
- /**
5
- * @typedef {Object} PluginDefinition
6
- * @property {Array<string|Object>} [js] - List of JS files to load. Can be strings (URLs) or objects with src, integrity, etc
7
- * @property {Array<string>} [css] - List of CSS files to load.
8
- */
9
-
10
- // --- CONFIGURATION ---
19
+ import Observer from "@cocreate/observer";
20
+ import { processOperators } from "./operators.js";
11
21
 
12
- /**
13
- * @type {Object.<string, PluginDefinition>}
14
- * Configuration object containing plugin definitions.
15
- * Populated dynamically from CoCreate.config.js or defaults.
16
- */
17
22
  const plugins = {};
18
-
19
- // --- CORE ENGINE ---
20
-
21
- /**
22
- * Global Cache for script promises to prevent race conditions and duplicate loads.
23
- * Stores the pending Promise for a script source URL.
24
- * @type {Map<string, Promise<void>>}
25
- */
26
23
  const scriptCache = new Map();
27
-
28
- /**
29
- * Cache the CSS marker once on load to determine injection point.
30
- * Used to ensure plugin CSS is injected in the correct order relative to user styles.
31
- * @type {Element|null}
32
- */
33
24
  const cssMarker = typeof document !== 'undefined' ? document.querySelector('link[plugins]') : null;
34
25
 
35
26
  /**
36
- * Global Initialization Function.
37
- * Processes one or more elements to detect and attach plugins.
38
- * * @param {HTMLElement|NodeList|Array<HTMLElement>} elements - Single Element, NodeList, or Array of Elements to initialize.
39
- * @returns {void}
27
+ * Initializes plugins on a collection of HTML elements.
28
+ * @param {HTMLElement|HTMLElement[]|NodeList} elements
40
29
  */
41
- function init(elements) {
30
+ export function init(elements) {
42
31
  if (!elements) return;
43
-
44
- let collection = [];
45
- if (elements instanceof HTMLElement || elements instanceof Element) {
46
- collection = [elements];
47
- } else if (elements.length !== undefined && typeof elements !== 'function') {
48
- collection = Array.from(elements);
49
- } else {
50
- collection = [elements];
51
- }
52
-
32
+ const collection = (elements instanceof HTMLElement || elements instanceof Element)
33
+ ? [elements]
34
+ : Array.from(elements);
35
+
53
36
  collection.forEach(el => {
54
- if (el && typeof el.getAttribute === 'function') {
37
+ if (el) {
55
38
  processPlugin(el);
56
39
  }
57
40
  });
58
41
  }
59
42
 
60
- /**
61
- * Processes an individual element to detect, load resources for, and execute plugins.
62
- * Reads the 'plugin' attribute (e.g., plugin="chart, map") to identify targets.
63
- * * @async
64
- * @param {HTMLElement} el - The DOM element to process.
65
- * @returns {Promise<void>} Resolves when all resources for the plugins are loaded.
66
- */
67
- async function processPlugin(el) {
68
- const rawAttr = el.getAttribute("plugin");
69
- if (!rawAttr) return;
70
-
71
- const pluginNames = rawAttr.split(',').map(s => s.trim());
72
-
73
- for (const pluginName of pluginNames) {
74
- const pluginDef = plugins[pluginName];
75
-
76
- // Only attempt to load resources if a configuration exists
77
- if (pluginDef) {
78
- // Load CSS
79
- if (pluginDef.css) pluginDef.css.forEach(href => {
80
- if (!document.querySelector(`link[href="${href}"]`)) {
81
- const link = document.createElement("link");
82
- link.rel = "stylesheet";
83
- link.href = href;
84
-
85
- // CSS INJECTION STRATEGY:
86
- // 1. Priority: Check for a specific marker element <link plugin>
87
- // (Cached globally in cssMarker)
88
-
89
- if (cssMarker) {
90
- // Insert before the marker
91
- cssMarker.parentNode.insertBefore(link, cssMarker);
92
- } else {
93
- // 2. Fallback: Prepend before existing CSS
94
- // To allow custom CSS to easily override plugin defaults, we must ensure
95
- // plugin CSS loads BEFORE user CSS.
96
- const firstStyle = document.head.querySelector('link[rel="stylesheet"], style');
97
-
98
- if (firstStyle) {
99
- document.head.insertBefore(link, firstStyle);
100
- } else {
101
- // If no CSS exists yet, appending is safe
102
- document.head.appendChild(link);
103
- }
43
+ async function preloadResources(pluginNames) {
44
+ for (const name of pluginNames) {
45
+ const pluginDef = plugins[name];
46
+ if (pluginDef && !window[name]) {
47
+ if (pluginDef.css) {
48
+ pluginDef.css.forEach(href => {
49
+ if (!document.querySelector(`link[href="${href}"]`)) {
50
+ const link = document.createElement("link");
51
+ link.rel = "stylesheet";
52
+ link.href = href;
53
+ if (cssMarker) cssMarker.parentNode.insertBefore(link, cssMarker);
54
+ else document.head.appendChild(link);
104
55
  }
105
- }
106
- });
56
+ });
57
+ }
107
58
 
108
- // Load JS with Promise Cache
109
59
  if (pluginDef.js) {
110
- const preWindowKeys = (typeof window !== 'undefined') ? new Set(Object.keys(window)) : new Set();
111
-
112
60
  for (const item of pluginDef.js) {
113
61
  const src = typeof item === 'string' ? item : item.src;
114
- const integrity = typeof item === 'object' ? item.integrity : null;
115
- const crossOrigin = typeof item === 'object' ? item.crossOrigin : null;
116
-
62
+ if (!src) continue;
117
63
  if (!scriptCache.has(src)) {
118
- const scriptPromise = new Promise((resolve, reject) => {
119
- // Check if already in DOM (manual load)
120
- const existing = document.querySelector(`script[src="${src}"]`);
121
- if (existing) {
122
- if (existing.dataset.loaded === "true") {
123
- resolve();
124
- } else {
125
- const prevOnload = existing.onload;
126
- existing.onload = () => {
127
- if (prevOnload) prevOnload();
128
- existing.dataset.loaded = "true";
129
- resolve();
130
- };
131
- existing.onerror = reject;
132
- }
133
- } else {
134
- const s = document.createElement("script");
135
- s.src = src;
136
- if (integrity) {
137
- s.integrity = integrity;
138
- s.crossOrigin = crossOrigin || "anonymous";
139
- }
140
- s.onload = () => {
141
- s.dataset.loaded = "true";
142
- resolve();
143
- };
144
- s.onerror = reject;
64
+ const existingScript = document.querySelector(`script[src*="${src}"]`);
65
+ if (existingScript && (existingScript.dataset.loaded === "true" || window[name])) {
66
+ scriptCache.set(src, Promise.resolve());
67
+ } else {
68
+ const s = document.createElement("script");
69
+ s.src = src;
70
+ scriptCache.set(src, new Promise((resolve) => {
71
+ s.onload = () => { s.dataset.loaded = "true"; resolve(); };
72
+ s.onerror = () => resolve();
145
73
  document.head.appendChild(s);
146
- }
147
- });
148
- scriptCache.set(src, scriptPromise);
149
- }
150
-
151
- try {
152
- await scriptCache.get(src);
153
- } catch (e) {
154
- console.error(`Failed to load script: ${src}`, e);
155
- }
156
- }
157
-
158
- // After loading JS files, map newly-added globals to the expected plugin name.
159
- // Exact (case-insensitive) matching only.
160
- try {
161
- if (typeof window !== 'undefined') {
162
- const expectedName = pluginName;
163
- const lower = expectedName.toLowerCase();
164
-
165
- const allKeys = Object.keys(window);
166
- const newKeys = allKeys.filter(k => !preWindowKeys.has(k));
167
- let mappedKey = null;
168
-
169
- for (const k of newKeys) {
170
- if (k.toLowerCase() === lower) { mappedKey = k; break; }
171
- }
172
-
173
- if (!mappedKey) {
174
- for (const k of allKeys) {
175
- if (k.toLowerCase() === lower) { mappedKey = k; break; }
176
- }
177
- }
178
-
179
- if (mappedKey && !window[expectedName]) {
180
- window[expectedName] = window[mappedKey];
181
- console.debug(`Mapped plugin global: window.${expectedName} <- window.${mappedKey}`);
74
+ }));
182
75
  }
183
76
  }
184
- } catch (err) {
185
- // Non-fatal
77
+ await scriptCache.get(src);
186
78
  }
187
79
  }
188
80
  }
189
-
190
- // Attempt to execute plugin even if no config was found (it might be on window already)
191
- executeGenericPlugin(el, pluginName);
192
81
  }
193
82
  }
194
83
 
195
- /**
196
- * Helper to determine if a function should be called with 'new'.
197
- * Uses heuristics like ES6 class syntax, lack of prototype (arrow function), or PascalCase naming.
198
- * * @param {Function} func - The function to check.
199
- * @param {string} [name] - The property name associated with the function (for casing check).
200
- * @returns {boolean} True if the function appears to be a constructor.
201
- */
202
- const isConstructor = (func, name) => {
203
- try {
204
- if (typeof func !== 'function') return false;
205
- if (/^\s*class\s+/.test(func.toString())) return true;
206
- if (!func.prototype) return false;
207
- const n = name || func.name;
208
- if (n && /^[A-Z]/.test(n)) return true;
209
- } catch(e) {}
210
- return false;
211
- };
212
-
213
- /**
214
- * Executes the logic for a generic plugin on a specific element.
215
- * Handles:
216
- * 1. Resolving the target class/function from window.
217
- * 2. Initializing the base instance.
218
- * 3. Processing attribute paths and nested JSON objects to execute methods or set properties.
219
- * * @param {HTMLElement} el - The target element.
220
- * @param {string} name - The name of the plugin (case-insensitive identifier).
221
- * @returns {void}
222
- */
223
- function executeGenericPlugin(el, name) {
224
- const prefix = name.toLowerCase();
225
- const mainAttr = el.getAttribute(prefix);
226
- let rawData = {};
227
-
228
- for (let attr of el.attributes) {
229
- let key = attr.name;
230
- if (key === prefix) {
231
- key = name;
232
- } else if (key.startsWith(prefix + '-')) {
233
- key = key.replaceAll("-", ".");
234
- } else if (!key.startsWith(prefix + '.')) {
235
- continue
236
- }
237
-
238
- try {
239
- rawData[key] = JSON.parse(attr.value);
240
- } catch(e) {
241
- rawData[key] = attr.value;
242
- }
243
-
84
+ async function processPlugin(el) {
85
+ if (!el.attributes) return;
86
+
87
+ // Direct attributes collection map lookup to avoid binding/context prototype errors
88
+ const pluginAttrNode = el.attributes['plugin'] || el.attributes.getNamedItem?.('plugin');
89
+ const rawPluginAttr = pluginAttrNode?.value?.trim();
90
+ if (!rawPluginAttr) return;
91
+
92
+ const context = {
93
+ element: el,
94
+ this: el,
95
+ plugin: null, // Tracks active sibling plugin instance
96
+ data: { ...el.dataset },
97
+ event: {},
98
+ crud: window.CoCreate?.crud || null
244
99
  };
245
100
 
246
- // 2. Resolve parameters (Token Resolver)
247
- let resolved = processParams(el, rawData);
248
- resolved = dotNotationToObject(resolved);
249
-
250
- let Plugin = window[name] || window[prefix];
251
- if (!Plugin) {
252
- console.error(`Plugin for ${name} not found on window.`);
253
- return;
254
- }
255
-
256
- // Iterate over resolved object.
257
- // Since we use dotNotationToObject, keys like "swiper.effect" are already nested as { swiper: { effect: ... } }
258
- for (let key in resolved) {
259
- // We generally expect the root key to match the plugin name (e.g., 'swiper')
260
- // We unwrap this root key to pass the actual config to the Plugin.
261
- if (key === name || key.toLowerCase() === prefix) {
262
- try {
263
- // Determine Target: Use existing instance on element if available, else use Window Plugin
264
- let Target = el[name] || Plugin;
265
- let val = resolved[key];
266
-
267
- // Pass context: Window as parent, Plugin Name as property (for potential context binding)
268
- // el and name used to store the result on the element.
269
- update(Target, val, window, name, el, name, el);
101
+ try {
102
+ let configs = [];
103
+
104
+ // Unified Direct Parser: Pure JSON parsing with simple zero-argument fallback
105
+ if (rawPluginAttr.startsWith('{') || rawPluginAttr.startsWith('[')) {
106
+ const parsedConfig = JSON.parse(rawPluginAttr);
107
+ configs = Array.isArray(parsedConfig) ? parsedConfig : [parsedConfig];
108
+ } else {
109
+ // Symmetrical Fallback: Plain strings mapped straight to constructor blocks
110
+ configs = [{ [rawPluginAttr]: [] }];
111
+ }
112
+
113
+ const targetPlugins = new Set();
114
+ configs.forEach(conf => {
115
+ Object.keys(conf).forEach(k => {
116
+ if (!k.startsWith('$')) {
117
+ targetPlugins.add(k.split('.')[0]);
118
+ }
119
+ });
120
+ });
121
+
122
+ await preloadResources(Array.from(targetPlugins));
270
123
 
271
- console.log(`Processed ${name}`);
272
- } catch (e) {
273
- console.error(`Error processing ${name}:`, e);
274
- }
124
+ // Execute each configuration block sequentially
125
+ for (const configBlock of configs) {
126
+ await executeConfigBlock(context, configBlock);
275
127
  }
128
+ } catch (err) {
129
+ console.error("[Plugin System] Failed to parse and execute plugin configuration:", err, rawPluginAttr);
276
130
  }
277
131
  }
278
132
 
279
- function resolvePathWithParent(root, path) {
280
- if (!root || !path || typeof path !== "string") return { parent: null, value: undefined };
281
- const parts = path.split(".").filter(Boolean);
282
- if (!parts.length) return { parent: null, value: undefined };
283
-
284
- let parent = null;
285
- let current = root;
286
- for (let i = 0; i < parts.length; i++) {
287
- const part = parts[i];
288
- if (current == null) return { parent: null, value: undefined };
289
- parent = current;
290
- current = current[part];
133
+ function instantiatePlugin(ConstructorFunc, argsArray) {
134
+ if (typeof ConstructorFunc !== 'function') return ConstructorFunc;
135
+
136
+ // Check standard ES6 class structure
137
+ const isEs6Class = /^\s*class\s+/.test(ConstructorFunc.toString());
138
+
139
+ if (isEs6Class) {
140
+ return new ConstructorFunc(...argsArray);
291
141
  }
292
- return { parent, value: current };
293
- }
294
-
295
- function normalizeCrudPayload(value) {
296
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
297
-
298
- if (value.type && Array.isArray(value[value.type])) return value[value.type];
299
- if (value.method && typeof value.method === "string") {
300
- const type = value.method.split(".")[0];
301
- if (type && Array.isArray(value[type])) return value[type];
142
+
143
+ try {
144
+ // Fallback Step A: Attempt standard function invocation (e.g. particlesJS)
145
+ return ConstructorFunc(...argsArray);
146
+ } catch (err) {
147
+ // Fallback Step B: If invocation throws constructor error (e.g. Babel transpiled ES5 classes)
148
+ if (err instanceof TypeError) {
149
+ return new ConstructorFunc(...argsArray);
150
+ }
151
+ throw err;
302
152
  }
303
- return value;
304
- }
305
-
306
- function getPluginInstancesFromElement(el) {
307
- if (!el || !el.__cocreatePluginInstances) return [];
308
- return Object.values(el.__cocreatePluginInstances).filter(Boolean);
309
- }
310
-
311
- function isReferenceAssignment(val) {
312
- return typeof val === "string" && val.trim().startsWith("=");
313
- }
314
-
315
- function normalizeReferencePath(refPath) {
316
- if (typeof refPath !== "string") return "";
317
- return refPath.trim().replace(/^=\s*/, "");
318
153
  }
319
154
 
320
- function resolveCallableReference(refPath, parent, hostElement) {
321
- const normalized = normalizeReferencePath(refPath);
322
- if (!normalized) return { fn: undefined, context: undefined, methodName: undefined };
323
-
324
- const methodName = normalized.split(".").pop();
325
- const startsWithThis = normalized === "$this" || normalized.startsWith("$this.");
326
- const startsWithWindow = normalized === "$window" || normalized.startsWith("$window.");
327
- const startsWithToken = normalized.startsWith("$");
328
-
329
- const candidates = [];
330
- if (startsWithThis) {
331
- const path = normalized.replace(/^\$this\.?/, "");
332
- candidates.push({ root: hostElement || parent, path });
333
- } else if (startsWithWindow) {
334
- const path = normalized.replace(/^\$window\.?/, "");
335
- candidates.push({ root: window, path });
336
- } else if (startsWithToken) {
337
- const path = normalized.replace(/^\$/, "");
338
- candidates.push({ root: hostElement, path });
339
- candidates.push({ root: parent, path });
340
- candidates.push({ root: window, path });
341
- } else {
342
- candidates.push({ root: hostElement, path: normalized });
343
- candidates.push({ root: parent, path: normalized });
344
- candidates.push({ root: window, path: normalized });
345
- }
346
-
347
- for (const candidate of candidates) {
348
- if (!candidate.root) continue;
349
- const { parent: resolvedParent, value } = resolvePathWithParent(candidate.root, candidate.path);
350
- if (typeof value === "function") {
351
- return { fn: value, context: resolvedParent, methodName };
352
- }
353
- }
354
-
355
- if (methodName) {
356
- const instances = getPluginInstancesFromElement(hostElement || parent);
357
- for (const instance of instances) {
358
- if (instance && typeof instance[methodName] === "function") {
359
- return { fn: instance[methodName], context: instance, methodName };
155
+ async function executeConfigBlock(context, configBlock) {
156
+ for (const [key, value] of Object.entries(configBlock)) {
157
+ // 1. If key is an operator, process it natively via AST engine
158
+ if (key.startsWith("$")) {
159
+ await processOperators(context, { [key]: value });
160
+ continue;
161
+ }
162
+
163
+ // 2. Intercept standard lifecycle methods to bind them directly as functions on the Element
164
+ if (["getValue", "setValue", "read", "write"].includes(key)) {
165
+ context.element[key] = async function(...args) {
166
+ const localContext = {
167
+ ...context,
168
+ event: { ...context.event, value: args[0], arguments: args }
169
+ };
170
+ return await processOperators(localContext, value);
171
+ };
172
+ continue;
173
+ }
174
+
175
+ const parts = key.split('.');
176
+ const rootPluginName = parts[0];
177
+
178
+ // 3. Resolve target from window object using dot notation
179
+ let ConstructorFunc = window;
180
+ for (const part of parts) {
181
+ if (ConstructorFunc) {
182
+ ConstructorFunc = ConstructorFunc[part];
360
183
  }
361
184
  }
362
- }
363
-
364
- return { fn: undefined, context: undefined, methodName };
365
- }
366
-
367
- function createFunctionAdapter(refPath, parent, property, hostElement) {
368
- const normalizedRefPath = normalizeReferencePath(refPath);
369
- const methodName = normalizedRefPath.split(".").pop();
370
185
 
371
- return function (...args) {
372
- const resolved = resolveCallableReference(normalizedRefPath, parent, hostElement);
373
- const fn = resolved.fn;
374
- const context = resolved.context;
375
-
376
- if (typeof fn !== "function") {
377
- console.error(`Plugin adapter failed: "${normalizedRefPath}" did not resolve to a function for ${property}.`);
378
- return;
379
- }
380
-
381
- if (property === "setValue") {
382
- const payload = normalizeCrudPayload(args[0]);
383
-
384
- if (methodName === "addEventSource" && context && typeof context.getEventSources === "function") {
385
- const sources = context.getEventSources();
386
- if (Array.isArray(sources)) {
387
- sources.forEach(source => source && typeof source.remove === "function" && source.remove());
388
- }
186
+ // 4. If it's a function, treat it as a constructor to initialize the plugin
187
+ if (typeof ConstructorFunc === 'function' && !context.element[rootPluginName]) {
188
+ let constructorArgs = [];
189
+ if (value !== undefined) {
190
+ const evaluatedArgs = await processOperators(context, value);
191
+ constructorArgs = Array.isArray(evaluatedArgs) ? evaluatedArgs : [evaluatedArgs];
389
192
  }
390
193
 
391
- return fn.call(context || this, payload);
392
- }
393
-
394
- return fn.apply(context || this, args);
395
- };
396
- }
397
-
398
- function update(Target, val, parent, property, elParent, elProperty, hostElement) {
399
- // RESOLUTION: Handle case-insensitivity before processing targets.
400
- // If Target is missing, check parent for a property matching 'property' (case-insensitive).
401
- if (!Target && parent && property) {
402
- const lowerProp = String(property).toLowerCase();
403
- for (const key in parent) {
404
- if (key.toLowerCase() === lowerProp) {
405
- Target = parent[key];
406
- property = key;
407
- if (elProperty) elProperty = key; // Update element structure key to match real property
408
- break;
194
+ // Create the plugin instance and bind it directly to the element under its root name
195
+ const instance = instantiatePlugin(ConstructorFunc, constructorArgs);
196
+ context.element[rootPluginName] = instance;
197
+ context.plugin = instance; // Update context.plugin reference for sibling method chaining
198
+ continue;
199
+ }
200
+
201
+ // 5. Explicit Instance Method Chaining (e.g. "FullCalendar.render")
202
+ let methodTarget = context.element[rootPluginName];
203
+ if (methodTarget && parts.length > 1) {
204
+ let method = methodTarget;
205
+ let parent = methodTarget;
206
+ for (let i = 1; i < parts.length; i++) {
207
+ parent = method;
208
+ if (method) method = method[parts[i]];
409
209
  }
410
- }
411
- }
412
-
413
- let instance;
414
- if (typeof Target === 'function') {
415
- if (isReferenceAssignment(val) && parent && property) {
416
- instance = createFunctionAdapter(val, parent, property, hostElement);
417
- parent[property] = instance;
418
- if (elParent && elProperty) elParent[elProperty] = instance;
419
- return;
420
- }
421
-
422
- if (!isConstructor(Target, property)) {
423
- // Call as a function (method or standalone)
424
- // Use 'parent' as context (this) if available to maintain class references
425
- if (parent) {
426
- if (Array.isArray(val)) {
427
- instance = Target.apply(parent, val);
428
- } else {
429
- instance = Target.call(parent, val);
430
- }
431
- } else {
432
- if (Array.isArray(val)) {
433
- instance = Target(...val);
434
- } else {
435
- instance = Target(val);
210
+ if (typeof method === 'function') {
211
+ let methodArgs = [];
212
+ if (value !== undefined) {
213
+ const evaluatedArgs = await processOperators(context, value);
214
+ methodArgs = Array.isArray(evaluatedArgs) ? evaluatedArgs : [evaluatedArgs];
436
215
  }
437
- }
438
- } else {
439
- // Call as a Constructor
440
- if (Array.isArray(val)) {
441
- instance = new Target(...val);
442
- } else {
443
- instance = new Target(val);
216
+ await method.apply(parent, methodArgs);
217
+ continue;
444
218
  }
445
219
  }
446
-
447
- // Assign the result to the element structure
448
- if (elParent && elProperty) {
449
- elParent[elProperty] = instance;
450
- }
451
220
 
452
- if (instance && instance.el && typeof instance.el === "object") {
453
- if (!instance.el.__cocreatePluginInstances) instance.el.__cocreatePluginInstances = {};
454
- const key = property || (Target && Target.name) || "instance";
455
- instance.el.__cocreatePluginInstances[key] = instance;
456
- }
457
-
458
- } else if (typeof Target === 'object' && Target !== null && typeof val === 'object' && val !== null && !Array.isArray(val)) {
459
- // Prepare the next level of the element structure
460
- if (elParent && elProperty) {
461
- if (!elParent[elProperty]) {
462
- elParent[elProperty] = {};
463
- }
464
- const nextElParent = elParent[elProperty];
465
-
466
- for (let key in val) {
467
- update(Target[key], val[key], Target, key, nextElParent, key, hostElement);
221
+ // 6. Sibling Method Chaining (e.g. "render" directly on context.plugin)
222
+ if (context.plugin && typeof context.plugin[key] === 'function') {
223
+ let methodArgs = [];
224
+ if (value !== undefined) {
225
+ const evaluatedArgs = await processOperators(context, value);
226
+ methodArgs = Array.isArray(evaluatedArgs) ? evaluatedArgs : [evaluatedArgs];
468
227
  }
228
+ await context.plugin[key].apply(context.plugin, methodArgs);
229
+ continue;
469
230
  }
470
- } else if (parent && property) {
471
- if (isReferenceAssignment(val)) {
472
- const adapter = createFunctionAdapter(val, parent, property, hostElement);
473
- parent[property] = adapter;
474
- if (elParent && elProperty) elParent[elProperty] = adapter;
475
- return;
476
- }
477
-
478
- // If it's not a function, we are setting a value on the plugin object
479
- parent[property] = val;
480
-
481
- // Map the value to the element structure
482
- if (elParent && elProperty) {
483
- elParent[elProperty] = val;
484
- }
485
-
486
- console.log(`Set plugin property ${property} to`, val);
487
- }
488
- }
489
-
490
- /**
491
- * Generic Parameter Processor
492
- * Handles:
493
- * - $this / $this.children
494
- * - $window.path.to.function(arg)
495
- * - $anime.stagger(100)
496
- * - Global access: $document, $window, etc.
497
- */
498
- function processParams(el, params) {
499
- if (typeof params === 'string' && params.startsWith('\u0024')) {
500
- try {
501
- // 1. Check for Method Call: $root.path.to.func(arg)
502
- const callMatch = params.match(/^\u0024([^.]+)\.(.+)\((.*)\)$/);
503
- if (callMatch) {
504
- const [_, root, path, arg] = callMatch;
505
- const obj = (root === 'this') ? el : window[root];
506
-
507
- // If root object exists, drill down
508
- if (obj) {
509
- const func = path.split('.').reduce((o, k) => (o || {})[k], obj);
510
- if (typeof func === 'function') {
511
- // Parse argument if JSON-like, else string
512
- const parsedArg = arg ? (function() { try { return JSON.parse(arg); } catch(e) { return arg; } })() : undefined;
513
- return func(parsedArg);
514
- }
515
- }
516
- }
517
-
518
- // 2. Check for Property Access: $root.path.to.prop or just $root
519
- const propMatch = params.match(/^\u0024([^.]+)(?:\.(.+))?$/);
520
- if (propMatch) {
521
- const [_, root, path] = propMatch;
522
- const obj = (root === 'this') ? el : window[root];
523
-
524
- if (obj) {
525
- if (!path) return (obj instanceof HTMLCollection) ? Array.from(obj) : obj;
526
-
527
- const val = path.split('.').reduce((o, k) => (o || {})[k], obj);
528
- // Convert HTMLCollections to Arrays
529
- return (val instanceof HTMLCollection) ? Array.from(val) : val;
530
- }
531
- }
532
231
 
533
- // 3. Check for standalone globals like $document or $window
534
- const globalKey = params.substring(1);
535
- if (window[globalKey]) {
536
- return window[globalKey];
537
- }
538
-
539
- } catch (e) {
540
- console.warn("Failed to resolve dynamic token:", params);
541
- }
232
+ // 7. Fallback: Process operators or generic actions on properties
233
+ await processOperators(context, { [key]: value });
542
234
  }
543
-
544
- if (Array.isArray(params)) return params.map(p => processParams(el, p));
545
- if (typeof params === 'object' && params !== null) {
546
- const res = {};
547
- for (let k in params) res[k] = processParams(el, params[k]);
548
- return res;
549
- }
550
- return params;
551
235
  }
552
236
 
553
- // --- STARTUP LOGIC ---
554
-
555
237
  if (typeof document !== 'undefined') {
556
- // Dynamic Import: Loads config if available, handles error if missing.
557
- // Works with 'npm start' (Bundlers) by creating a code-split chunk.
238
+ const selector = "[plugin]";
239
+ Observer.init({
240
+ name: "plugin",
241
+ types: ["addedNodes", "attributes"],
242
+ selector: selector,
243
+ attributeFilter: ["plugin"],
244
+ callback: (mutation) => init(mutation.target)
245
+ });
246
+
558
247
  import("./CoCreate.config.js")
559
248
  .then((Config) => {
560
- // LOGIC: Merge exports into plugins object
561
- if (Config.plugins) {
562
- Object.assign(plugins, Config.plugins);
563
- }
564
- else if (Config.default) {
565
- if (Config.default.plugins) {
566
- Object.assign(plugins, Config.default.plugins);
567
- } else {
568
- Object.assign(plugins, Config.default);
569
- }
570
- }
249
+ const data = Config.plugins || Config.default?.plugins || Config.default || {};
250
+ Object.assign(plugins, data);
571
251
  })
572
- .catch((err) => {
573
- // Optional: fail silently for optional config
574
- })
575
- .finally(() => {
576
- // Start Observer
577
- Observer.init({
578
- name: "plugin",
579
- types: ["addedNodes", "attributes"],
580
- selector: "[plugin]",
581
- attributeFilter: ["plugin"],
582
- callback: (mutation) => {
583
- init(mutation.target);
584
- }
585
- });
586
-
587
- // Initial Init
588
- init(document.querySelectorAll("[plugin]"));
589
- });
590
- }
591
-
592
- export default { init, plugins }
252
+ .catch(() => {})
253
+ .finally(() => init(document.querySelectorAll(selector)));
254
+ }