@cocreate/plugins 1.3.0 → 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.
@@ -1,8 +1,10 @@
1
1
  name: Automated Workflow
2
+
2
3
  on:
3
4
  push:
4
5
  branches:
5
6
  - master
7
+
6
8
  jobs:
7
9
  about:
8
10
  runs-on: ubuntu-latest
@@ -18,27 +20,37 @@ jobs:
18
20
  with:
19
21
  direction: overwrite-github
20
22
  githubToken: "${{ secrets.GITHUB }}"
23
+
21
24
  release:
22
25
  runs-on: ubuntu-latest
23
26
  steps:
24
27
  - name: Checkout
25
28
  uses: actions/checkout@v4
29
+ with:
30
+ fetch-depth: 0 # Required so semantic-release can trace git tags/history
31
+
26
32
  - name: Setup Node.js
27
33
  uses: actions/setup-node@v4
28
34
  with:
29
- node-version: 22 # Required for the latest semantic-release plugins
30
- - name: Semantic Release
31
- uses: cycjimmy/semantic-release-action@v4 # Update to v4 for better Node 20+ support
32
- id: semantic
33
- with:
34
- extra_plugins: |
35
- @semantic-release/changelog
35
+ node-version: 22
36
+
37
+ - name: Install Semantic Release & Plugins
38
+ # Installs semantic-release and its plugins on the runner
39
+ run: |
40
+ npm install -g semantic-release \
41
+ @semantic-release/changelog \
42
+ @semantic-release/npm \
43
+ @semantic-release/github \
36
44
  @semantic-release/git
37
- @semantic-release/github
45
+
46
+ - name: Run Semantic Release (Native)
47
+ id: semantic
48
+ # This will automatically pick up your export default config file in the repository root
49
+ run: npx semantic-release
38
50
  env:
39
- GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" # Use the built-in token if possible
51
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
40
52
  NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
53
+
41
54
  outputs:
42
55
  new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
