@cocreate/plugins 1.2.0 → 1.3.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/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [1.3.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.2.1...v1.3.0) (2026-03-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Update demo/index.html to test sequential execution of attributes as logic ([d71e0a5](https://github.com/CoCreate-app/CoCreate-plugins/commit/d71e0a562bc48337746583a04bf230f84134f6c7))
7
+
8
+
9
+ ### Features
10
+
11
+ * Refactor plugin initialization to sequentially load resources and execute attributes in DOM order. Add support for direct $this references in attributes. Improve error handling and logging during plugin execution. ([08ecdb2](https://github.com/CoCreate-app/CoCreate-plugins/commit/08ecdb2b2bb8bba2cf3950eb2ef5e7ac77916203))
12
+
13
+ ## [1.2.1](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.2.0...v1.2.1) (2026-03-02)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * Handle case-insensitive plugin global mapping and add support for function reference assignments in plugin properties. ([904a947](https://github.com/CoCreate-app/CoCreate-plugins/commit/904a947abc868937b6e650b62e080d00341272a1))
19
+
1
20
  # [1.2.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.1.1...v1.2.0) (2026-02-15)
2
21
 
3
22
 
package/demo/index.html CHANGED
@@ -1,230 +1,131 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
- <head>
4
- <title>Plugins API Demo</title>
5
- <meta charset="utf-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <link rel="manifest" href="/manifest.webmanifest" />
8
-
9
- <!-- Bootstrap 5 CSS for Demo Presentation -->
10
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
11
-
12
- <style>
13
- body { background-color: #f8f9fa; padding-bottom: 50px; }
14
- .demo-card { margin-bottom: 2rem; border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
15
- .card-header { background-color: #fff; border-bottom: 1px solid #eee; padding: 1.5rem; }
16
- .operator-badge { font-family: monospace; background: #e9ecef; color: #d63384; padding: 2px 6px; border-radius: 4px; }
17
- </style>
18
- </head>
19
- <body>
3
+ <head>
4
+ <title>Plugins API Test Suite</title>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+
8
+ <!-- UI Framework for Demo -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <!-- Library CSS (Toastify) -->
12
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
13
+
14
+ <style>
15
+ :root {
16
+ --primary-color: #0d6efd;
17
+ }
18
+ body { background-color: #f8fafc; padding-top: 50px; padding-bottom: 100px; font-family: system-ui, -apple-system, sans-serif; }
19
+ .card { border: none; border-radius: 12px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); margin-bottom: 2rem; }
20
+ .card-header { background: #fff; border-bottom: 1px solid #e2e8f0; font-weight: 600; padding: 1.25rem; }
21
+ .code-block { background: #1e293b; color: #f1f5f9; padding: 1rem; border-radius: 8px; font-family: 'Fira Code', monospace; font-size: 0.85rem; margin-top: 1rem; overflow-x: auto; }
22
+ .live-preview { background: #fff; border: 2px dashed #e2e8f0; border-radius: 8px; padding: 2rem; display: flex; align-items: center; justify-content: center; min-height: 140px; }
23
+ .badge-sequential { background-color: #dcfce7; color: #166534; font-size: 0.75rem; padding: 0.25rem 0.6rem; border-radius: 9999px; font-weight: 600; margin-bottom: 0.5rem; display: inline-block; }
24
+ </style>
25
+ </head>
26
+ <body>
20
27
 
21
- <div class="container py-5">
22
- <div class="row mb-4">
23
- <div class="col-12 text-center">
24
- <h1 class="display-5 fw-bold text-primary">CoCreate Plugins API</h1>
25
- <p class="lead text-muted">A declarative, attribute-based system for initializing and configuring JavaScript libraries.</p>
26
- </div>
27
- </div>
28
+ <div class="container" style="max-width: 800px;">
29
+ <div class="text-center mb-5">
30
+ <h1 class="fw-bold">Linear Execution Test Suite</h1>
31
+ <p class="text-muted">Testing the sequential processing of HTML attributes as logic.</p>
32
+ </div>
28
33
 
29
- <!-- SECTION 1: THE BASICS -->
30
- <div class="row">
31
- <div class="col-12">
32
- <div class="card demo-card">
33
- <div class="card-header">
34
- <h4 class="m-0">1. The Basics: Declarative Initialization</h4>
35
- </div>
36
- <div class="card-body">
37
- <p>Initialize any supported plugin using the <code>plugin="PluginName"</code> attribute. Configure it using the lowercase attribute matching the plugin name (e.g., <code>pluginname="{ options }"</code>).</p>
38
-
39
- <div class="row align-items-center">
40
- <div class="col-md-6">
41
- <h6 class="text-muted text-uppercase fs-7 fw-bold mb-3">Live Demo</h6>
42
- <!-- Toastify Example -->
43
- <button
44
- type="button"
45
- class="btn btn-success"
46
- plugin="Toastify"
47
- toastify='{
48
- "text": "Hello! I am a declarative toast.",
49
- "duration": 3000,
50
- "gravity": "top",
51
- "position": "right",
52
- "style": { "background": "#198754" }
53
- }'
54
- onclick="this.Toastify.showToast()">
55
- Click me to Show Toast
56
- </button>
57
- </div>
58
- <div class="col-md-6">
59
- <h6 class="text-muted text-uppercase fs-7 fw-bold mb-3">Code</h6>
60
- <pre><code class="language-html">&lt;button
61
- plugin="Toastify"
62
- toastify='{
63
- "text": "Hello!",
64
- "duration": 3000,
65
- "style": { "background": "#198754" }
66
- }'
67
- onclick="this.Toastify.showToast()"&gt;
68
- Click Me
69
- &lt;/button&gt;</code></pre>
70
- </div>
71
- </div>
72
- </div>
34
+ <!-- TEST 1: Dot-Notation + Method Execution -->
35
+ <div class="card">
36
+ <div class="card-header">1. Sequential Property & Method Call</div>
37
+ <div class="card-body">
38
+ <p class="text-secondary small">This button sets multiple properties on the Toastify instance and then calls the execution method, all in order.</p>
39
+ <div class="live-preview flex-column">
40
+ <button type="button" class="btn btn-primary px-4"
41
+ plugin="Toastify"
42
+ Toastify.text="Step-by-step logic works!"
43
+ Toastify.duration="4000"
44
+ Toastify.style.background="linear-gradient(to right, #00b09b, #96c93d)"
45
+ Toastify.showToast()="">
46
+ Trigger Sequential Toast
47
+ </button>
48
+ <div class="code-block w-100">
49
+ plugin="Toastify" <br>
50
+ Toastify.text="..." <br>
51
+ Toastify.showToast()=""
73
52
  </div>
74
53
  </div>
75
54
  </div>
55
+ </div>
76
56
 
77
- <!-- SECTION 2: OPERATORS & VARIABLES -->
78
- <div class="row">
79
- <div class="col-12">
80
- <div class="card demo-card">
81
- <div class="card-header">
82
- <h4 class="m-0">2. Variable Operators</h4>
83
- </div>
84
- <div class="card-body">
85
- <p>The API supports special variables to reference DOM elements and global objects dynamically within your JSON configuration.</p>
86
-
87
- <div class="table-responsive mb-4">
88
- <table class="table table-bordered">
89
- <thead class="table-light">
90
- <tr>
91
- <th>Operator</th>
92
- <th>Description</th>
93
- <th>Usage Example</th>
94
- </tr>
95
- </thead>
96
- <tbody>
97
- <tr>
98
- <td><span class="operator-badge">$this</span></td>
99
- <td>References the current DOM element where the attribute is placed.</td>
100
- <td><code>"element": "$this"</code></td>
101
- </tr>
102
- <tr>
103
- <td><span class="operator-badge">$window</span></td>
104
- <td>Accesses the global <code>window</code> object (useful for callbacks).</td>
105
- <td><code>"callback": "$window.console.log"</code></td>
106
- </tr>
107
- </tbody>
108
- </table>
109
- </div>
110
-
111
- <!-- RaterJs Example ($this) -->
112
- <div class="border rounded p-3 bg-light mb-3">
113
- <h5 class="mb-3">Example: Using <code>$this</code> for Element Binding</h5>
114
- <div class="row">
115
- <div class="col-md-6">
116
- <!-- RaterJs Implementation -->
117
- <div dir="ltr"
118
- plugin="raterJs"
119
- raterjs='[{
120
- "element": "$this",
121
- "starSize": 32,
122
- "rating": 3.5,
123
- "step": 0.5
124
- }]'>
125
- </div>
126
- </div>
127
- <div class="col-md-6">
128
- <pre><code class="language-html">&lt;div
129
- plugin="raterJs"
130
- raterjs='[{
131
- "element": "$this",
132
- "starSize": 32,
133
- "rating": 3.5
134
- }]'&gt;
135
- &lt;/div&gt;</code></pre>
136
- </div>
137
- </div>
138
- </div>
139
-
140
- </div>
57
+ <!-- TEST 2: JSON Object + Method Call -->
58
+ <div class="card">
59
+ <div class="card-header">2. Object-Based Initialization</div>
60
+ <div class="card-body">
61
+ <p class="text-secondary small">Initializing using a full JSON object, followed immediately by a method call.</p>
62
+ <div class="live-preview flex-column">
63
+ <button type="button" class="btn btn-dark px-4"
64
+ plugin="Toastify"
65
+ Toastify()='{"text": "JSON Object Booted!", "gravity": "bottom", "position": "left"}'
66
+ Toastify.showToast()="">
67
+ Trigger JSON Boot
68
+ </button>
69
+ <div class="code-block w-100">
70
+ plugin="Toastify" <br>
71
+ Toastify='{"text": "..."}' <br>
72
+ $Toastify.showToast()=""
141
73
  </div>
142
74
  </div>
143
75
  </div>
76
+ </div>
144
77
 
145
- <!-- SECTION 3: CONFIGURATION PATTERNS -->
146
- <div class="row">
147
- <div class="col-12">
148
- <div class="card demo-card">
149
- <div class="card-header">
150
- <h4 class="m-0">3. Configuration Patterns (Array vs. Object)</h4>
151
- </div>
152
- <div class="card-body">
153
- <p>Plugins have different constructor signatures. The API handles both by inspecting the JSON structure.</p>
154
-
155
- <div class="row">
156
- <div class="col-md-6">
157
- <div class="card h-100 bg-light border-0">
158
- <div class="card-body">
159
- <h6 class="fw-bold">Pattern A: Single Config Object</h6>
160
- <p class="small text-muted">Used when the plugin accepts a single object containing all settings (e.g., <code>raterJs({ element: div })</code>).</p>
161
- <hr>
162
- <code>config='[{ "element": "$this", "opt": "val" }]'</code>
163
- <br><small class="text-danger">*Note: Enclosed in an array to represent arguments list.</small>
164
- </div>
165
- </div>
166
- </div>
167
- <div class="col-md-6">
168
- <div class="card h-100 bg-light border-0">
169
- <div class="card-body">
170
- <h6 class="fw-bold">Pattern B: Argument List</h6>
171
- <p class="small text-muted">Used when the plugin accepts multiple distinct arguments (e.g., <code>Swiper(element, options)</code>).</p>
172
- <hr>
173
- <code>config='["$this", { "opt": "val" }]'</code>
174
- </div>
175
- </div>
176
- </div>
177
- </div>
178
- </div>
78
+ <!-- TEST 3: Interleaved $this and Plugin Logic -->
79
+ <div class="card">
80
+ <div class="card-header">3. Interleaved Element & Plugin Logic</div>
81
+ <div class="card-body">
82
+ <p class="text-secondary small">This test changes the button's own style using <code>$this</code> before triggering the plugin logic.</p>
83
+ <div class="live-preview flex-column">
84
+ <button type="button" class="btn btn-outline-danger px-4"
85
+ $this.style.borderRadius="0px"
86
+ $this.innerText="Order Processed"
87
+ plugin="Toastify"
88
+ Toastify.text="Element changed first!"
89
+ Toastify.showToast()>
90
+ Change Me & Toast
91
+ </button>
92
+ <div class="code-block w-100">
93
+ $this.style.borderRadius="0px" <br>
94
+ plugin="Toastify" <br>
95
+ Toastify.showToast()=""
179
96
  </div>
180
97
  </div>
181
98
  </div>
182
-
183
- <!-- SECTION 4: ADVANCED (Callbacks) -->
184
- <div class="row">
185
- <div class="col-12">
186
- <div class="card demo-card">
187
- <div class="card-header">
188
- <h4 class="m-0">4. Advanced: Callbacks & Global Functions</h4>
189
- </div>
190
- <div class="card-body">
191
- <p>Use <code>$window</code> or specific global names (like <code>$Swal</code>) to pass functions into configuration attributes.</p>
192
-
193
- <div class="row align-items-center">
194
- <div class="col-md-6">
195
- <!-- SweetAlert Example -->
196
- <button
197
- type="button"
198
- class="btn btn-primary"
199
- onclick="Swal.fire({
200
- title: 'Are you sure?',
201
- text: 'You can define this entire config in HTML!',
202
- icon: 'warning',
203
- showCancelButton: true,
204
- confirmButtonText: 'Yes, delete it!'
205
- })">
206
- Trigger SweetAlert
207
- </button>
208
- </div>
209
- <div class="col-md-6">
210
- <pre><code class="language-html">&lt;button
211
- onclick="Swal.fire({
212
- title: 'Ready?',
213
- preConfirm: '$window.myCustomFunction'
214
- }) &gt;
215
- Launch
216
- &lt;/button&gt;</code></pre>
217
- </div>
218
- </div>
219
- </div>
220
- </div>
99
+ </div>
100
+
101
+ <!-- TEST 4: Input State Manipulation -->
102
+ <div class="card">
103
+ <div class="card-header">4. Input Property Manipulation</div>
104
+ <div class="card-body">
105
+ <p class="text-secondary small">Directly setting properties on an input element on load.</p>
106
+ <div class="live-preview">
107
+ <input type="text" class="form-control"
108
+ $this.value="CoCreate Linear Engine"
109
+ $this.style.border="2px solid #0d6efd"
110
+ $this.placeholder="Wait for it..."
111
+ plugin="Toastify"
112
+ Toastify.text="Input properties applied"
113
+ Toastify.duration="2000"
114
+ Toastify.showToast()="">
221
115
  </div>
222
116
  </div>
223
-
224
117
  </div>
225
118
 
226
- <!-- CoCreate Engine -->
227
- <script src="https://CoCreate.app/dist/CoCreate.js"></script>
119
+ </div>
120
+
121
+ <!-- CoCreate JS Bundle (Contains plugin.js and operatorEngine.js) -->
122
+ <script src="../../../CoCreateJS/src/dist/CoCreate.js"></script>
123
+
124
+ <!-- External Plugin Library -->
125
+ <script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
228
126
 
229
- </body>
127
+ <script type="module">
128
+ console.log("Test Suite Ready. The 'plugin.js' sequencer will now process attributes in order.");
129
+ </script>
130
+ </body>
230
131
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/plugins",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "CoCreate plugins",
5
5
  "author": "CoCreate LLC",
6
6
  "license": "AGPL-3.0",
package/src/index.js CHANGED
@@ -1,421 +1,248 @@
1
1
  import Observer from "@cocreate/observer";
2
- import {dotNotationToObject} from "@cocreate/utils";
2
+ import { processOperatorsAsync } from "@cocreate/utils";
3
3
 
4
4
  /**
5
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.
6
+ * @property {Array<string|Object>} [js] - List of JS files to load.
7
7
  * @property {Array<string>} [css] - List of CSS files to load.
8
8
  */
9
9
 
10
10
  // --- CONFIGURATION ---
11
-
12
- /**
13
- * @type {Object.<string, PluginDefinition>}
14
- * Configuration object containing plugin definitions.
15
- * Populated dynamically from CoCreate.config.js or defaults.
16
- */
17
11
  const plugins = {};
18
12
 
19
13
  // --- 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
14
  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
15
  const cssMarker = typeof document !== 'undefined' ? document.querySelector('link[plugins]') : null;
34
16
 
35
17
  /**
36
18
  * 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}
40
19
  */
41
- function init(elements) {
20
+ function init(elements) {
42
21
  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
-
22
+ let collection = (elements instanceof HTMLElement || elements instanceof Element) ? [elements] : Array.from(elements || []);
53
23
  collection.forEach(el => {
54
- if (el && typeof el.getAttribute === 'function') {
55
- processPlugin(el);
56
- }
24
+ if (el && typeof el.getAttribute === 'function') processPlugin(el);
57
25
  });
58
26
  }
59
27
 
60
28
  /**
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.
29
+ * Processes resources and executes attributes in strict DOM order.
30
+ * The attribute list IS the script. We iterate once and execute everything in sequence.
66
31
  */
67
32
  async function processPlugin(el) {
68
33
  const rawAttr = el.getAttribute("plugin");
69
- if (!rawAttr) return;
70
-
71
- const pluginNames = rawAttr.split(',').map(s => s.trim());
34
+ const pluginNames = rawAttr ? rawAttr.split(',').map(s => s.trim()) : [];
72
35
 
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
36
+ // 1. RESOURCE PRE-LOADING
37
+ for (const name of pluginNames) {
38
+ const pluginDef = plugins[name];
39
+
40
+ if (pluginDef && !window[name]) {
79
41
  if (pluginDef.css) pluginDef.css.forEach(href => {
80
42
  if (!document.querySelector(`link[href="${href}"]`)) {
81
43
  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
- }
104
- }
44
+ link.rel = "stylesheet"; link.href = href;
45
+ if (cssMarker) cssMarker.parentNode.insertBefore(link, cssMarker);
46
+ else document.head.appendChild(link);
105
47
  }
106
48
  });
107
49
 
108
- // Load JS with Promise Cache
109
50
  if (pluginDef.js) {
110
51
  for (const item of pluginDef.js) {
111
52
  const src = typeof item === 'string' ? item : item.src;
112
- const integrity = typeof item === 'object' ? item.integrity : null;
113
- const crossOrigin = typeof item === 'object' ? item.crossOrigin : null;
114
-
53
+ if (!src) continue;
115
54
  if (!scriptCache.has(src)) {
116
- const scriptPromise = new Promise((resolve, reject) => {
117
- // Check if already in DOM (manual load)
118
- const existing = document.querySelector(`script[src="${src}"]`);
119
- if (existing) {
120
- if (existing.dataset.loaded === "true") {
121
- resolve();
122
- } else {
123
- const prevOnload = existing.onload;
124
- existing.onload = () => {
125
- if (prevOnload) prevOnload();
126
- existing.dataset.loaded = "true";
127
- resolve();
128
- };
129
- existing.onerror = reject;
130
- }
131
- } else {
132
- const s = document.createElement("script");
133
- s.src = src;
134
- if (integrity) {
135
- s.integrity = integrity;
136
- s.crossOrigin = crossOrigin || "anonymous";
137
- }
138
- s.onload = () => {
139
- s.dataset.loaded = "true";
140
- resolve();
141
- };
142
- s.onerror = reject;
55
+ const existingScript = document.querySelector(`script[src*="${src}"]`);
56
+ if (existingScript && (existingScript.dataset.loaded === "true" || window[name])) {
57
+ scriptCache.set(src, Promise.resolve());
58
+ } else {
59
+ const s = document.createElement("script");
60
+ s.src = src;
61
+ scriptCache.set(src, new Promise((resolve) => {
62
+ s.onload = () => { s.dataset.loaded = "true"; resolve(); };
63
+ s.onerror = () => resolve();
143
64
  document.head.appendChild(s);
144
- }
145
- });
146
- scriptCache.set(src, scriptPromise);
147
- }
148
-
149
- try {
150
- await scriptCache.get(src);
151
- } catch (e) {
152
- console.error(`Failed to load script: ${src}`, e);
65
+ }));
66
+ }
153
67
  }
68
+ await scriptCache.get(src);
154
69
  }
155
70
  }
156
71
  }
157
-
158
- // Attempt to execute plugin even if no config was found (it might be on window already)
159
- executeGenericPlugin(el, pluginName);
160
72
  }
161
- }
162
-
163
- /**
164
- * Helper to determine if a function should be called with 'new'.
165
- * Uses heuristics like ES6 class syntax, lack of prototype (arrow function), or PascalCase naming.
166
- * * @param {Function} func - The function to check.
167
- * @param {string} [name] - The property name associated with the function (for casing check).
168
- * @returns {boolean} True if the function appears to be a constructor.
169
- */
170
- const isConstructor = (func, name) => {
171
- try {
172
- if (typeof func !== 'function') return false;
173
- if (/^\s*class\s+/.test(func.toString())) return true;
174
- if (!func.prototype) return false;
175
- const n = name || func.name;
176
- if (n && /^[A-Z]/.test(n)) return true;
177
- } catch(e) {}
178
- return false;
179
- };
180
73
 
181
- /**
182
- * Executes the logic for a generic plugin on a specific element.
183
- * Handles:
184
- * 1. Resolving the target class/function from window.
185
- * 2. Initializing the base instance.
186
- * 3. Processing attribute paths and nested JSON objects to execute methods or set properties.
187
- * * @param {HTMLElement} el - The target element.
188
- * @param {string} name - The name of the plugin (case-insensitive identifier).
189
- * @returns {void}
190
- */
191
- function executeGenericPlugin(el, name) {
192
- const prefix = name.toLowerCase();
193
- const mainAttr = el.getAttribute(prefix);
194
- let rawData = {};
195
-
196
- for (let attr of el.attributes) {
197
- let key = attr.name;
198
- if (key === prefix) {
199
- key = name;
200
- } else if (key.startsWith(prefix + '-')) {
201
- key = key.replaceAll("-", ".");
202
- } else if (!key.startsWith(prefix + '.')) {
203
- continue
74
+ // 2. LINEAR ATTRIBUTE SCRIPT EXECUTION
75
+ const attributes = Array.from(el.attributes);
76
+ for (let i = 0; i < attributes.length; i++) {
77
+ const attr = attributes[i];
78
+ const attrName = attr.name;
79
+ const attrLower = attrName.toLowerCase();
80
+
81
+ const isDirectThis = attrName.startsWith('$this.');
82
+ let cleanAttrName = attrLower;
83
+ if (isDirectThis) {
84
+ cleanAttrName = attrLower.substring(6); // Strip '$this.' for plugin matching
204
85
  }
205
86
 
206
- try {
207
- rawData[key] = JSON.parse(attr.value);
208
- } catch(e) {
209
- rawData[key] = attr.value;
210
- }
87
+ // Router: Find if this matches a listed plugin
88
+ const activePluginName = pluginNames.find(p => cleanAttrName === p.toLowerCase() || cleanAttrName.startsWith(p.toLowerCase() + '.'));
211
89
 
212
- };
90
+ if (!activePluginName && !isDirectThis) continue;
213
91
 
214
- // 2. Resolve parameters (Token Resolver)
215
- let resolved = processParams(el, rawData);
216
- resolved = dotNotationToObject(resolved);
92
+ try {
93
+ let val = attr.value.trim();
94
+ if (val === "") val = "true";
217
95
 
218
- let Plugin = window[name] || window[prefix];
219
- if (!Plugin) {
220
- console.error(`Plugin for ${name} not found on window.`);
221
- return;
222
- }
96
+ // BOOTSTRAPPING (Initialize on First Sight via Operator Engine)
97
+ let existingInstance = activePluginName && el[activePluginName];
223
98
 
224
- // Iterate over resolved object.
225
- // Since we use dotNotationToObject, keys like "swiper.effect" are already nested as { swiper: { effect: ... } }
226
- for (let key in resolved) {
227
- // We generally expect the root key to match the plugin name (e.g., 'swiper')
228
- // We unwrap this root key to pass the actual config to the Plugin.
229
- if (key === name || key.toLowerCase() === prefix) {
230
- try {
231
- // Determine Target: Use existing instance on element if available, else use Window Plugin
232
- let Target = el[name] || Plugin;
233
- let val = resolved[key];
99
+ if (activePluginName && !existingInstance) {
100
+ const initVal = (cleanAttrName === activePluginName.toLowerCase()) ? val : null;
234
101
 
235
- // Pass context: Window as parent, Plugin Name as property (for potential context binding)
236
- // el and name used to store the result on the element.
237
- update(Target, val, window, name, el, name);
238
-
239
- console.log(`Processed ${name}`);
240
- } catch (e) {
241
- console.error(`Error processing ${name}:`, e);
242
- }
243
- }
244
- }
245
- }
246
-
247
- function update(Target, val, parent, property, elParent, elProperty) {
248
- // RESOLUTION: Handle case-insensitivity before processing targets.
249
- // If Target is missing, check parent for a property matching 'property' (case-insensitive).
250
- if (!Target && parent && property) {
251
- const lowerProp = String(property).toLowerCase();
252
- for (const key in parent) {
253
- if (key.toLowerCase() === lowerProp) {
254
- Target = parent[key];
255
- property = key;
256
- if (elProperty) elProperty = key; // Update element structure key to match real property
257
- break;
258
- }
259
- }
260
- }
102
+ let safeInit = initVal;
103
+ if (initVal) {
104
+ if (!initVal.includes('$') && isNaN(initVal) && initVal !== 'true' && initVal !== 'false' && initVal !== 'null') {
105
+ if (!initVal.startsWith("'") && !initVal.startsWith('"')) safeInit = `'${initVal}'`;
106
+ }
107
+ }
108
+
109
+ // Temporarily expose the library to the element so processOperatorsAsync can find and execute it natively
110
+ if (window[activePluginName]) {
111
+ el[activePluginName] = window[activePluginName];
112
+ }
261
113
 
262
- let instance;
263
- if (typeof Target === 'function') {
264
- if (!isConstructor(Target, property)) {
265
- // Call as a function (method or standalone)
266
- // Use 'parent' as context (this) if available to maintain class references
267
- if (parent) {
268
- if (Array.isArray(val)) {
269
- instance = Target.apply(parent, val);
270
- } else {
271
- instance = Target.call(parent, val);
114
+ // Construct initialization string (e.g., $Toastify(...) )
115
+ let initExpr = safeInit ? `$${activePluginName}(${safeInit})` : `$${activePluginName}()`;
116
+
117
+ // Await initialization from the operator engine
118
+ const initResult = await processOperatorsAsync(el, initExpr, [], null, [], new Map([["$this", el]]));
119
+
120
+ if (initResult !== undefined && initResult !== null && initResult !== "") {
121
+ // Overwrite the temporary library function with the returned instance
122
+ el[activePluginName] = initResult;
123
+ existingInstance = initResult;
272
124
  }
273
- } else {
274
- if (Array.isArray(val)) {
275
- instance = Target(...val);
276
- } else {
277
- instance = Target(val);
125
+
126
+ // If this attribute was the base initializer, we're done processing it
127
+ if (cleanAttrName === activePluginName.toLowerCase()) {
128
+ continue;
278
129
  }
279
130
  }
280
- } else {
281
- // Call as a Constructor
282
- if (Array.isArray(val)) {
283
- instance = new Target(...val);
131
+
132
+ // --- CASE-SENSITIVE PATH RESOLUTION ---
133
+ // Reconstruct the attribute path with correct casing from the instance/element deeply
134
+ let keyParts = attrName.split('.');
135
+ let resolvedParts = [];
136
+ let pointer = null;
137
+
138
+ if (isDirectThis) {
139
+ resolvedParts.push('$this');
140
+ pointer = el;
284
141
  } else {
285
- instance = new Target(val);
142
+ resolvedParts.push(activePluginName);
143
+ pointer = existingInstance || el; // Fallback to element if instance isn't populated
286
144
  }
287
- }
288
-
289
- // Assign the result to the element structure
290
- if (elParent && elProperty) {
291
- elParent[elProperty] = instance;
292
- }
293
145
 
294
- } else if (typeof Target === 'object' && Target !== null && typeof val === 'object' && val !== null && !Array.isArray(val)) {
295
- // Prepare the next level of the element structure
296
- if (elParent && elProperty) {
297
- if (!elParent[elProperty]) {
298
- elParent[elProperty] = {};
299
- }
300
- const nextElParent = elParent[elProperty];
301
-
302
- for (let key in val) {
303
- update(Target[key], val[key], Target, key, nextElParent, key);
304
- }
305
- }
306
- } else if (parent && property) {
307
- // If it's not a function, we are setting a value on the plugin object
308
- parent[property] = val;
309
-
310
- // Map the value to the element structure
311
- if (elParent && elProperty) {
312
- elParent[elProperty] = val;
313
- }
314
-
315
- console.log(`Set plugin property ${property} to`, val);
316
- }
317
- }
146
+ for (let j = 1; j < keyParts.length; j++) {
147
+ let part = keyParts[j];
148
+ let isMethod = part.endsWith('()');
149
+ let cleanPart = part.replace('()', '');
150
+ let matchedKey = part;
318
151
 
319
- /**
320
- * Generic Parameter Processor
321
- * Handles:
322
- * - $this / $this.children
323
- * - $window.path.to.function(arg)
324
- * - $anime.stagger(100)
325
- * - Global access: $document, $window, etc.
326
- */
327
- function processParams(el, params) {
328
- if (typeof params === 'string' && params.startsWith('\u0024')) {
329
- try {
330
- // 1. Check for Method Call: $root.path.to.func(arg)
331
- const callMatch = params.match(/^\u0024([^.]+)\.(.+)\((.*)\)$/);
332
- if (callMatch) {
333
- const [_, root, path, arg] = callMatch;
334
- const obj = (root === 'this') ? el : window[root];
335
-
336
- // If root object exists, drill down
337
- if (obj) {
338
- const func = path.split('.').reduce((o, k) => (o || {})[k], obj);
339
- if (typeof func === 'function') {
340
- // Parse argument if JSON-like, else string
341
- const parsedArg = arg ? (function() { try { return JSON.parse(arg); } catch(e) { return arg; } })() : undefined;
342
- return func(parsedArg);
152
+ if (pointer != null) {
153
+ if (pointer[cleanPart] !== undefined) {
154
+ matchedKey = part;
155
+ } else {
156
+ let lower = cleanPart.toLowerCase();
157
+ let realKey = null;
158
+
159
+ let currentObj = pointer;
160
+ while (currentObj) {
161
+ let props = Object.getOwnPropertyNames(currentObj);
162
+ let found = props.find(p => p.toLowerCase() === lower);
163
+ if (found) {
164
+ realKey = found;
165
+ break;
166
+ }
167
+ currentObj = Object.getPrototypeOf(currentObj);
168
+ }
169
+
170
+ if (realKey) {
171
+ matchedKey = isMethod ? realKey + '()' : realKey;
172
+ }
343
173
  }
174
+ pointer = pointer[matchedKey.replace('()', '')];
344
175
  }
176
+ resolvedParts.push(matchedKey);
345
177
  }
346
178
 
347
- // 2. Check for Property Access: $root.path.to.prop or just $root
348
- const propMatch = params.match(/^\u0024([^.]+)(?:\.(.+))?$/);
349
- if (propMatch) {
350
- const [_, root, path] = propMatch;
351
- const obj = (root === 'this') ? el : window[root];
352
-
353
- if (obj) {
354
- if (!path) return (obj instanceof HTMLCollection) ? Array.from(obj) : obj;
355
-
356
- const val = path.split('.').reduce((o, k) => (o || {})[k], obj);
357
- // Convert HTMLCollections to Arrays
358
- return (val instanceof HTMLCollection) ? Array.from(val) : val;
179
+ let correctedAttrName = resolvedParts.join('.');
180
+
181
+ // --- DECORATION & EXECUTION ---
182
+ // Plugins must be prefixed with $ for the operator engine's property resolution
183
+ let prefixedAttrName = correctedAttrName;
184
+ if (activePluginName && !isDirectThis) {
185
+ prefixedAttrName = "$" + correctedAttrName;
186
+ }
187
+
188
+ const isMethodCall = correctedAttrName.includes('(') || correctedAttrName.endsWith('()');
189
+ let expression = "";
190
+
191
+ if (isMethodCall) {
192
+ expression = prefixedAttrName;
193
+ } else {
194
+ let safeValue = val;
195
+ if (!val.includes('$') && isNaN(val) && val !== 'true' && val !== 'false' && val !== 'null') {
196
+ if (!val.startsWith("'") && !val.startsWith('"')) safeValue = `'${val}'`;
359
197
  }
198
+ expression = `${prefixedAttrName} = ${safeValue}`;
360
199
  }
361
200
 
362
- // 3. Check for standalone globals like $document or $window
363
- const globalKey = params.substring(1);
364
- if (window[globalKey]) {
365
- return window[globalKey];
201
+ // Execute using the existing processOperatorsAsync system.
202
+ const result = await processOperatorsAsync(el, expression, [], null, [], new Map([["$this", el]]));
203
+
204
+ // CAPTURE & ASSIGN: If the element still lacks the instance, save the result of the execution to the element
205
+ if (activePluginName && !el[activePluginName] && result !== undefined && result !== null && result !== "") {
206
+ el[activePluginName] = result;
366
207
  }
367
208
 
368
209
  } catch (e) {
369
- console.warn("Failed to resolve dynamic token:", params);
210
+ console.warn(`[Plugin System] Sequential Execution Error (${attrName}):`, e);
370
211
  }
371
212
  }
372
-
373
- if (Array.isArray(params)) return params.map(p => processParams(el, p));
374
- if (typeof params === 'object' && params !== null) {
375
- const res = {};
376
- for (let k in params) res[k] = processParams(el, params[k]);
377
- return res;
213
+
214
+ // 3. FALLBACK BOOT
215
+ for (const name of pluginNames) {
216
+ if (!el[name]) {
217
+ if (window[name]) {
218
+ el[name] = window[name];
219
+ }
220
+ const initResult = await processOperatorsAsync(el, `$${name}()`, [], null, [], new Map([["$this", el]]));
221
+ if (initResult !== undefined && initResult !== null && initResult !== "") {
222
+ el[name] = initResult;
223
+ }
224
+ }
378
225
  }
379
- return params;
380
226
  }
381
227
 
382
- // --- STARTUP LOGIC ---
383
-
228
+ // Global Startup
384
229
  if (typeof document !== 'undefined') {
385
- // Dynamic Import: Loads config if available, handles error if missing.
386
- // Works with 'npm start' (Bundlers) by creating a code-split chunk.
230
+ const selector = "[plugin]";
231
+ Observer.init({
232
+ name: "plugin",
233
+ types: ["addedNodes", "attributes"],
234
+ selector: selector,
235
+ attributeFilter: ["plugin"],
236
+ callback: (mutation) => init(mutation.target)
237
+ });
238
+
387
239
  import("./CoCreate.config.js")
388
240
  .then((Config) => {
389
- // LOGIC: Merge exports into plugins object
390
- if (Config.plugins) {
391
- Object.assign(plugins, Config.plugins);
392
- }
393
- else if (Config.default) {
394
- if (Config.default.plugins) {
395
- Object.assign(plugins, Config.default.plugins);
396
- } else {
397
- Object.assign(plugins, Config.default);
398
- }
399
- }
400
- })
401
- .catch((err) => {
402
- // Optional: fail silently for optional config
241
+ const data = Config.plugins || Config.default?.plugins || Config.default || {};
242
+ Object.assign(plugins, data);
403
243
  })
404
- .finally(() => {
405
- // Start Observer
406
- Observer.init({
407
- name: "plugin",
408
- types: ["addedNodes", "attributes"],
409
- selector: "[plugin]",
410
- attributeFilter: ["plugin"],
411
- callback: (mutation) => {
412
- init(mutation.target);
413
- }
414
- });
415
-
416
- // Initial Init
417
- init(document.querySelectorAll("[plugin]"));
418
- });
244
+ .catch(() => {})
245
+ .finally(() => init(document.querySelectorAll(selector)));
419
246
  }
420
247
 
421
- export default { init, plugins }
248
+ export default { init, plugins };