@symbiotejs/symbiote 3.6.0 → 3.8.0-webmcp.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI_REFERENCE.md +56 -1
- package/CHANGELOG.md +14 -0
- package/README.md +15 -5
- package/core/PubSub.js +16 -0
- package/core/Symbiote.js +41 -48
- package/core/dictionary.js +22 -0
- package/core/full.js +1 -0
- package/core/html.js +38 -8
- package/core/itemizeProcessor.js +13 -0
- package/core/parseProp.js +87 -0
- package/core/tpl-processors.js +111 -13
- package/core/webmcp.js +807 -0
- package/node/SSR.js +26 -1
- package/package.json +31 -6
- package/scripts/update-exports.js +4 -0
- package/types/core/PubSub.d.ts +1 -0
- package/types/core/PubSub.d.ts.map +1 -1
- package/types/core/Symbiote.d.ts +7 -0
- package/types/core/Symbiote.d.ts.map +1 -1
- package/types/core/dictionary.d.ts +11 -0
- package/types/core/full.d.ts +1 -0
- package/types/core/html.d.ts.map +1 -1
- package/types/core/itemizeProcessor.d.ts.map +1 -1
- package/types/core/parseProp.d.ts +11 -0
- package/types/core/parseProp.d.ts.map +1 -0
- package/types/core/tpl-processors.d.ts +7 -0
- package/types/core/tpl-processors.d.ts.map +1 -1
- package/types/core/webmcp.d.ts +65 -0
- package/types/core/webmcp.d.ts.map +1 -0
- package/types/node/SSR.d.ts.map +1 -1
- package/types/utils/reassignDictionary.d.ts +11 -0
- package/types/utils/reassignDictionary.d.ts.map +1 -1
- package/scripts/postinstall.js +0 -9
package/AI_REFERENCE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Symbiote.js — AI Context Reference (v3.x)
|
|
2
2
|
|
|
3
3
|
> **Purpose**: Authoritative reference for AI code assistants. All information is derived from source code analysis of [symbiote.js](https://github.com/symbiotejs/symbiote.js).
|
|
4
|
-
> Current version: **3.
|
|
4
|
+
> Current version: **3.8.0**. Zero dependencies. ~7.3 KB brotli / ~8.1 KB gzip.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -18,6 +18,7 @@ import Symbiote, { html, css } from 'https://esm.run/@symbiotejs/symbiote';
|
|
|
18
18
|
import Symbiote from '@symbiotejs/symbiote/core/Symbiote.js';
|
|
19
19
|
import { PubSub } from '@symbiotejs/symbiote/core/PubSub.js';
|
|
20
20
|
import { AppRouter } from '@symbiotejs/symbiote/core/AppRouter.js';
|
|
21
|
+
import { ToolDescriptor } from '@symbiotejs/symbiote/webmcp';
|
|
21
22
|
import { html } from '@symbiotejs/symbiote/core/html.js';
|
|
22
23
|
import { css } from '@symbiotejs/symbiote/core/css.js';
|
|
23
24
|
```
|
|
@@ -25,6 +26,9 @@ import { css } from '@symbiotejs/symbiote/core/css.js';
|
|
|
25
26
|
### Core exports (index.js)
|
|
26
27
|
`Symbiote` (default), `html`, `css`, `PubSub`, `DICT`, `animateOut`
|
|
27
28
|
|
|
29
|
+
### WebMCP experimental exports (`@symbiotejs/symbiote/webmcp`)
|
|
30
|
+
`ToolDescriptor`, `installWebMCP`, `webMCPRegistry`, `syncWebMCPTools`, `unregisterWebMCPTools`, `getActiveWebMCPTools`
|
|
31
|
+
|
|
28
32
|
### Utils exports (`@symbiotejs/symbiote/utils`)
|
|
29
33
|
`UID`, `setNestedProp`, `applyStyles`, `applyAttributes`, `create`, `kebabToCamel`, `reassignDictionary`
|
|
30
34
|
|
|
@@ -267,6 +271,57 @@ ctx.multiPub({ score: 100, userName: 'Hero' });
|
|
|
267
271
|
|
|
268
272
|
---
|
|
269
273
|
|
|
274
|
+
## WebMCP Tools (Experimental)
|
|
275
|
+
|
|
276
|
+
Install the experimental npm release with:
|
|
277
|
+
```shell
|
|
278
|
+
npm i @symbiotejs/symbiote@webmcp
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
WebMCP is optional and must be imported before participating components render:
|
|
282
|
+
```js
|
|
283
|
+
import Symbiote, { html } from '@symbiotejs/symbiote';
|
|
284
|
+
import { ToolDescriptor } from '@symbiotejs/symbiote/webmcp';
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Automatic mode creates tools from bound event handlers:
|
|
288
|
+
```js
|
|
289
|
+
Symbiote.mcpToolMode = true;
|
|
290
|
+
|
|
291
|
+
class MyCounter extends Symbiote {
|
|
292
|
+
count = 0;
|
|
293
|
+
incrementCount() { this.$.count++; }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
MyCounter.template = html`
|
|
297
|
+
<button ${{onclick: 'incrementCount'}}>Increment</button>
|
|
298
|
+
`;
|
|
299
|
+
MyCounter.reg('my-counter');
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Generated names preserve the handler key and custom element tag, e.g. `incrementCount_in_my-counter`. Itemized component tools include `_KEY_` when available, and popup `^handler` bindings can create ancestor-owned tools with source item context.
|
|
303
|
+
|
|
304
|
+
Use `ToolDescriptor` for descriptions, schemas, execution, and dynamic visibility:
|
|
305
|
+
```js
|
|
306
|
+
init$ = {
|
|
307
|
+
canRun: false,
|
|
308
|
+
run_tool: new ToolDescriptor({
|
|
309
|
+
description: 'Run the visible action.',
|
|
310
|
+
deps: ['canRun'],
|
|
311
|
+
when: () => this.$.canRun,
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: { amount: { type: 'number' } },
|
|
315
|
+
},
|
|
316
|
+
execute: ({ amount = 1 }) => this.run(amount),
|
|
317
|
+
}),
|
|
318
|
+
};
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
`when()` is not auto-tracked; declare `deps` explicitly. Components may provide `componentDescription` as a string or async function to append more context to component-owned tool descriptions. Native testing requires a browser with WebMCP support, such as Chrome Canary 150.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
270
325
|
## Shared Context (`*` prefix)
|
|
271
326
|
|
|
272
327
|
Components grouped by the `ctx` HTML attribute (or `--ctx` CSS custom property) share a data context. Properties with `*` prefix are read/written in this shared context — inspired by native HTML `name` attribute grouping (like radio button groups):
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.8.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Experimental WebMCP support.** New optional `@symbiotejs/symbiote/webmcp` entry point exposes live Symbiote UI actions as native browser WebMCP tools. Import it before rendering participating components, then use `mcpToolMode` for automatic event-handler tools or `ToolDescriptor` for custom descriptions, input schemas, execution, and `when()` visibility.
|
|
8
|
+
|
|
9
|
+
- **Component context for agents.** Components can define `componentDescription` as a string or async function. The resolved text is appended to component-owned tool descriptions.
|
|
10
|
+
|
|
11
|
+
- **WebMCP lifecycle registration.** Component tools register on render and unregister on DOM removal. Itemized components inherit global `mcpToolMode`, keyed item tools include `_KEY_` context, and popup `^` event bindings can expose ancestor-owned tools with source item context.
|
|
12
|
+
|
|
13
|
+
### Notes
|
|
14
|
+
|
|
15
|
+
- This is an experimental release intended for browser builds with native WebMCP support, such as Chrome Canary 150. Publish with the `webmcp` npm tag while the API stabilizes.
|
|
16
|
+
|
|
3
17
|
## 3.5.4
|
|
4
18
|
|
|
5
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[](https://github.com/symbiotejs/symbiote.js/actions/workflows/tests.yml)
|
|
2
2
|
[](https://www.npmjs.com/package/@symbiotejs/symbiote)
|
|
3
3
|
[](https://www.npmjs.com/package/@symbiotejs/symbiote)
|
|
4
|
-

|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
<img src="https://rnd-pro.com/svg/symbiote/index.svg" width="200" alt="Symbiote.js">
|
|
11
11
|
|
|
12
|
-
A lightweight, standards-first UI library built on Web Components. No virtual DOM, no compiler, no build step required - works directly in the browser. A bundler is recommended for production performance, but entirely optional. **~
|
|
12
|
+
A lightweight, standards-first UI library built on Web Components. No virtual DOM, no compiler, no build step required - works directly in the browser. A bundler is recommended for production performance, but entirely optional. **~7.3kb** brotli / **~8.1kb** gzip.
|
|
13
13
|
|
|
14
14
|
Symbiote.js gives you the convenience of a modern framework while staying close to the native platform - HTML, CSS, and DOM APIs. Components are real custom elements that work everywhere: in any framework, in plain HTML, or in a micro-frontend architecture. And with **isomorphic mode**, the same component code works on the server and the client - server-rendered pages hydrate automatically, no diffing, no mismatch errors.
|
|
15
15
|
|
|
16
16
|
## What's new in v3
|
|
17
17
|
|
|
18
|
+
- **Experimental WebMCP support** - expose live Symbiote UI actions as browser-native tools for agents. See the [WebMCP docs](./docs/webmcp.md).
|
|
18
19
|
- **Server-Side Rendering** - render components to HTML with `SSR.processHtml()` or stream chunks with `SSR.renderToStream()`. Client-side hydration via `ssrMode` attaches bindings to existing DOM without re-rendering.
|
|
19
20
|
- **Isomorphic components** - `isoMode` flag makes components work in both SSR and client-only scenarios automatically. If server-rendered content exists, it hydrates; otherwise it renders the template from scratch. One component, zero conditional logic.
|
|
20
21
|
- **Computed properties** - reactive derived state with microtask batching.
|
|
@@ -330,6 +331,7 @@ CSS values are parsed automatically - quoted strings become strings, numbers bec
|
|
|
330
331
|
## Best for
|
|
331
332
|
|
|
332
333
|
- **Complex widgets** embedded in any host application
|
|
334
|
+
- **Low-code HTML-based solutions** - simple declarative everything
|
|
333
335
|
- **Micro frontends** - standard custom elements, no framework coupling
|
|
334
336
|
- **Reusable component libraries** - works in React, Vue, Angular, or plain HTML
|
|
335
337
|
- **SSR-powered apps** - lightweight server rendering without framework lock-in
|
|
@@ -339,12 +341,13 @@ CSS values are parsed automatically - quoted strings become strings, numbers bec
|
|
|
339
341
|
|
|
340
342
|
| Library | Minified | Gzip | Brotli |
|
|
341
343
|
|---------|----------|------|--------|
|
|
342
|
-
| **Symbiote.js** (core) |
|
|
343
|
-
| **Symbiote.js** (full, with AppRouter) |
|
|
344
|
+
| **Symbiote.js** (core) | 23.6 kb | 8.1 kb | **7.3 kb** |
|
|
345
|
+
| **Symbiote.js** (full, with AppRouter + WebMCP export) | 35.6 kb | 12.1 kb | **11.0 kb** |
|
|
346
|
+
| **Symbiote.js** (WebMCP extension) | 31.4 kb | 10.6 kb | **9.6 kb** |
|
|
344
347
|
| **Lit** 3.3 | 15.5 kb | 6.0 kb | **~5.1 kb** |
|
|
345
348
|
| **React 19 + ReactDOM** | ~186 kb | ~59 kb | **~50 kb** |
|
|
346
349
|
|
|
347
|
-
Symbiote and Lit have similar base sizes, but Symbiote's **
|
|
350
|
+
Symbiote and Lit have similar base sizes, but Symbiote's **7.3 kb** core includes more built-in features: global state management, lists (itemize API), exit animations, computed properties etc. Lit needs additional packages for comparable features. React is **~7× larger** before adding a router, state manager, or SSR framework.
|
|
348
351
|
|
|
349
352
|
## Browser support
|
|
350
353
|
|
|
@@ -359,6 +362,13 @@ All modern browsers: Chrome, Firefox, Safari, Edge, Opera.
|
|
|
359
362
|
- [AI Reference](https://github.com/symbiotejs/symbiote.js/blob/main/AI_REFERENCE.md)
|
|
360
363
|
- [Changelog](https://github.com/symbiotejs/symbiote.js/blob/main/CHANGELOG.md)
|
|
361
364
|
|
|
365
|
+
## Related articles
|
|
366
|
+
|
|
367
|
+
- [Symbiote.js: superpowers for Web Components](https://dev.to/foxeyes/symbiotejs-superpowers-for-web-components-1gid)
|
|
368
|
+
- [Symbiote.js: v3 highlights](https://dev.to/foxeyes/symbiotejs-v3-web-components-with-ssr-in-6kb-10n6)
|
|
369
|
+
- [Symbiote.js vs Lit](https://dev.to/foxeyes/lit-vs-symbiotejs-22gj)
|
|
370
|
+
- [JSDA Stack - A Revolutionary Simple Approach to Build Modern Web](https://dev.to/foxeyes/jsda-kit-a-revolutionary-simple-approach-to-build-modern-web-1dip)
|
|
371
|
+
|
|
362
372
|
**Questions or proposals? Welcome to [Symbiote Discussions](https://github.com/symbiotejs/symbiote.js/discussions)!** ❤️
|
|
363
373
|
|
|
364
374
|
---
|
package/core/PubSub.js
CHANGED
|
@@ -253,6 +253,7 @@ export class PubSub {
|
|
|
253
253
|
}
|
|
254
254
|
this.store[prop] = val;
|
|
255
255
|
this.notify(prop, val);
|
|
256
|
+
globalThis[DICT.MCP_PUBSUB_CHANGED_KEY]?.(this, prop);
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
/**
|
|
@@ -274,6 +275,7 @@ export class PubSub {
|
|
|
274
275
|
}
|
|
275
276
|
this.store[prop] = val;
|
|
276
277
|
this.notify(prop, val);
|
|
278
|
+
globalThis[DICT.MCP_PUBSUB_CHANGED_KEY]?.(this, prop);
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
/** @returns {T} */
|
|
@@ -300,6 +302,17 @@ export class PubSub {
|
|
|
300
302
|
}
|
|
301
303
|
}
|
|
302
304
|
|
|
305
|
+
/** @param {keyof T} prop */
|
|
306
|
+
delete(prop) {
|
|
307
|
+
if (!this.#storeIsProxy && !(prop in this.store)) {
|
|
308
|
+
PubSub.#warn('delete', prop, this);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
delete this.store[prop];
|
|
312
|
+
this.notify(prop, null);
|
|
313
|
+
globalThis[DICT.MCP_PUBSUB_CHANGED_KEY]?.(this, prop);
|
|
314
|
+
}
|
|
315
|
+
|
|
303
316
|
/** @param {keyof T} prop */
|
|
304
317
|
notify(prop, val) {
|
|
305
318
|
// @ts-expect-error
|
|
@@ -386,11 +399,14 @@ export class PubSub {
|
|
|
386
399
|
}
|
|
387
400
|
}
|
|
388
401
|
}
|
|
402
|
+
globalThis[DICT.MCP_PUBSUB_REGISTERED_KEY]?.(data, uid);
|
|
389
403
|
return data;
|
|
390
404
|
}
|
|
391
405
|
|
|
392
406
|
/** @param {String | Symbol} uid */
|
|
393
407
|
static deleteCtx(uid) {
|
|
408
|
+
let ctx = PubSub.globalStore.get(uid);
|
|
409
|
+
ctx && globalThis[DICT.MCP_PUBSUB_DELETED_KEY]?.(ctx, uid);
|
|
394
410
|
PubSub.globalStore.delete(uid);
|
|
395
411
|
}
|
|
396
412
|
|
package/core/Symbiote.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DICT } from './dictionary.js';
|
|
|
4
4
|
import { animateOut } from './animateOut.js';
|
|
5
5
|
import { setNestedProp } from '../utils/setNestedProp.js';
|
|
6
6
|
import { prepareStyleSheet } from '../utils/prepareStyleSheet.js';
|
|
7
|
+
import { parseProp } from './parseProp.js';
|
|
7
8
|
|
|
8
9
|
import PROCESSORS from './tpl-processors.js';
|
|
9
10
|
import { parseCssPropertyValue } from '../utils/parseCssPropertyValue.js';
|
|
@@ -45,6 +46,15 @@ export class Symbiote extends HTMLElement {
|
|
|
45
46
|
/** @type {HTMLTemplateElement} */
|
|
46
47
|
static __tpl;
|
|
47
48
|
|
|
49
|
+
/** @type {Boolean} */
|
|
50
|
+
static mcpToolMode = false;
|
|
51
|
+
|
|
52
|
+
/** @type {string | (() => string | Promise<string>)} */
|
|
53
|
+
static componentDescription;
|
|
54
|
+
|
|
55
|
+
/** @type {string | (() => string | Promise<string>)} */
|
|
56
|
+
componentDescription;
|
|
57
|
+
|
|
48
58
|
static set devMode(val) {
|
|
49
59
|
devState.devMode = val;
|
|
50
60
|
}
|
|
@@ -76,6 +86,7 @@ export class Symbiote extends HTMLElement {
|
|
|
76
86
|
* @param {Boolean} [shadow]
|
|
77
87
|
*/
|
|
78
88
|
render(template, shadow = this.renderShadow) {
|
|
89
|
+
this[DICT.MCP_EVENTS_KEY] = [];
|
|
79
90
|
/** @type {DocumentFragment} */
|
|
80
91
|
let fr;
|
|
81
92
|
if ((shadow || this.#super.shadowStyleSheets) && !this.shadowRoot) {
|
|
@@ -150,6 +161,7 @@ export class Symbiote extends HTMLElement {
|
|
|
150
161
|
} catch (e) {
|
|
151
162
|
if (!globalThis.__SYMBIOTE_SSR) throw e;
|
|
152
163
|
}
|
|
164
|
+
this.syncWebMCPTools?.();
|
|
153
165
|
};
|
|
154
166
|
|
|
155
167
|
if (this.#super.shadowStyleSheets) {
|
|
@@ -192,6 +204,21 @@ export class Symbiote extends HTMLElement {
|
|
|
192
204
|
this.allowTemplateInits = true;
|
|
193
205
|
/** @type {Boolean} */
|
|
194
206
|
this.lazyMode = false;
|
|
207
|
+
/** @type {Boolean} */
|
|
208
|
+
this.mcpToolMode = false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
syncWebMCPTools() {
|
|
212
|
+
if (globalThis.__SYMBIOTE_SSR) return;
|
|
213
|
+
let sync = globalThis[DICT.MCP_SYNC_OWNER_KEY];
|
|
214
|
+
if (sync) {
|
|
215
|
+
sync(this);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
unregisterWebMCPTools() {
|
|
220
|
+
if (globalThis.__SYMBIOTE_SSR) return;
|
|
221
|
+
globalThis[DICT.MCP_UNREGISTER_OWNER_KEY]?.(this);
|
|
195
222
|
}
|
|
196
223
|
|
|
197
224
|
/** @returns {String} */
|
|
@@ -228,47 +255,10 @@ export class Symbiote extends HTMLElement {
|
|
|
228
255
|
* @template {Symbiote} T
|
|
229
256
|
* @param {String} prop
|
|
230
257
|
* @param {T} fnCtx
|
|
258
|
+
* @returns {import('./parseProp.js').ParsedProp | null}
|
|
231
259
|
*/
|
|
232
|
-
static
|
|
233
|
-
|
|
234
|
-
let ctx;
|
|
235
|
-
/** @type {String} */
|
|
236
|
-
let name;
|
|
237
|
-
let first = prop.charCodeAt(0);
|
|
238
|
-
// Fast path for common local props (no prefix, no /)
|
|
239
|
-
// Char codes: * = 42, ^ = 94, @ = 64, + = 43, - = 45
|
|
240
|
-
if (first !== 42 && first !== 94 && first !== 64 && first !== 43 && first !== 45 && !prop.includes('/')) {
|
|
241
|
-
return { ctx: fnCtx.localCtx, name: prop };
|
|
242
|
-
}
|
|
243
|
-
if (first === 42) {
|
|
244
|
-
ctx = fnCtx.sharedCtx;
|
|
245
|
-
name = prop.slice(1);
|
|
246
|
-
} else if (first === 94) {
|
|
247
|
-
name = prop.slice(1);
|
|
248
|
-
let found = fnCtx;
|
|
249
|
-
while (found && !found?.has?.(name)) {
|
|
250
|
-
// @ts-expect-error
|
|
251
|
-
found = found.parentElement || found.parentNode || found.host;
|
|
252
|
-
}
|
|
253
|
-
ctx = found?.localCtx || fnCtx.localCtx;
|
|
254
|
-
} else if (prop.includes('/')) {
|
|
255
|
-
let slashIdx = prop.indexOf('/');
|
|
256
|
-
ctx = PubSub.getCtx(prop.slice(0, slashIdx), false);
|
|
257
|
-
if (!ctx) {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
name = prop.slice(slashIdx + 1);
|
|
261
|
-
} else if (first === 45 && prop.charCodeAt(1) === 45) {
|
|
262
|
-
ctx = fnCtx.localCtx;
|
|
263
|
-
name = prop;
|
|
264
|
-
if (!ctx.has(name)) {
|
|
265
|
-
fnCtx.bindCssData(name);
|
|
266
|
-
}
|
|
267
|
-
} else {
|
|
268
|
-
ctx = fnCtx.localCtx;
|
|
269
|
-
name = prop;
|
|
270
|
-
}
|
|
271
|
-
return { ctx, name };
|
|
260
|
+
static parseProp(prop, fnCtx) {
|
|
261
|
+
return parseProp(prop, fnCtx);
|
|
272
262
|
}
|
|
273
263
|
|
|
274
264
|
/**
|
|
@@ -284,7 +274,7 @@ export class Symbiote extends HTMLElement {
|
|
|
284
274
|
}
|
|
285
275
|
handler(val);
|
|
286
276
|
};
|
|
287
|
-
let parsed = Symbiote
|
|
277
|
+
let parsed = Symbiote.parseProp(/** @type {string} */ (prop), this);
|
|
288
278
|
if (!parsed) {
|
|
289
279
|
// Named context not found — defer subscription
|
|
290
280
|
let slashIdx = /** @type {string} */ (prop).indexOf('/');
|
|
@@ -317,14 +307,14 @@ export class Symbiote extends HTMLElement {
|
|
|
317
307
|
|
|
318
308
|
/** @param {String} prop */
|
|
319
309
|
notify(prop) {
|
|
320
|
-
let parsed = Symbiote
|
|
310
|
+
let parsed = Symbiote.parseProp(prop, this);
|
|
321
311
|
if (!parsed) return;
|
|
322
312
|
parsed.ctx.notify(parsed.name);
|
|
323
313
|
}
|
|
324
314
|
|
|
325
315
|
/** @param {String} prop */
|
|
326
316
|
has(prop) {
|
|
327
|
-
let parsed = Symbiote
|
|
317
|
+
let parsed = Symbiote.parseProp(prop, this);
|
|
328
318
|
if (!parsed) return false;
|
|
329
319
|
return parsed.ctx.has(parsed.name);
|
|
330
320
|
}
|
|
@@ -336,7 +326,7 @@ export class Symbiote extends HTMLElement {
|
|
|
336
326
|
* @param {Boolean} [rewrite]
|
|
337
327
|
*/
|
|
338
328
|
add(prop, val, rewrite = false) {
|
|
339
|
-
let parsed = Symbiote
|
|
329
|
+
let parsed = Symbiote.parseProp(prop, this);
|
|
340
330
|
if (!parsed) return;
|
|
341
331
|
parsed.ctx.add(parsed.name, val, rewrite);
|
|
342
332
|
}
|
|
@@ -362,9 +352,9 @@ export class Symbiote extends HTMLElement {
|
|
|
362
352
|
if (first !== 42 && first !== 94 && first !== 64 && first !== 43 && first !== 45 && !prop.includes('/')) {
|
|
363
353
|
this.localCtx.pub(prop, val);
|
|
364
354
|
} else {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
355
|
+
let parsed = Symbiote.parseProp(prop, this);
|
|
356
|
+
if (!parsed) return true;
|
|
357
|
+
parsed.ctx.pub(parsed.name, val);
|
|
368
358
|
}
|
|
369
359
|
return true;
|
|
370
360
|
},
|
|
@@ -373,7 +363,7 @@ export class Symbiote extends HTMLElement {
|
|
|
373
363
|
if (first !== 42 && first !== 94 && first !== 64 && first !== 43 && first !== 45 && !prop.includes('/')) {
|
|
374
364
|
return this.localCtx.read(prop);
|
|
375
365
|
}
|
|
376
|
-
let parsed = Symbiote
|
|
366
|
+
let parsed = Symbiote.parseProp(prop, this);
|
|
377
367
|
if (!parsed) return undefined;
|
|
378
368
|
return parsed.ctx.read(parsed.name);
|
|
379
369
|
},
|
|
@@ -516,6 +506,8 @@ export class Symbiote extends HTMLElement {
|
|
|
516
506
|
}
|
|
517
507
|
this.render();
|
|
518
508
|
}
|
|
509
|
+
} else {
|
|
510
|
+
this.syncWebMCPTools?.();
|
|
519
511
|
}
|
|
520
512
|
this.connectedOnce = true;
|
|
521
513
|
}
|
|
@@ -589,6 +581,7 @@ export class Symbiote extends HTMLElement {
|
|
|
589
581
|
if (!this.connectedOnce) {
|
|
590
582
|
return;
|
|
591
583
|
}
|
|
584
|
+
this.unregisterWebMCPTools();
|
|
592
585
|
this.dropCssDataCache();
|
|
593
586
|
if (!this.readyToDestroy) {
|
|
594
587
|
return;
|
package/core/dictionary.js
CHANGED
|
@@ -37,4 +37,26 @@ export const DICT = {
|
|
|
37
37
|
TEXT_NODE_OPEN_TOKEN: '{{',
|
|
38
38
|
// Text node binding token:
|
|
39
39
|
TEXT_NODE_CLOSE_TOKEN: '}}',
|
|
40
|
+
// WebMCP component event handler metadata key:
|
|
41
|
+
MCP_EVENTS_KEY: '__symbioteMcpEventHandlers',
|
|
42
|
+
// WebMCP ToolDescriptor marker key:
|
|
43
|
+
MCP_TOOL_DESCRIPTOR_MARKER: '__symbioteToolDescriptor',
|
|
44
|
+
// WebMCP owner id metadata key:
|
|
45
|
+
MCP_OWNER_ID_KEY: '__symbioteMcpOwnerId',
|
|
46
|
+
// WebMCP global hook for owner sync:
|
|
47
|
+
MCP_SYNC_OWNER_KEY: '__SYMBIOTE_WEBMCP_SYNC_OWNER',
|
|
48
|
+
// WebMCP global hook for owner cleanup:
|
|
49
|
+
MCP_UNREGISTER_OWNER_KEY: '__SYMBIOTE_WEBMCP_UNREGISTER_OWNER',
|
|
50
|
+
// WebMCP global hook for PubSub context registration:
|
|
51
|
+
MCP_PUBSUB_REGISTERED_KEY: '__SYMBIOTE_WEBMCP_PUBSUB_REGISTERED',
|
|
52
|
+
// WebMCP global hook for PubSub context deletion:
|
|
53
|
+
MCP_PUBSUB_DELETED_KEY: '__SYMBIOTE_WEBMCP_PUBSUB_DELETED',
|
|
54
|
+
// WebMCP global hook for PubSub property changes:
|
|
55
|
+
MCP_PUBSUB_CHANGED_KEY: '__SYMBIOTE_WEBMCP_PUBSUB_CHANGED',
|
|
56
|
+
// WebMCP itemize item visible index metadata key:
|
|
57
|
+
MCP_ITEM_INDEX_KEY: '__symbioteMcpItemIndex',
|
|
58
|
+
// WebMCP itemize item stable key metadata key:
|
|
59
|
+
MCP_ITEM_KEY_KEY: '__symbioteMcpItemKey',
|
|
60
|
+
// WebMCP forwarded event target owners metadata key:
|
|
61
|
+
MCP_EVENT_TARGET_OWNERS_KEY: '__symbioteMcpEventTargetOwners',
|
|
40
62
|
};
|
package/core/full.js
CHANGED
package/core/html.js
CHANGED
|
@@ -10,6 +10,35 @@ export const RESERVED_ATTRIBUTES = [
|
|
|
10
10
|
DICT.CTX_NAME_ATTR,
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
+
const RESERVED_ATTRIBUTES_SET = new Set(RESERVED_ATTRIBUTES);
|
|
14
|
+
const SELF_CLOSING_CUSTOM_ELEMENT_RE = /<([a-z][.0-9_a-z]*-[\-.0-9_a-z]*)(\s+(?:"[^"]*"|'[^']*'|[^'"<>])*)?\/>/gi;
|
|
15
|
+
|
|
16
|
+
function hasImmediateClosingTag(htmlString, startIdx, tagName) {
|
|
17
|
+
let idx = startIdx;
|
|
18
|
+
let len = htmlString.length;
|
|
19
|
+
while (idx < len) {
|
|
20
|
+
let code = htmlString.charCodeAt(idx);
|
|
21
|
+
if (code !== 32 && code !== 9 && code !== 10 && code !== 12 && code !== 13) break;
|
|
22
|
+
idx++;
|
|
23
|
+
}
|
|
24
|
+
if (htmlString.charCodeAt(idx) !== 60 || htmlString.charCodeAt(idx + 1) !== 47) return false;
|
|
25
|
+
let tagStart = idx + 2;
|
|
26
|
+
if (htmlString.charCodeAt(tagStart + tagName.length) !== 62) return false;
|
|
27
|
+
let closingTagName = htmlString.slice(tagStart, tagStart + tagName.length);
|
|
28
|
+
return closingTagName === tagName || closingTagName.toLowerCase() === tagName.toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function closeSelfClosingCustomElements(resultHtml) {
|
|
32
|
+
if (resultHtml.indexOf('/>') === -1) return resultHtml;
|
|
33
|
+
return resultHtml.replace(SELF_CLOSING_CUSTOM_ELEMENT_RE, (match, tagName, attrs = '', offset, htmlString) => {
|
|
34
|
+
let openTag = `<${tagName}${attrs && attrs.trimEnd()}>`;
|
|
35
|
+
if (hasImmediateClosingTag(htmlString, offset + match.length, tagName)) {
|
|
36
|
+
return openTag;
|
|
37
|
+
}
|
|
38
|
+
return `${openTag}</${tagName}>`;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
13
42
|
/** @typedef {Record<keyof import('./Symbiote.js').Symbiote, String>} BindDescriptor */
|
|
14
43
|
|
|
15
44
|
/**
|
|
@@ -20,19 +49,20 @@ export const RESERVED_ATTRIBUTES = [
|
|
|
20
49
|
*/
|
|
21
50
|
export function html(parts, ...props) {
|
|
22
51
|
let resultHtml = '';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
let propsLength = props.length;
|
|
53
|
+
for (let idx = 0; idx < parts.length; idx++) {
|
|
54
|
+
resultHtml += parts[idx];
|
|
55
|
+
if (idx >= propsLength) continue;
|
|
26
56
|
let val = props[idx];
|
|
27
57
|
if (val === undefined || val === null) {
|
|
28
58
|
errMsg(15, val);
|
|
29
|
-
|
|
59
|
+
continue;
|
|
30
60
|
}
|
|
31
|
-
if (val
|
|
61
|
+
if (val.constructor === Object) {
|
|
32
62
|
let bindStr = '';
|
|
33
63
|
// @ts-expect-error
|
|
34
64
|
for (let key in val) {
|
|
35
|
-
if (
|
|
65
|
+
if (RESERVED_ATTRIBUTES_SET.has(key)) {
|
|
36
66
|
resultHtml += ` ${key}="${val[key]}"`;
|
|
37
67
|
} else {
|
|
38
68
|
bindStr += `${key}:${val[key]};`;
|
|
@@ -42,8 +72,8 @@ export function html(parts, ...props) {
|
|
|
42
72
|
} else {
|
|
43
73
|
resultHtml += val;
|
|
44
74
|
}
|
|
45
|
-
}
|
|
46
|
-
return resultHtml;
|
|
75
|
+
}
|
|
76
|
+
return closeSelfClosingCustomElements(resultHtml);
|
|
47
77
|
}
|
|
48
78
|
|
|
49
79
|
export default html;
|
package/core/itemizeProcessor.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { animateOut } from './animateOut.js';
|
|
2
2
|
import { warnMsg } from './warn.js';
|
|
3
3
|
import { setupItemize } from './itemizeSetup.js';
|
|
4
|
+
import { DICT } from './dictionary.js';
|
|
5
|
+
|
|
6
|
+
function setItemWebMCPMetadata(itemEl, item, idx) {
|
|
7
|
+
if (!itemEl) return;
|
|
8
|
+
itemEl[DICT.MCP_ITEM_INDEX_KEY] = idx;
|
|
9
|
+
if (item && Object.prototype.hasOwnProperty.call(item, '_KEY_')) {
|
|
10
|
+
itemEl[DICT.MCP_ITEM_KEY_KEY] = item._KEY_;
|
|
11
|
+
} else {
|
|
12
|
+
delete itemEl[DICT.MCP_ITEM_KEY_KEY];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
/**
|
|
6
17
|
* @template {import('./Symbiote.js').Symbiote} T
|
|
@@ -22,6 +33,7 @@ export function itemizeProcessor(fr, fnCtx) {
|
|
|
22
33
|
let fillItems = (/** @type {*[]} */ items) => {
|
|
23
34
|
items.forEach((item, idx) => {
|
|
24
35
|
if (currentList[idx]) {
|
|
36
|
+
setItemWebMCPMetadata(currentList[idx], item, idx);
|
|
25
37
|
if (currentList[idx].set$) {
|
|
26
38
|
currentList[idx].set$(item);
|
|
27
39
|
} else {
|
|
@@ -37,6 +49,7 @@ export function itemizeProcessor(fr, fnCtx) {
|
|
|
37
49
|
if (isLazy) {
|
|
38
50
|
repeatItem.lazyMode = true;
|
|
39
51
|
}
|
|
52
|
+
setItemWebMCPMetadata(repeatItem, item, idx);
|
|
40
53
|
Object.assign((repeatItem?.init$ || repeatItem), item);
|
|
41
54
|
fragment.appendChild(repeatItem);
|
|
42
55
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import PubSub from './PubSub.js';
|
|
2
|
+
import { DICT } from './dictionary.js';
|
|
3
|
+
|
|
4
|
+
const BINDING_PREFIXES = [
|
|
5
|
+
DICT.SHARED_CTX_PX,
|
|
6
|
+
DICT.PARENT_CTX_PX,
|
|
7
|
+
DICT.ATTR_BIND_PX,
|
|
8
|
+
DICT.COMPUTED_PX,
|
|
9
|
+
DICT.CSS_DATA_PX,
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
/** @param {string} propStr */
|
|
13
|
+
function hasBindingPrefix(propStr) {
|
|
14
|
+
for (let prefix of BINDING_PREFIXES) {
|
|
15
|
+
if (propStr.startsWith(prefix)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} ParsedProp
|
|
24
|
+
* @property {PubSub} ctx
|
|
25
|
+
* @property {string} name
|
|
26
|
+
* @property {'local' | 'parent' | 'shared' | 'named' | 'css-data'} scope
|
|
27
|
+
* @property {string} [ctxName]
|
|
28
|
+
* @property {any} [owner]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a Symbiote binding token to its PubSub context and property name.
|
|
33
|
+
*
|
|
34
|
+
* @template {import('./Symbiote.js').Symbiote} T
|
|
35
|
+
* @param {String} prop
|
|
36
|
+
* @param {T} fnCtx
|
|
37
|
+
* @returns {ParsedProp | null}
|
|
38
|
+
*/
|
|
39
|
+
export function parseProp(prop, fnCtx) {
|
|
40
|
+
/** @type {PubSub} */
|
|
41
|
+
let ctx;
|
|
42
|
+
/** @type {String} */
|
|
43
|
+
let name;
|
|
44
|
+
let propStr = String(prop);
|
|
45
|
+
// Fast path for common local props (no prefix, no /)
|
|
46
|
+
if (!hasBindingPrefix(propStr) && !propStr.includes(DICT.NAMED_CTX_SPLTR)) {
|
|
47
|
+
return { ctx: fnCtx.localCtx, name: propStr, scope: 'local', owner: fnCtx };
|
|
48
|
+
}
|
|
49
|
+
if (propStr.startsWith(DICT.SHARED_CTX_PX)) {
|
|
50
|
+
ctx = fnCtx.sharedCtx;
|
|
51
|
+
name = propStr.slice(DICT.SHARED_CTX_PX.length);
|
|
52
|
+
return { ctx, name, scope: 'shared', ctxName: fnCtx.ctxName };
|
|
53
|
+
}
|
|
54
|
+
if (propStr.startsWith(DICT.PARENT_CTX_PX)) {
|
|
55
|
+
name = propStr.slice(DICT.PARENT_CTX_PX.length);
|
|
56
|
+
let found = fnCtx;
|
|
57
|
+
while (found && !found?.has?.(name)) {
|
|
58
|
+
// @ts-expect-error
|
|
59
|
+
found = found.parentElement || found.parentNode || found.host;
|
|
60
|
+
}
|
|
61
|
+
ctx = found?.localCtx || fnCtx.localCtx;
|
|
62
|
+
return { ctx, name, scope: 'parent', owner: found || fnCtx };
|
|
63
|
+
}
|
|
64
|
+
if (propStr.includes(DICT.NAMED_CTX_SPLTR)) {
|
|
65
|
+
let slashIdx = propStr.indexOf(DICT.NAMED_CTX_SPLTR);
|
|
66
|
+
let ctxName = propStr.slice(0, slashIdx);
|
|
67
|
+
ctx = PubSub.getCtx(ctxName, false);
|
|
68
|
+
if (!ctx) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
name = propStr.slice(slashIdx + 1);
|
|
72
|
+
return { ctx, name, scope: 'named', ctxName };
|
|
73
|
+
}
|
|
74
|
+
if (propStr.startsWith(DICT.CSS_DATA_PX)) {
|
|
75
|
+
ctx = fnCtx.localCtx;
|
|
76
|
+
name = propStr;
|
|
77
|
+
if (!ctx.has(name)) {
|
|
78
|
+
fnCtx.bindCssData(name);
|
|
79
|
+
}
|
|
80
|
+
return { ctx, name, scope: 'css-data', owner: fnCtx };
|
|
81
|
+
}
|
|
82
|
+
ctx = fnCtx.localCtx;
|
|
83
|
+
name = propStr;
|
|
84
|
+
return { ctx, name, scope: 'local', owner: fnCtx };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default parseProp;
|