43
- new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
44
-
56
+ new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [1.4.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.3.1...v1.4.0) (2026-07-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * enhance plugin initialization and resource preloading logic ([ee17bc1](https://github.com/CoCreate-app/CoCreate-plugins/commit/ee17bc12b47c09085cd2527ef18a6fce36f21123))
7
+ * improve plugin processing and operator execution logic ([486faa8](https://github.com/CoCreate-app/CoCreate-plugins/commit/486faa81839a49d4958ccb75721b161102cc0168))
8
+ * update automated workflow and release configuration for improved semantic release handling ([06e65c9](https://github.com/CoCreate-app/CoCreate-plugins/commit/06e65c9eddd29398f3bd824e295089722faf9fad))
9
+
10
+ ## [1.3.1](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.3.0...v1.3.1) (2026-07-11)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * bump package versions ([3f992ea](https://github.com/CoCreate-app/CoCreate-plugins/commit/3f992ea145328efac344e4f470fe19a87d5209e9))
16
+ * clean attribute name for method matching in processPlugin function ([7389d12](https://github.com/CoCreate-app/CoCreate-plugins/commit/7389d12bd6f4206c506456d64341ceef4b304313))
17
+ * correct minor typos in code comments and variable names for clarity. ([7e43622](https://github.com/CoCreate-app/CoCreate-plugins/commit/7e4362289c9f51ac6673873802d5b58a7992638e))
18
+ * demo plugin initialization syntax and remove redundant tests ([c126bc8](https://github.com/CoCreate-app/CoCreate-plugins/commit/c126bc87edfb9669ea6c8a79e13a0cd0c09a1bd3))
19
+ * removed post install and add add packages to apprved list ([548be97](https://github.com/CoCreate-app/CoCreate-plugins/commit/548be977ba710af907ad8e95f7ebcd3a75462326))
20
+ * semantic version handling ([7606d54](https://github.com/CoCreate-app/CoCreate-plugins/commit/7606d54cd447a3cf10c3f86d54bca1f0aa7fe1a1))
21
+
1
22
  # [1.3.0](https://github.com/CoCreate-app/CoCreate-plugins/compare/v1.2.1...v1.3.0) (2026-03-11)
2
23
 
3
24
 
package/demo/index.html CHANGED
@@ -38,83 +38,31 @@
38
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
39
  <div class="live-preview flex-column">
40
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()="">
41
+ plugin='{
42
+ "Toastify": {
43
+ "text": "Hello from a single object!",
44
+ "duration": 3000,
45
+ "style": {
46
+ "background": "#0d6efd"
47
+ }
48
+ },
49
+ "Toastify.showToast": []
50
+ }'>
46
51
  Trigger Sequential Toast
47
52
  </button>
48
53
  <div class="code-block w-100">
49
- plugin="Toastify" <br>
50
- Toastify.text="..." <br>
51
- Toastify.showToast()=""
52
- </div>
54
+ plugin='{<br>
55
+ &nbsp;&nbsp;"Toastify": {<br>
56
+ &nbsp;&nbsp;&nbsp;&nbsp;"text": "...",<br>
57
+ &nbsp;&nbsp;&nbsp;&nbsp;"duration": 3000<br>
58
+ &nbsp;&nbsp;},<br>
59
+ &nbsp;&nbsp;"Toastify.showToast": []<br>
60
+ }'
61
+ </div>
53
62
  </div>
54
63
  </div>
55
64
  </div>
56
65
 
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()=""
73
- </div>
74
- </div>
75
- </div>
76
- </div>
77
-
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()=""
96
- </div>
97
- </div>
98
- </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()="">
115
- </div>
116
- </div>
117
- </div>
118
66
 
119
67
  </div>
120
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocreate/plugins",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CoCreate plugins",
5
5
  "author": "CoCreate LLC",
6
6
  "license": "AGPL-3.0",
@@ -10,8 +10,7 @@
10
10
  "scripts": {
11
11
  "start": "npx webpack --config webpack.config.js",
12
12
  "build": "npx webpack --mode=production --config webpack.config.js",
13
- "dev": "npx webpack --config webpack.config.js --watch",
14
- "postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
13
+ "dev": "npx webpack --config webpack.config.js --watch"
15
14
  },
16
15
  "repository": {
17
16
  "type": "git",
@@ -26,16 +25,15 @@
26
25
  "url": "https://github.com/sponsors/CoCreate-app"
27
26
  },
28
27
  "main": "./src/index.js",
28
+ "dependencies": {
29
+ "@cocreate/observer": "^1.19.0"
30
+ },
29
31
  "devDependencies": {
30
- "css-loader": "^5.1.3",
31
- "esbuild": "^0.25.2",
32
- "esbuild-loader": "^4.3.0",
33
- "mini-css-extract-plugin": "^1.5.0",
34
- "webpack": "^5.24.4",
35
- "webpack-cli": "^4.5.0",
36
- "webpack-log": "^3.0.1"
32
+ "@cocreate/webpack": "^1.4.3"
37
33
  },
38
- "dependencies": {
39
- "@cocreate/observer": "^1.18.3"
34
+ "allowScripts": {
35
+ "@cocreate/observer@1.19.0": true,
36
+ "@cocreate/utils@1.42.2": true,
37
+ "@cocreate/webpack@1.4.3": true
40
38
  }
41
39
  }
package/release.config.js CHANGED
@@ -10,12 +10,20 @@ module.exports = {
10
10
  changelogFile: "CHANGELOG.md",
11
11
  },
12
12
  ],
13
- "@semantic-release/npm",
14
- "@semantic-release/github",
13
+ "@semantic-release/npm",
15
14
  [
16
- "@semantic-release/git",
15
+ "@semantic-release/github",
17
16
  {
17
+ successComment: false,
18
+ failTitle: false,
19
+ },
20
+ ],
21
+ [
22
+ "@semantic-release/git",
23
+ {
24
+ // Only stage and commit the changelog and package.json
18
25
  assets: ["CHANGELOG.md", "package.json"],
26
+ message: "chore(release): ${nextRelease.version} [skip ci]",
19
27
  },
20
28
  ],
21
29
  ],
package/src/index.js CHANGED
@@ -1,51 +1,60 @@
1
- import Observer from "@cocreate/observer";
2
- import { processOperatorsAsync } 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.
7
- * @property {Array<string>} [css] - List of CSS files to load.
8
- */
19
+ import Observer from "@cocreate/observer";
20
+ import { processOperators } from "./operators.js";
9
21
 
10
- // --- CONFIGURATION ---
11
22
  const plugins = {};
12
-
13
- // --- CORE ENGINE ---
14
23
  const scriptCache = new Map();
15
24
  const cssMarker = typeof document !== 'undefined' ? document.querySelector('link[plugins]') : null;
16
25
 
17
26
  /**
18
- * Global Initialization Function.
27
+ * Initializes plugins on a collection of HTML elements.
28
+ * @param {HTMLElement|HTMLElement[]|NodeList} elements
19
29
  */
20
- function init(elements) {
30
+ export function init(elements) {
21
31
  if (!elements) return;
22
- let collection = (elements instanceof HTMLElement || elements instanceof Element) ? [elements] : Array.from(elements || []);
32
+ const collection = (elements instanceof HTMLElement || elements instanceof Element)
33
+ ? [elements]
34
+ : Array.from(elements);
35
+
23
36
  collection.forEach(el => {
24
- if (el && typeof el.getAttribute === 'function') processPlugin(el);
37
+ if (el) {
38
+ processPlugin(el);
39
+ }
25
40
  });
26
41
  }
27
42
 
28
- /**
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.
31
- */
32
- async function processPlugin(el) {
33
- const rawAttr = el.getAttribute("plugin");
34
- const pluginNames = rawAttr ? rawAttr.split(',').map(s => s.trim()) : [];
35
-
36
- // 1. RESOURCE PRE-LOADING
43
+ async function preloadResources(pluginNames) {
37
44
  for (const name of pluginNames) {
38
45
  const pluginDef = plugins[name];
39
-
40
46
  if (pluginDef && !window[name]) {
41
- if (pluginDef.css) pluginDef.css.forEach(href => {
42
- if (!document.querySelector(`link[href="${href}"]`)) {
43
- const link = document.createElement("link");
44
- link.rel = "stylesheet"; link.href = href;
45
- if (cssMarker) cssMarker.parentNode.insertBefore(link, cssMarker);
46
- else document.head.appendChild(link);
47
- }
48
- });
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);
55
+ }
56
+ });
57
+ }
49
58
 
50
59
  if (pluginDef.js) {
51
60
  for (const item of pluginDef.js) {
@@ -70,162 +79,161 @@ async function processPlugin(el) {
70
79
  }
71
80
  }
72
81
  }
82
+ }
73
83
 
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
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
99
+ };
100
+
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]: [] }];
85
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));
86
123
 
87
- // Router: Find if this matches a listed plugin
88
- const activePluginName = pluginNames.find(p => cleanAttrName === p.toLowerCase() || cleanAttrName.startsWith(p.toLowerCase() + '.'));
89
-
90
- if (!activePluginName && !isDirectThis) continue;
124
+ // Execute each configuration block sequentially
125
+ for (const configBlock of configs) {
126
+ await executeConfigBlock(context, configBlock);
127
+ }
128
+ } catch (err) {
129
+ console.error("[Plugin System] Failed to parse and execute plugin configuration:", err, rawPluginAttr);
130
+ }
131
+ }
91
132
 
92
- try {
93
- let val = attr.value.trim();
94
- if (val === "") val = "true";
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);
141
+ }
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;
152
+ }
153
+ }
95
154
 
96
- // BOOTSTRAPPING (Initialize on First Sight via Operator Engine)
97
- let existingInstance = activePluginName && el[activePluginName];
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
+ }
98
162
 
