@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 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.7**. Zero dependencies. ~6.4 KB brotli / ~7.1 KB gzip.
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
  [![Tests](https://github.com/symbiotejs/symbiote.js/actions/workflows/tests.yml/badge.svg)](https://github.com/symbiotejs/symbiote.js/actions/workflows/tests.yml)
2
2
  [![npm version](https://img.shields.io/npm/v/@symbiotejs/symbiote)](https://www.npmjs.com/package/@symbiotejs/symbiote)
3
3
  [![npm downloads](https://img.shields.io/npm/dm/@symbiotejs/symbiote)](https://www.npmjs.com/package/@symbiotejs/symbiote)
4
- ![bundle size](https://img.shields.io/badge/brotli-5.9_kb-blue)
4
+ ![bundle size](https://img.shields.io/badge/brotli-7.3_kb-blue)
5
5
  ![types](https://img.shields.io/badge/types-JSDoc+d.ts-blue)
6
6
  ![license](https://img.shields.io/badge/license-MIT-green)
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. **~6kb** brotli / **~7kb** gzip.
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) | 18.9 kb | 6.6 kb | **5.9 kb** |
343
- | **Symbiote.js** (full, with AppRouter) | 23.2 kb | 7.9 kb | **7.2 kb** |
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 **5.9 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 **~8× larger** before adding a router, state manager, or SSR framework.
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 #parseProp(prop, fnCtx) {
233
- /** @type {PubSub} */
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.#parseProp(/** @type {string} */ (prop), this);
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.#parseProp(prop, this);
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.#parseProp(prop, this);
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.#parseProp(prop, this);
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
- let parsed = Symbiote.#parseProp(prop, this);
366
- if (!parsed) return true;
367
- parsed.ctx.pub(parsed.name, val);
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.#parseProp(prop, this);
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;
@@ -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
@@ -1,2 +1,3 @@
1
1
  export { default, Symbiote, html, css, PubSub, DICT, animateOut } from './index.js';
2
2
  export { AppRouter } from './AppRouter.js';
3
+ export { ToolDescriptor } from './webmcp.js';
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
- parts.forEach((part, idx) => {
24
- resultHtml += part;
25
- if (idx >= props.length) return;
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
- return;
59
+ continue;
30
60
  }
31
- if (val?.constructor === Object) {
61
+ if (val.constructor === Object) {
32
62
  let bindStr = '';
33
63
  // @ts-expect-error
34
64
  for (let key in val) {
35
- if (RESERVED_ATTRIBUTES.includes(key)) {
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;
@@ -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;