99
- if (activePluginName && !existingInstance) {
100
- const initVal = (cleanAttrName === activePluginName.toLowerCase()) ? val : null;
101
-
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
- }
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
+ }
113
174
 
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;
124
- }
175
+ const parts = key.split('.');
176
+ const rootPluginName = parts[0];
125
177
 
126
- // If this attribute was the base initializer, we're done processing it
127
- if (cleanAttrName === activePluginName.toLowerCase()) {
128
- continue;
129
- }
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];
130
183
  }
184
+ }
131
185
 
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;
141
- } else {
142
- resolvedParts.push(activePluginName);
143
- pointer = existingInstance || el; // Fallback to element if instance isn't populated
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];
144
192
  }
145
193
 
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;
151
-
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
- }
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
+ }
169
200
 
170
- if (realKey) {
171
- matchedKey = isMethod ? realKey + '()' : realKey;
172
- }
173
- }
174
- pointer = pointer[matchedKey.replace('()', '')];
175
- }
176
- resolvedParts.push(matchedKey);
177
- }
178
-
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;
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]];
186
209
  }
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}'`;
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];
197
215
  }
198
- expression = `${prefixedAttrName} = ${safeValue}`;
199
- }
200
-
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;
216
+ await method.apply(parent, methodArgs);
217
+ continue;
207
218
  }
208
-
209
- } catch (e) {
210
- console.warn(`[Plugin System] Sequential Execution Error (${attrName}):`, e);
211
219
  }
212
- }
213
220
 
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;
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];
223
227
  }
228
+ await context.plugin[key].apply(context.plugin, methodArgs);
229
+ continue;
224
230
  }
231
+
232
+ // 7. Fallback: Process operators or generic actions on properties
233
+ await processOperators(context, { [key]: value });
225
234
  }
226
235
  }
227
236
 
228
- // Global Startup
229
237
  if (typeof document !== 'undefined') {
230
238
  const selector = "[plugin]";
231
239
  Observer.init({
@@ -243,6 +251,4 @@ if (typeof document !== 'undefined') {
243
251
  })
244
252
  .catch(() => {})
245
253
  .finally(() => init(document.querySelectorAll(selector)));
246
- }
247
-
248
- export default { init, plugins };
254
+ }
@@ -0,0 +1,293 @@
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
+ ********************************************************************************/
18
+
19
+ import { getValueFromObject, queryElements } from "@cocreate/utils";
20
+
21
+ const IsomorphicGlobals = {
22
+ Math,
23
+ Number,
24
+ String,
25
+ Boolean,
26
+ Date,
27
+ JSON,
28
+ Array,
29
+ Object,
30
+ parseInt,
31
+ parseFloat,
32
+ encodeURIComponent,
33
+ decodeURIComponent,
34
+ btoa: (str) => {
35
+ if (typeof btoa === 'function') return btoa(String(str));
36
+ return Buffer.from(String(str)).toString("base64");
37
+ },
38
+ atob: (b64) => {
39
+ if (typeof atob === 'function') return atob(b64);
40
+ return Buffer.from(String(b64), "base64").toString("utf-8");
41
+ },
42
+ concat: (...args) => args.flat().map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(''),
43
+
44
+ // Core Asynchronous Flow Control Operators
45
+ await: {
46
+ all: async (promises) => Promise.all(promises),
47
+ any: async (promises) => Promise.any(promises),
48
+ race: async (promises) => Promise.race(promises),
49
+ allSettled: async (promises) => Promise.allSettled(promises),
50
+ delay: (ms) => new Promise(resolve => setTimeout(resolve, Number(ms) || 0))
51
+ }
52
+ };
53
+
54
+ const BrowserGlobals = {
55
+ get $organization_id() { return typeof localStorage !== 'undefined' ? localStorage.getItem("organization_id") : ""; },
56
+ get $user_id() { return typeof localStorage !== 'undefined' ? localStorage.getItem("user_id") : ""; },
57
+ get $client_id() { return typeof localStorage !== 'undefined' ? localStorage.getItem("clientId") : ""; },
58
+ get $session_id() { return typeof localStorage !== 'undefined' ? localStorage.getItem("session_id") : ""; },
59
+ get $innerWidth() { return typeof window !== 'undefined' ? window.innerWidth : 0; },
60
+ get $innerHeight() { return typeof window !== 'undefined' ? window.innerHeight : 0; },
61
+ get $href() { return typeof window !== 'undefined' ? window.location.href.replace(/\/$/, "") : ""; },
62
+ get $origin() { return typeof window !== 'undefined' ? window.location.origin : ""; },
63
+ get $protocol() { return typeof window !== 'undefined' ? window.location.protocol : ""; },
64
+ get $hostname() { return typeof window !== 'undefined' ? window.location.hostname : ""; },
65
+ get $host() { return typeof window !== 'undefined' ? window.location.host : ""; },
66
+ get $port() { return typeof window !== 'undefined' ? window.location.port : ""; },
67
+ get $pathname() { return typeof window !== 'undefined' ? window.location.pathname.replace(/\/$/, "") : ""; },
68
+ get $hash() { return typeof window !== 'undefined' ? window.location.hash : ""; },
69
+ get $subdomain() {
70
+ if (typeof window === 'undefined') return "";
71
+ const parts = window.location.hostname.split('.');
72
+ return parts.length > 2 ? parts[0] : "";
73
+ },
74
+ get $relativePath() {
75
+ if (typeof window === 'undefined') return "./";
76
+ const currentPath = window.location.pathname.replace(/\/[^\/]*$/, "");
77
+ const depth = currentPath.split("/").filter(Boolean).length;
78
+ return depth > 0 ? "../".repeat(depth) : "./";
79
+ },
80
+ get $path() {
81
+ if (typeof window === 'undefined') return "";
82
+ let path = window.location.pathname;
83
+ if (path.split("/").pop().includes(".")) path = path.replace(/\/[^\/]+$/, "/");
84
+ return path === "/" ? "" : path;
85
+ },
86
+ $true: () => true,
87
+ $false: () => false,
88
+ $parse: (value = "") => {
89
+ try { return JSON.parse(value); } catch (e) { return value; }
90
+ },
91
+ $query: function(selector) {
92
+ if (typeof document === 'undefined') return undefined;
93
+ let element = this?.element || null;
94
+ if (typeof selector === 'string') {
95
+ selector = selector.trim().replace(/^['"]|['"]$/g, '');
96
+ }
97
+ try {
98
+ const results = queryElements({ element, selector });
99
+ return results && results.length > 0 ? results[0] : undefined;
100
+ } catch (error) {
101
+ console.warn(`[AST Engine] Invalid $query selector => "${selector}"`, error);
102
+ return undefined;
103
+ }
104
+ },
105
+ $queryAll: function(selector) {
106
+ if (typeof document === 'undefined') return [];
107
+ let element = this?.element || null;
108
+ if (typeof selector === 'string') {
109
+ selector = selector.trim().replace(/^['"]|['"]$/g, '');
110
+ if (selector.includes('$document')) return [];
111
+ }
112
+ try {
113
+ const results = queryElements({ element, selector });
114
+ return results ? Array.from(results) : [];
115
+ } catch (error) {
116
+ console.warn(`[AST Engine] Invalid $queryAll selector => "${selector}"`, error);
117
+ return [];
118
+ }
119
+ }
120
+ };
121
+
122
+ const getSandboxScope = (context) => {
123
+ const isBrowser = typeof window !== 'undefined';
124
+ return {
125
+ ...IsomorphicGlobals,
126
+ ...(isBrowser ? BrowserGlobals : {}),
127
+ ...context
128
+ };
129
+ };
130
+
131
+ /**
132
+ * Main recursive traversal loop of the AST Compiler.
133
+ * Analyzes object structures from the inside-out to resolve nested expression boundaries.
134
+ */
135
+ export async function processOperators(context, execute) {
136
+ if (execute instanceof Promise) {
137
+ execute = await execute;
138
+ }
139
+
140
+ if (execute === null || execute === undefined) {
141
+ return execute;
142
+ }
143
+
144
+ if (Array.isArray(execute)) {
145
+ const evaluatedArray = [];
146
+ for (let i = 0; i < execute.length; i++) {
147
+ evaluatedArray[i] = await processOperators(context, execute[i]);
148
+ }
149
+ return evaluatedArray;
150
+ } else if (typeof execute === "object") {
151
+ const keys = Object.keys(execute);
152
+
153
+ // Match operators starting with "$" except raw database storage parameters
154
+ const operatorKey = keys.find(k => k.startsWith("$") && !["$array", "$filter", "$storage", "$database"].includes(k));
155
+
156
+ if (operatorKey && keys.length === 1) {
157
+ return await executeOperator(context, operatorKey, execute[operatorKey]);
158
+ }
159
+
160
+ const evaluatedObject = {};
161
+ for (const key of keys) {
162
+ let finalKey = key;
163
+
164
+ if (key.startsWith("$") && !["$array", "$filter", "$storage", "$database"].includes(key)) {
165
+ const resolvedKey = await executeOperator(context, key);
166
+ if (typeof resolvedKey === "string" || typeof resolvedKey === "number") {
167
+ finalKey = String(resolvedKey);
168
+ }
169
+ }
170
+
171
+ let finalValue = execute[key];
172
+ if (typeof finalValue === "string" && finalValue.startsWith("$") && !["$array", "$filter", "$storage", "$database"].includes(finalValue)) {
173
+ finalValue = await executeOperator(context, finalValue);
174
+ } else if (Array.isArray(finalValue) || (typeof finalValue === "object" && finalValue !== null)) {
175
+ finalValue = await processOperators(context, finalValue);
176
+ }
177
+
178
+ evaluatedObject[finalKey] = finalValue;
179
+ }
180
+
181
+ return evaluatedObject;
182
+ } else if (typeof execute === "string" && execute.startsWith("$") && !["$array", "$filter", "$storage", "$database"].includes(execute)) {
183
+ return await executeOperator(context, execute);
184
+ }
185
+
186
+ return execute;
187
+ }
188
+
189
+ async function executeOperator(context, operator, args) {
190
+ const localContext = {
191
+ state: {},
192
+ ...context
193
+ };
194
+
195
+ // Evaluate core context property mappings
196
+ if (args === undefined) {
197
+ if (operator.startsWith("$state.")) {
198
+ return getValueFromObject(localContext.state, operator.substring(7));
199
+ } else if (operator === "$state") {
200
+ return localContext.state;
201
+ } else if (operator.startsWith("$data.")) {
202
+ return getValueFromObject(localContext.data, operator.substring(6));
203
+ } else if (operator === "$data") {
204
+ return localContext.data;
205
+ } else if (operator.startsWith("$event.")) {
206
+ return getValueFromObject(localContext.event, operator.substring(7));
207
+ } else if (operator === "$event") {
208
+ return localContext.event;
209
+ } else if (operator.startsWith("$request.")) {
210
+ return getValueFromObject(localContext.request, operator.substring(9));
211
+ } else if (operator === "$request") {
212
+ return localContext.request;
213
+ } else if (operator.startsWith("$header.")) {
214
+ const headerKey = operator.substring(8).toLowerCase();
215
+ return localContext.request?.headers?.[headerKey];
216
+ } else if (operator === "$rawBody") {
217
+ return localContext.rawBody;
218
+ } else if (operator === "$this" || operator === "$element") {
219
+ return localContext.element;
220
+ } else if (operator === "$plugin" || operator === "$instance") {
221
+ return localContext.plugin;
222
+ } else if (operator === "$value") {
223
+ const el = localContext.element;
224
+ return el && typeof el.getValue === 'function' ? el.getValue() : "";
225
+ }
226
+ }
227
+
228
+ let isExplicitNew = false;
229
+ let cleanPath = operator.startsWith("$") ? operator.substring(1) : operator;
230
+
231
+ if (cleanPath.startsWith("new.")) {
232
+ isExplicitNew = true;
233
+ cleanPath = cleanPath.substring(4);
234
+ }
235
+
236
+ const parts = cleanPath.split('.');
237
+ const SandboxScope = getSandboxScope(localContext);
238
+
239
+ let target = SandboxScope[parts[0]];
240
+ let parentContext = SandboxScope;
241
+
242
+ if (target === undefined && localContext.element) {
243
+ target = localContext.element[parts[0]];
244
+ parentContext = localContext.element;
245
+ }
246
+
247
+ if (target !== undefined) {
248
+ for (let i = 1; i < parts.length; i++) {
249
+ parentContext = target;
250
+ target = target[parts[i]];
251
+ if (target === undefined) break;
252
+ }
253
+
254
+ if (typeof target === 'function') {
255
+ let argsArray = [];
256
+ if (args !== undefined) {
257
+ if (typeof args === 'object' && args !== null && (args.$args !== undefined || args.$arguments !== undefined)) {
258
+ const explicitArgs = args.$args !== undefined ? args.$args : args.$arguments;
259
+
260
+ if (Array.isArray(explicitArgs)) {
261
+ argsArray = [];
262
+ for (let i = 0; i < explicitArgs.length; i++) {
263
+ argsArray.push(await processOperators(localContext, explicitArgs[i]));
264
+ }
265
+ } else {
266
+ const evaluatedSingle = await processOperators(localContext, explicitArgs);
267
+ argsArray = [evaluatedSingle];
268
+ }
269
+ } else {
270
+ const evaluatedSingle = await processOperators(localContext, args);
271
+ argsArray = [evaluatedSingle];
272
+ }
273
+ }
274
+
275
+ const isEs6Class = /^\s*class\s+/.test(target.toString());
276
+
277
+ if (isExplicitNew || isEs6Class) {
278
+ return new target(...argsArray);
279
+ }
280
+
281
+ return await target.apply(parentContext, argsArray);
282
+ } else if (target !== undefined) {
283
+ return target;
284
+ }
285
+ }
286
+
287
+ return operator;
288
+ }
289
+
290
+ export { processOperators as processOperatorsAsync };
291
+ export const customOperators = {};
292
+
293
+ export default processOperators;