mount-observer 0.1.27 → 0.1.29

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/README.md CHANGED
@@ -1555,6 +1555,264 @@ The handler looks for the nearest scope container using `.closest()`:
1555
1555
 
1556
1556
  IDs are generated using a global counter (via `Symbol.for`) to ensure uniqueness across multiple module instances. Generated IDs follow the pattern `gid-0`, `gid-1`, `gid-2`, etc.
1557
1557
 
1558
+ ## Scoped Parser Registry for EMC Scripts
1559
+
1560
+ The scoped parser registry system enables lazy-loading of complex parsers for enhancement attributes while maintaining framework isolation. Each synthesizer element (be-hive, htmx-container, alpine-scope, etc.) maintains its own parser registry to prevent conflicts between different framework libraries.
1561
+
1562
+ **Why use scoped parser registries?**
1563
+
1564
+ - Lazy load complex parsers only when needed (e.g., nested-regex-groups)
1565
+ - Maintain framework isolation (HTMX, Alpine, be-hive can coexist)
1566
+ - Declarative parser loading via HTML script tags
1567
+ - Programmatic parser registration for dynamic scenarios
1568
+ - Automatic parser waiting before enhancement initialization
1569
+ - Built-in parsers remain globally available
1570
+
1571
+ ### Basic Usage - Declarative Parser Loading
1572
+
1573
+ Load parsers declaratively using `<script type="emc-parser">` elements:
1574
+
1575
+ ```html
1576
+ <be-hive>
1577
+ <!-- Load parser first -->
1578
+ <script type="emc-parser"
1579
+ src="nested-regex-groups/parser.js"
1580
+ parser-name="nestedRegexGroups"></script>
1581
+
1582
+ <!-- Then load enhancement that depends on it -->
1583
+ <script type="emc"
1584
+ src="be-switched/emc.json"
1585
+ wait-for-parsers="nestedRegexGroups"></script>
1586
+ </be-hive>
1587
+
1588
+ <script type="module">
1589
+ import { MountObserver } from 'mount-observer/MountObserver.js';
1590
+
1591
+ // Bootstrap the handlers
1592
+ new MountObserver({
1593
+ do: 'builtIns.emcScript'
1594
+ }).observe(document);
1595
+ </script>
1596
+ ```
1597
+
1598
+ **What happens:**
1599
+
1600
+ 1. The `emc-parser` script loads the parser module via dynamic import
1601
+ 2. Parser is registered in the be-hive element's scoped registry
1602
+ 3. `parser-registered` event is dispatched
1603
+ 4. The `emc` script waits for the parser to be registered
1604
+ 5. Once ready, the enhancement is processed with access to the parser
1605
+
1606
+ ### Multiple Parsers
1607
+
1608
+ Wait for multiple parsers using space-delimited names:
1609
+
1610
+ ```html
1611
+ <be-hive>
1612
+ <script type="emc-parser" src="parser1.js" parser-name="parser1"></script>
1613
+ <script type="emc-parser" src="parser2.js" parser-name="parser2"></script>
1614
+
1615
+ <!-- Wait for both parsers -->
1616
+ <script type="emc"
1617
+ src="my-enhancement/emc.json"
1618
+ wait-for-parsers="parser1 parser2"></script>
1619
+ </be-hive>
1620
+ ```
1621
+
1622
+ ### Custom Timeout
1623
+
1624
+ Configure parser loading timeout (default: 60 seconds):
1625
+
1626
+ ```html
1627
+ <be-hive>
1628
+ <script type="emc-parser" src="slow-parser.js" parser-name="slowParser"></script>
1629
+
1630
+ <!-- Wait up to 2 minutes -->
1631
+ <script type="emc"
1632
+ src="my-enhancement/emc.json"
1633
+ wait-for-parsers="slowParser"
1634
+ data-parser-timeout="120000"></script>
1635
+ </be-hive>
1636
+ ```
1637
+
1638
+ ### Programmatic Parser Registration
1639
+
1640
+ Register parsers programmatically via JavaScript:
1641
+
1642
+ ```html
1643
+ <be-hive id="myHive">
1644
+ <script type="emc"
1645
+ src="my-enhancement/emc.json"
1646
+ wait-for-parsers="customParser"></script>
1647
+ </be-hive>
1648
+
1649
+ <script type="module">
1650
+ import { registerParser } from 'assign-gingerly/parserRegistry.js';
1651
+
1652
+ // Load parser programmatically
1653
+ const parser = await import('./custom-parser.js');
1654
+ const beHive = document.getElementById('myHive');
1655
+
1656
+ registerParser(beHive, 'customParser', parser.default);
1657
+ </script>
1658
+ ```
1659
+
1660
+ ### Parser Interface
1661
+
1662
+ Parsers are simple functions that transform attribute string values:
1663
+
1664
+ ```javascript
1665
+ // my-parser.js
1666
+ export default function parse(value) {
1667
+ // Transform the string value
1668
+ if (value === null) return null;
1669
+
1670
+ // Your parsing logic here
1671
+ return parsedValue;
1672
+ }
1673
+ ```
1674
+
1675
+ **Requirements:**
1676
+ - Parser must be a function with signature `(v: string | null) => any`
1677
+ - Must be exported as the default export
1678
+ - Should handle `null` values appropriately
1679
+ - Should throw descriptive errors for invalid input
1680
+
1681
+ ### Scoped vs Global Parsers
1682
+
1683
+ **Scoped parsers** (registered in be-hive elements):
1684
+ - Isolated to a specific synthesizer element and its descendants
1685
+ - Different frameworks can use the same parser names without conflicts
1686
+ - Registered via `emc-parser` scripts or `registerParser()`
1687
+
1688
+ **Global parsers** (built-in):
1689
+ - Available everywhere without registration
1690
+ - Includes: `timestamp`, `date`, `csv`, `int`, `float`, `boolean`, `json`
1691
+ - Fallback when parser not found in scoped registry
1692
+
1693
+ **Resolution order:**
1694
+ 1. Check scoped registry (if synthesizer element exists)
1695
+ 2. Fall back to global registry
1696
+ 3. Throw error if not found in either
1697
+
1698
+ ### Shadow DOM Syndication
1699
+
1700
+ Parser registries are scoped to synthesizer elements and apply to all shadow roots within that scope:
1701
+
1702
+ ```html
1703
+ <!-- Syndicator in document root -->
1704
+ <be-hive>
1705
+ <script type="emc-parser" src="parser.js" parser-name="myParser"></script>
1706
+ <script type="emc" src="enhancement/emc.json" wait-for-parsers="myParser"></script>
1707
+ </be-hive>
1708
+
1709
+ <!-- Component with shadow root -->
1710
+ <my-component>
1711
+ #shadow
1712
+ <!-- Subscriber receives scripts from syndicator -->
1713
+ <be-hive></be-hive>
1714
+
1715
+ <!-- Elements here can use the parser -->
1716
+ <div be-switched="...">Content</div>
1717
+ </my-component>
1718
+ ```
1719
+
1720
+ **How it works:**
1721
+ 1. Parser script is syndicated to shadow root's be-hive
1722
+ 2. Parser is registered in the shadow root's scoped registry
1723
+ 3. EMC script is syndicated and waits for parser
1724
+ 4. Enhancements in shadow root have access to the parser
1725
+
1726
+ ### Error Handling
1727
+
1728
+ **Parser loading errors:**
1729
+
1730
+ ```html
1731
+ <!-- Parser module fails to load -->
1732
+ <script type="emc-parser"
1733
+ src="broken-parser.js"
1734
+ parser-name="broken"
1735
+ data-parser-error="Module not found"></script>
1736
+ ```
1737
+
1738
+ **Parser waiting timeout:**
1739
+
1740
+ ```html
1741
+ <!-- EMC script times out waiting for parser -->
1742
+ <script type="emc"
1743
+ src="my-enhancement/emc.json"
1744
+ wait-for-parsers="missingParser"
1745
+ data-emc-error="Timeout waiting for parsers: missingParser"></script>
1746
+ ```
1747
+
1748
+ **Error messages include:**
1749
+ - Which parser(s) are missing
1750
+ - Which EMC script is waiting
1751
+ - Suggestions to check parser-name and script order
1752
+
1753
+ ### Complete Example
1754
+
1755
+ ```html
1756
+ <!DOCTYPE html>
1757
+ <html>
1758
+ <head>
1759
+ <script type="module">
1760
+ import { MountObserver } from 'mount-observer/MountObserver.js';
1761
+ import { Synthesizer } from 'mount-observer/Synthesizer.js';
1762
+
1763
+ // Define synthesizer element
1764
+ class BeHive extends Synthesizer {}
1765
+ customElements.define('be-hive', BeHive);
1766
+ </script>
1767
+ </head>
1768
+ <body>
1769
+ <!-- Syndicator with parser and enhancement -->
1770
+ <be-hive>
1771
+ <!-- Load complex parser -->
1772
+ <script type="emc-parser"
1773
+ src="nested-regex-groups/parser.js"
1774
+ parser-name="nestedRegexGroups"></script>
1775
+
1776
+ <!-- Enhancement that uses the parser -->
1777
+ <script type="emc"
1778
+ src="be-switched/emc.json"
1779
+ wait-for-parsers="nestedRegexGroups"></script>
1780
+ </be-hive>
1781
+
1782
+ <!-- Component using the enhancement -->
1783
+ <my-component>
1784
+ #shadow
1785
+ <be-hive></be-hive>
1786
+
1787
+ <!-- This element will be enhanced -->
1788
+ <div be-switched="case1: /pattern1/ | case2: /pattern2/">
1789
+ Content
1790
+ </div>
1791
+ </my-component>
1792
+ </body>
1793
+ </html>
1794
+ ```
1795
+
1796
+ ### Benefits
1797
+
1798
+ - **Lazy loading**: Load parsers only when needed
1799
+ - **Framework isolation**: Different frameworks don't conflict
1800
+ - **Declarative**: HTML-first approach with script tags
1801
+ - **Flexible**: Supports both declarative and programmatic registration
1802
+ - **Automatic**: Parser waiting handled by the framework
1803
+ - **Scoped**: Each synthesizer has its own parser registry
1804
+ - **Efficient**: Parsers are cached and reused
1805
+
1806
+ ### Requirements
1807
+
1808
+ - Must import `mount-observer/handlers/EMCParserScript.js` for parser loading
1809
+ - Must import `mount-observer/handlers/EMCScript.js` for EMC processing
1810
+ - Must use a synthesizer element (be-hive, etc.) for scoped registries
1811
+ - Parser modules must export a function as default export
1812
+ - EMC scripts must specify `wait-for-parsers` attribute to wait for parsers
1813
+
1814
+ [Implemented as Scoped Parser Registry requirement](.kiro/specs/scoped-parser-registry-requirements.md)
1815
+
1558
1816
 
1559
1817
  # Thorough Exposition Begins Here
1560
1818
 
package/Synthesizer.js CHANGED
@@ -11,6 +11,8 @@ import 'mount-observer/handlers/HoistTemplate.js';
11
11
  import { hoist } from 'mount-observer/handlers/HoistTemplate.js';
12
12
  import { emc } from 'mount-observer/handlers/EMCScript.js';
13
13
  import 'mount-observer/handlers/EMCScript.js';
14
+ import 'mount-observer/handlers/EMCParserScript.js';
15
+ import { emcParser } from 'mount-observer/handlers/EMCParserScript.js';
14
16
  /**
15
17
  * Track which root nodes have already had handlers activated.
16
18
  * Uses WeakSet to avoid memory leaks when nodes are garbage collected.
@@ -53,7 +55,7 @@ export class Synthesizer extends HTMLElement {
53
55
  * List of built-in handlers to activate.
54
56
  */
55
57
  static builtInHandlers = [
56
- mos, scriptExport, include, hoist, emc
58
+ mos, scriptExport, include, hoist, emcParser, emc
57
59
  ];
58
60
  connectedCallback() {
59
61
  // Synthesizer elements are infrastructure, not UI
@@ -145,7 +147,7 @@ export class Synthesizer extends HTMLElement {
145
147
  */
146
148
  #initializeSyndicator() {
147
149
  // Process existing script elements
148
- const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"]');
150
+ const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"]');
149
151
  scripts.forEach(script => {
150
152
  if (this.checkIfAllowed(script)) {
151
153
  this.#broadcastScript(script);
@@ -157,7 +159,7 @@ export class Synthesizer extends HTMLElement {
157
159
  for (const node of mutation.addedNodes) {
158
160
  if (node instanceof HTMLScriptElement) {
159
161
  const type = node.getAttribute('type');
160
- if (type === 'mountobserver' || type === 'emc') {
162
+ if (type === 'mountobserver' || type === 'emc' || type === 'emc-parser') {
161
163
  if (this.checkIfAllowed(node)) {
162
164
  this.#broadcastScript(node);
163
165
  }
@@ -190,7 +192,7 @@ export class Synthesizer extends HTMLElement {
190
192
  }
191
193
  // Process existing scripts from syndicator
192
194
  // Only process scripts that pass the syndicator's filtering
193
- const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"]');
195
+ const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"]');
194
196
  scripts.forEach(script => {
195
197
  if (syndicator.checkIfAllowed(script)) {
196
198
  this.#processScript(script);
package/Synthesizer.ts CHANGED
@@ -12,6 +12,8 @@ import 'mount-observer/handlers/HoistTemplate.js';
12
12
  import {hoist} from 'mount-observer/handlers/HoistTemplate.js';
13
13
  import {emc} from 'mount-observer/handlers/EMCScript.js';
14
14
  import 'mount-observer/handlers/EMCScript.js';
15
+ import 'mount-observer/handlers/EMCParserScript.js';
16
+ import {emcParser} from 'mount-observer/handlers/EMCParserScript.js';
15
17
 
16
18
  /**
17
19
  * Track which root nodes have already had handlers activated.
@@ -57,7 +59,7 @@ export abstract class Synthesizer extends HTMLElement {
57
59
  * List of built-in handlers to activate.
58
60
  */
59
61
  protected static builtInHandlers = [
60
- mos, scriptExport, include, hoist, emc
62
+ mos, scriptExport, include, hoist, emcParser, emc
61
63
  ];
62
64
 
63
65
  connectedCallback(): void {
@@ -164,7 +166,7 @@ export abstract class Synthesizer extends HTMLElement {
164
166
  */
165
167
  #initializeSyndicator(): void {
166
168
  // Process existing script elements
167
- const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"]');
169
+ const scripts = this.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"]');
168
170
  scripts.forEach(script => {
169
171
  if (this.checkIfAllowed(script as HTMLScriptElement)) {
170
172
  this.#broadcastScript(script as HTMLScriptElement);
@@ -177,7 +179,7 @@ export abstract class Synthesizer extends HTMLElement {
177
179
  for (const node of mutation.addedNodes) {
178
180
  if (node instanceof HTMLScriptElement) {
179
181
  const type = node.getAttribute('type');
180
- if (type === 'mountobserver' || type === 'emc') {
182
+ if (type === 'mountobserver' || type === 'emc' || type === 'emc-parser') {
181
183
  if (this.checkIfAllowed(node)) {
182
184
  this.#broadcastScript(node);
183
185
  }
@@ -215,7 +217,7 @@ export abstract class Synthesizer extends HTMLElement {
215
217
 
216
218
  // Process existing scripts from syndicator
217
219
  // Only process scripts that pass the syndicator's filtering
218
- const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"]');
220
+ const scripts = syndicator.querySelectorAll('script[type="mountobserver"], script[type="emc"], script[type="emc-parser"]');
219
221
  scripts.forEach(script => {
220
222
  if (syndicator.checkIfAllowed(script as HTMLScriptElement)) {
221
223
  this.#processScript(script as HTMLScriptElement);
@@ -0,0 +1,109 @@
1
+ import { EvtRt } from '../EvtRt.js';
2
+ import { getParserRegistry } from 'assign-gingerly/parserRegistry.js';
3
+ /**
4
+ * Handler for EMC Parser Script Elements.
5
+ * Processes script[type="emc-parser"] elements to load and register parser modules
6
+ * in the containing synthesizer element's scoped parser registry.
7
+ *
8
+ * Usage:
9
+ * <script type="emc-parser" src="./my-parser.js" parser-name="myParser"></script>
10
+ *
11
+ * The parser module should export a parser function as the default export:
12
+ * export default function myParser(v: string | null): any { ... }
13
+ */
14
+ export class EMCParserScriptHandler extends EvtRt {
15
+ // Static properties define default MountConfig constraints
16
+ static matching = 'script[type="emc-parser"]';
17
+ static whereInstanceOf = HTMLScriptElement;
18
+ async mount(mountedElement, mountConfig, context) {
19
+ this.abort(); // Clean up event listeners (one-time operation)
20
+ const scriptElement = mountedElement;
21
+ // Read required attributes
22
+ const src = scriptElement.getAttribute('src');
23
+ const parserName = scriptElement.getAttribute('parser-name');
24
+ // Validate required attributes
25
+ if (!src) {
26
+ const errorMsg = 'EMCParserScript: missing src attribute';
27
+ console.error(errorMsg, scriptElement);
28
+ scriptElement.setAttribute('data-parser-error', 'missing src attribute');
29
+ return;
30
+ }
31
+ if (!parserName) {
32
+ const errorMsg = 'EMCParserScript: missing parser-name attribute';
33
+ console.error(errorMsg, scriptElement);
34
+ scriptElement.setAttribute('data-parser-error', 'missing parser-name attribute');
35
+ return;
36
+ }
37
+ // Find containing synthesizer element
38
+ const synthesizerElement = this.findContainingSynthesizer(scriptElement);
39
+ if (!synthesizerElement) {
40
+ const errorMsg = 'EMCParserScript: no containing synthesizer element found';
41
+ console.error(errorMsg, scriptElement);
42
+ scriptElement.setAttribute('data-parser-error', 'no containing synthesizer element');
43
+ return;
44
+ }
45
+ try {
46
+ // Dynamic import the parser module
47
+ const module = await import(src);
48
+ // Get parser function from default export
49
+ const parser = module.default;
50
+ // Validate parser is a function
51
+ if (typeof parser !== 'function') {
52
+ throw new Error(`Parser module "${src}" must export a function as default export. ` +
53
+ `Received: ${typeof parser}`);
54
+ }
55
+ // Get scoped registry and register parser
56
+ const registry = getParserRegistry(synthesizerElement);
57
+ registry.register(parserName, parser);
58
+ // Dispatch parser-registered event
59
+ scriptElement.dispatchEvent(new CustomEvent('parser-registered', {
60
+ detail: { parserName },
61
+ bubbles: true
62
+ }));
63
+ console.log(`Registered parser "${parserName}" from ${src}`);
64
+ }
65
+ catch (error) {
66
+ const errorMessage = error instanceof Error ? error.message : String(error);
67
+ console.error(`Failed to load parser "${parserName}" from "${src}":`, errorMessage);
68
+ scriptElement.setAttribute('data-parser-error', errorMessage);
69
+ }
70
+ }
71
+ /**
72
+ * Find the nearest ancestor synthesizer element.
73
+ * Traverses up through shadow root boundaries.
74
+ * Looks for elements with data-synthesizer attribute, be-hive tag, or __isSynthesizer property.
75
+ */
76
+ findContainingSynthesizer(element) {
77
+ let current = element;
78
+ while (current) {
79
+ if (current instanceof Element) {
80
+ // Check for synthesizer marker or known synthesizer tag names
81
+ if (current.hasAttribute('data-synthesizer') ||
82
+ current.localName === 'be-hive' ||
83
+ current.__isSynthesizer === true) {
84
+ return current;
85
+ }
86
+ }
87
+ // Try parent element
88
+ if (current.parentElement) {
89
+ current = current.parentElement;
90
+ }
91
+ // Try shadow root host
92
+ else if (current instanceof ShadowRoot) {
93
+ current = current.host;
94
+ }
95
+ // Try parent node (for document fragments)
96
+ else if (current.parentNode) {
97
+ current = current.parentNode;
98
+ }
99
+ else {
100
+ break;
101
+ }
102
+ }
103
+ return undefined;
104
+ }
105
+ }
106
+ // Register built-in handler
107
+ import { MountObserver } from '../MountObserver.js';
108
+ export const emcParser = 'builtIns.emcParserScript';
109
+ MountObserver.define(emcParser, EMCParserScriptHandler);
@@ -0,0 +1,132 @@
1
+ import { EvtRt } from '../EvtRt.js';
2
+ import { MountConfig, MountContext } from '../types/mount-observer/types.js';
3
+ import { getParserRegistry } from 'assign-gingerly/parserRegistry.js';
4
+
5
+ /**
6
+ * Handler for EMC Parser Script Elements.
7
+ * Processes script[type="emc-parser"] elements to load and register parser modules
8
+ * in the containing synthesizer element's scoped parser registry.
9
+ *
10
+ * Usage:
11
+ * <script type="emc-parser" src="./my-parser.js" parser-name="myParser"></script>
12
+ *
13
+ * The parser module should export a parser function as the default export:
14
+ * export default function myParser(v: string | null): any { ... }
15
+ */
16
+ export class EMCParserScriptHandler extends EvtRt {
17
+ // Static properties define default MountConfig constraints
18
+ static matching = 'script[type="emc-parser"]';
19
+ static whereInstanceOf = HTMLScriptElement;
20
+
21
+ async mount(mountedElement: Element, mountConfig: MountConfig, context: MountContext): Promise<void> {
22
+ this.abort(); // Clean up event listeners (one-time operation)
23
+
24
+ const scriptElement = mountedElement as HTMLScriptElement;
25
+
26
+ // Read required attributes
27
+ const src = scriptElement.getAttribute('src');
28
+ const parserName = scriptElement.getAttribute('parser-name');
29
+
30
+ // Validate required attributes
31
+ if (!src) {
32
+ const errorMsg = 'EMCParserScript: missing src attribute';
33
+ console.error(errorMsg, scriptElement);
34
+ scriptElement.setAttribute('data-parser-error', 'missing src attribute');
35
+ return;
36
+ }
37
+
38
+ if (!parserName) {
39
+ const errorMsg = 'EMCParserScript: missing parser-name attribute';
40
+ console.error(errorMsg, scriptElement);
41
+ scriptElement.setAttribute('data-parser-error', 'missing parser-name attribute');
42
+ return;
43
+ }
44
+
45
+ // Find containing synthesizer element
46
+ const synthesizerElement = this.findContainingSynthesizer(scriptElement);
47
+ if (!synthesizerElement) {
48
+ const errorMsg = 'EMCParserScript: no containing synthesizer element found';
49
+ console.error(errorMsg, scriptElement);
50
+ scriptElement.setAttribute('data-parser-error', 'no containing synthesizer element');
51
+ return;
52
+ }
53
+
54
+ try {
55
+ // Dynamic import the parser module
56
+ const module = await import(src);
57
+
58
+ // Get parser function from default export
59
+ const parser = module.default;
60
+
61
+ // Validate parser is a function
62
+ if (typeof parser !== 'function') {
63
+ throw new Error(
64
+ `Parser module "${src}" must export a function as default export. ` +
65
+ `Received: ${typeof parser}`
66
+ );
67
+ }
68
+
69
+ // Get scoped registry and register parser
70
+ const registry = getParserRegistry(synthesizerElement);
71
+ registry.register(parserName, parser);
72
+
73
+ // Dispatch parser-registered event
74
+ scriptElement.dispatchEvent(new CustomEvent('parser-registered', {
75
+ detail: { parserName },
76
+ bubbles: true
77
+ }));
78
+
79
+ console.log(`Registered parser "${parserName}" from ${src}`);
80
+
81
+ } catch (error) {
82
+ const errorMessage = error instanceof Error ? error.message : String(error);
83
+ console.error(`Failed to load parser "${parserName}" from "${src}":`, errorMessage);
84
+ scriptElement.setAttribute('data-parser-error', errorMessage);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Find the nearest ancestor synthesizer element.
90
+ * Traverses up through shadow root boundaries.
91
+ * Looks for elements with data-synthesizer attribute, be-hive tag, or __isSynthesizer property.
92
+ */
93
+ private findContainingSynthesizer(element: Element): Element | undefined {
94
+ let current: Node | null = element;
95
+
96
+ while (current) {
97
+ if (current instanceof Element) {
98
+ // Check for synthesizer marker or known synthesizer tag names
99
+ if (current.hasAttribute('data-synthesizer') ||
100
+ current.localName === 'be-hive' ||
101
+ (current as any).__isSynthesizer === true) {
102
+ return current;
103
+ }
104
+ }
105
+
106
+ // Try parent element
107
+ if (current.parentElement) {
108
+ current = current.parentElement;
109
+ }
110
+ // Try shadow root host
111
+ else if (current instanceof ShadowRoot) {
112
+ current = (current as ShadowRoot).host;
113
+ }
114
+ // Try parent node (for document fragments)
115
+ else if (current.parentNode) {
116
+ current = current.parentNode;
117
+ }
118
+ else {
119
+ break;
120
+ }
121
+ }
122
+
123
+ return undefined;
124
+ }
125
+ }
126
+
127
+ // Register built-in handler
128
+ import { MountObserver } from '../MountObserver.js';
129
+
130
+ export const emcParser = 'builtIns.emcParserScript';
131
+
132
+ MountObserver.define(emcParser, EMCParserScriptHandler);
@@ -1,6 +1,7 @@
1
1
  import { EvtRt } from '../EvtRt.js';
2
2
  import '../ElementMountExtension.js';
3
3
  import 'assign-gingerly/object-extension.js';
4
+ import { getParserRegistry } from 'assign-gingerly/parserRegistry.js';
4
5
  /**
5
6
  * Handler for EMC (Element Mount Configuration) Script Elements.
6
7
  * Processes script[type="emc"] elements to declaratively configure element enhancements.
@@ -18,6 +19,29 @@ export class EMCScriptHandler extends EvtRt {
18
19
  async mount(mountedElement, MountConfig, context) {
19
20
  this.abort(); // Clean up event listeners (one-time operation)
20
21
  const scriptElement = mountedElement;
22
+ // Find containing synthesizer element early (needed for parser waiting)
23
+ const synthesizerElement = this.findContainingSynthesizer(scriptElement);
24
+ // Check for parser waiting before processing EMC config
25
+ const waitForParsers = scriptElement.getAttribute('wait-for-parsers');
26
+ if (waitForParsers && synthesizerElement) {
27
+ const parserNames = waitForParsers.trim().split(/\s+/).filter(name => name.length > 0);
28
+ if (parserNames.length > 0) {
29
+ // Read timeout attribute (default: 60000ms = 1 minute)
30
+ const timeoutAttr = scriptElement.getAttribute('data-parser-timeout');
31
+ const timeout = timeoutAttr ? parseInt(timeoutAttr, 10) : 60000;
32
+ try {
33
+ // Get scoped registry and wait for parsers
34
+ const registry = getParserRegistry(synthesizerElement);
35
+ await registry.waitFor(parserNames, timeout);
36
+ }
37
+ catch (error) {
38
+ const errorMessage = error instanceof Error ? error.message : String(error);
39
+ console.error(`Parser waiting failed for EMC script:`, errorMessage, scriptElement);
40
+ scriptElement.setAttribute('data-emc-error', errorMessage);
41
+ return; // Stop processing on timeout/error
42
+ }
43
+ }
44
+ }
21
45
  let emcConfig = scriptElement.export;
22
46
  if (!emcConfig) {
23
47
  // Check if script has src attribute
@@ -68,14 +92,14 @@ export class EMCScriptHandler extends EvtRt {
68
92
  scriptElement.id = `${scriptElement.parentElement.localName}.${enhKey}`;
69
93
  }
70
94
  // Construct MountConfig from EMC config and mount it
71
- const mountConfig = await this.buildMountConfig(emcConfig);
95
+ const mountConfig = await this.buildMountConfig(emcConfig, synthesizerElement);
72
96
  await scriptElement.mount(mountConfig);
73
97
  }
74
98
  /**
75
99
  * Build a MountConfig from an EMC config.
76
100
  * Combines the matching selector with withAttrs if present.
77
101
  */
78
- async buildMountConfig(emcConfig) {
102
+ async buildMountConfig(emcConfig, synthesizerElement) {
79
103
  const { enhConfig, ...mountConfigBase } = emcConfig;
80
104
  let matching = mountConfigBase.matching || '';
81
105
  // If withAttrs is defined, use buildCSSQuery to combine with matching
@@ -96,7 +120,7 @@ export class EMCScriptHandler extends EvtRt {
96
120
  ...mountConfigBase,
97
121
  matching,
98
122
  do: (mountedElement) => {
99
- return this.handleMount(mountedElement, emcConfig);
123
+ return this.handleMount(mountedElement, emcConfig, synthesizerElement);
100
124
  }
101
125
  };
102
126
  return mountConfig;
@@ -104,7 +128,7 @@ export class EMCScriptHandler extends EvtRt {
104
128
  /**
105
129
  * Handle when an element mounts that matches the EMC config.
106
130
  */
107
- async handleMount(mountedElement, emcConfig) {
131
+ async handleMount(mountedElement, emcConfig, synthesizerElement) {
108
132
  const enhKey = emcConfig.enhConfig.enhKey;
109
133
  // Step 1: Check if element already has this enhancement
110
134
  const enh = mountedElement.enh;
@@ -128,7 +152,9 @@ export class EMCScriptHandler extends EvtRt {
128
152
  if (!enh) {
129
153
  throw new Error('Element does not have enh property. Make sure ElementMountExtension is loaded.');
130
154
  }
131
- await enh.get(enhancementConfig);
155
+ // Pass synthesizerElement through SpawnContext if available
156
+ const spawnContext = synthesizerElement ? { synthesizerElement } : undefined;
157
+ await enh.get(enhancementConfig, spawnContext);
132
158
  }
133
159
  /**
134
160
  * Register an enhancement in the enhancement registry.
@@ -165,6 +191,40 @@ export class EMCScriptHandler extends EvtRt {
165
191
  enhancementRegistry.push(enhancementConfig);
166
192
  return enhancementConfig;
167
193
  }
194
+ /**
195
+ * Find the nearest ancestor synthesizer element.
196
+ * Traverses up through shadow root boundaries.
197
+ * Looks for elements with data-synthesizer attribute, be-hive tag, or __isSynthesizer property.
198
+ */
199
+ findContainingSynthesizer(element) {
200
+ let current = element;
201
+ while (current) {
202
+ if (current instanceof Element) {
203
+ // Check for synthesizer marker or known synthesizer tag names
204
+ if (current.hasAttribute('data-synthesizer') ||
205
+ current.localName === 'be-hive' ||
206
+ current.__isSynthesizer === true) {
207
+ return current;
208
+ }
209
+ }
210
+ // Try parent element
211
+ if (current.parentElement) {
212
+ current = current.parentElement;
213
+ }
214
+ // Try shadow root host
215
+ else if (current instanceof ShadowRoot) {
216
+ current = current.host;
217
+ }
218
+ // Try parent node (for document fragments)
219
+ else if (current.parentNode) {
220
+ current = current.parentNode;
221
+ }
222
+ else {
223
+ break;
224
+ }
225
+ }
226
+ return undefined;
227
+ }
168
228
  }
169
229
  // Register built-in handler
170
230
  import { MountObserver } from '../MountObserver.js';
@@ -2,6 +2,7 @@ import { EvtRt } from '../EvtRt.js';
2
2
  import { EMC, MountConfig, MountContext } from '../types/mount-observer/types.js';
3
3
  import '../ElementMountExtension.js';
4
4
  import 'assign-gingerly/object-extension.js';
5
+ import { getParserRegistry } from 'assign-gingerly/parserRegistry.js';
5
6
 
6
7
  /**
7
8
  * Handler for EMC (Element Mount Configuration) Script Elements.
@@ -23,6 +24,32 @@ export class EMCScriptHandler extends EvtRt {
23
24
 
24
25
  const scriptElement = mountedElement as HTMLScriptElement;
25
26
 
27
+ // Find containing synthesizer element early (needed for parser waiting)
28
+ const synthesizerElement = this.findContainingSynthesizer(scriptElement);
29
+
30
+ // Check for parser waiting before processing EMC config
31
+ const waitForParsers = scriptElement.getAttribute('wait-for-parsers');
32
+ if (waitForParsers && synthesizerElement) {
33
+ const parserNames = waitForParsers.trim().split(/\s+/).filter(name => name.length > 0);
34
+
35
+ if (parserNames.length > 0) {
36
+ // Read timeout attribute (default: 60000ms = 1 minute)
37
+ const timeoutAttr = scriptElement.getAttribute('data-parser-timeout');
38
+ const timeout = timeoutAttr ? parseInt(timeoutAttr, 10) : 60000;
39
+
40
+ try {
41
+ // Get scoped registry and wait for parsers
42
+ const registry = getParserRegistry(synthesizerElement);
43
+ await registry.waitFor(parserNames, timeout);
44
+ } catch (error) {
45
+ const errorMessage = error instanceof Error ? error.message : String(error);
46
+ console.error(`Parser waiting failed for EMC script:`, errorMessage, scriptElement);
47
+ scriptElement.setAttribute('data-emc-error', errorMessage);
48
+ return; // Stop processing on timeout/error
49
+ }
50
+ }
51
+ }
52
+
26
53
  let emcConfig = (scriptElement as any).export;
27
54
  if (!emcConfig) {
28
55
  // Check if script has src attribute
@@ -80,7 +107,7 @@ export class EMCScriptHandler extends EvtRt {
80
107
  }
81
108
 
82
109
  // Construct MountConfig from EMC config and mount it
83
- const mountConfig = await this.buildMountConfig(emcConfig);
110
+ const mountConfig = await this.buildMountConfig(emcConfig, synthesizerElement);
84
111
  await scriptElement.mount(mountConfig);
85
112
  }
86
113
 
@@ -88,7 +115,7 @@ export class EMCScriptHandler extends EvtRt {
88
115
  * Build a MountConfig from an EMC config.
89
116
  * Combines the matching selector with withAttrs if present.
90
117
  */
91
- private async buildMountConfig(emcConfig: EMC): Promise<MountConfig> {
118
+ private async buildMountConfig(emcConfig: EMC, synthesizerElement?: Element): Promise<MountConfig> {
92
119
  const { enhConfig, ...mountConfigBase } = emcConfig;
93
120
 
94
121
  let matching = mountConfigBase.matching || '';
@@ -112,7 +139,7 @@ export class EMCScriptHandler extends EvtRt {
112
139
  ...mountConfigBase,
113
140
  matching,
114
141
  do: (mountedElement: Element) => {
115
- return this.handleMount(mountedElement, emcConfig);
142
+ return this.handleMount(mountedElement, emcConfig, synthesizerElement);
116
143
  }
117
144
  };
118
145
 
@@ -122,7 +149,7 @@ export class EMCScriptHandler extends EvtRt {
122
149
  /**
123
150
  * Handle when an element mounts that matches the EMC config.
124
151
  */
125
- private async handleMount(mountedElement: Element, emcConfig: EMC): Promise<void> {
152
+ private async handleMount(mountedElement: Element, emcConfig: EMC, synthesizerElement?: Element): Promise<void> {
126
153
  const enhKey = emcConfig.enhConfig.enhKey;
127
154
 
128
155
  // Step 1: Check if element already has this enhancement
@@ -153,7 +180,9 @@ export class EMCScriptHandler extends EvtRt {
153
180
  throw new Error('Element does not have enh property. Make sure ElementMountExtension is loaded.');
154
181
  }
155
182
 
156
- await enh.get(enhancementConfig);
183
+ // Pass synthesizerElement through SpawnContext if available
184
+ const spawnContext = synthesizerElement ? { synthesizerElement } : undefined;
185
+ await enh.get(enhancementConfig, spawnContext);
157
186
  }
158
187
 
159
188
  /**
@@ -198,6 +227,44 @@ export class EMCScriptHandler extends EvtRt {
198
227
 
199
228
  return enhancementConfig;
200
229
  }
230
+
231
+ /**
232
+ * Find the nearest ancestor synthesizer element.
233
+ * Traverses up through shadow root boundaries.
234
+ * Looks for elements with data-synthesizer attribute, be-hive tag, or __isSynthesizer property.
235
+ */
236
+ private findContainingSynthesizer(element: Element): Element | undefined {
237
+ let current: Node | null = element;
238
+
239
+ while (current) {
240
+ if (current instanceof Element) {
241
+ // Check for synthesizer marker or known synthesizer tag names
242
+ if (current.hasAttribute('data-synthesizer') ||
243
+ current.localName === 'be-hive' ||
244
+ (current as any).__isSynthesizer === true) {
245
+ return current;
246
+ }
247
+ }
248
+
249
+ // Try parent element
250
+ if (current.parentElement) {
251
+ current = current.parentElement;
252
+ }
253
+ // Try shadow root host
254
+ else if (current instanceof ShadowRoot) {
255
+ current = (current as ShadowRoot).host;
256
+ }
257
+ // Try parent node (for document fragments)
258
+ else if (current.parentNode) {
259
+ current = current.parentNode;
260
+ }
261
+ else {
262
+ break;
263
+ }
264
+ }
265
+
266
+ return undefined;
267
+ }
201
268
  }
202
269
 
203
270
  // Register built-in handler
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "mount-observer",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Observe and act on css matches.",
5
5
  "main": "MountObserver.js",
6
6
  "module": "MountObserver.js",
7
7
  "dependencies": {
8
- "assign-gingerly": "0.0.26",
8
+ "assign-gingerly": "0.0.27",
9
9
  "id-generation": "0.0.4"
10
10
  },
11
11
  "devDependencies": {
@@ -74,6 +74,44 @@ export type pathString = `?.${string}`;
74
74
  export type CustomElementName = string;
75
75
  export type CustomElementConstructorStaticMethodName = string;
76
76
 
77
+ /**
78
+ * Context passed to parser functions
79
+ * Provides access to configuration and spawn context for advanced parsing scenarios
80
+ */
81
+ export interface ParserContext<T = any> {
82
+ /**
83
+ * The attribute configuration that matched this attribute
84
+ * Useful for parsers that need to access additional config properties
85
+ */
86
+ attrConfig: AttrConfig<T>;
87
+
88
+ /**
89
+ * The spawn context containing enhancement config and synthesizer element
90
+ * Useful for parsers that need access to the enhancement or synthesizer context
91
+ */
92
+ spawnContext?: SpawnContext<T>;
93
+
94
+ /**
95
+ * The element being enhanced
96
+ * Useful for parsers that need to read other attributes or element properties
97
+ */
98
+ element: Element;
99
+
100
+ /**
101
+ * The attribute name that was matched (resolved from template)
102
+ * Useful for parsers that handle multiple attributes
103
+ */
104
+ attrName: string;
105
+ }
106
+
107
+ /**
108
+ * Parser function signature
109
+ * Can accept just the attribute value (simple form) or value + context (advanced form)
110
+ */
111
+ export type ParserFunction<T = any> =
112
+ | ((attrValue: string | null) => any)
113
+ | ((attrValue: string | null, context?: ParserContext<T>) => any);
114
+
77
115
  export interface AttrConfig<T = any> {
78
116
  /**
79
117
  * Type of the property value (JSON-serializable string format)
@@ -100,13 +138,19 @@ export interface AttrConfig<T = any> {
100
138
  /**
101
139
  * Parser to transform attribute string value
102
140
  * - Function: Inline parser function (not JSON serializable)
103
- * - String: Named parser reference (JSON serializable) - looks up in global parser registry (e.g., 'timestamp', 'csv')
104
- * - Tuple: [CustomElementName, StaticMethodName] - looks up static method on custom element constructor (e.g., ['my-widget', 'parseSpecial'])
141
+ * - Simple form: (attrValue: string | null) => any
142
+ * - Advanced form: (attrValue: string | null, context: ParserContext) => any
143
+ * - String: Named parser reference (JSON serializable) - looks up in scoped registry (if available) then global parser registry (e.g., 'timestamp', 'csv')
144
+ *
145
+ * Parser functions can optionally accept a second parameter (ParserContext) which provides:
146
+ * - attrConfig: The full AttrConfig object for this attribute
147
+ * - spawnContext: The SpawnContext with enhancement config and synthesizer element
148
+ * - element: The element being enhanced
149
+ * - attrName: The resolved attribute name
105
150
  */
106
151
  parser?:
107
- | ((attrValue: string | null) => any)
108
- | string
109
- | [CustomElementName, CustomElementConstructorStaticMethodName]
152
+ | ParserFunction<T>
153
+ | string
110
154
  ;
111
155
 
112
156
  /**
@@ -156,6 +200,12 @@ export type AttrPatterns<T = any> = {
156
200
  export interface SpawnContext<T = any, TMountContext = any> {
157
201
  config: EnhancementConfig<T>;
158
202
  mountCtx?: TMountContext;
203
+ /**
204
+ * Reference to the synthesizer element (be-hive, htmx-container, alpine-scope, etc.)
205
+ * that contains the EMC script defining this enhancement.
206
+ * Used for scoped parser registry access during attribute parsing.
207
+ */
208
+ synthesizerElement?: Element;
159
209
  }
160
210
 
161
211
  /**
@@ -1,3 +1,17 @@
1
- export interface BeABeaconProps {
1
+ export interface EndUserProps {
2
2
  eventName: string;
3
+ }
4
+
5
+ export interface AllProps extends EndUserProps {
6
+ enhancedElement: Element;
7
+ }
8
+
9
+ export type AP = AllProps;
10
+
11
+ export type PAP = Partial<AP>;
12
+
13
+ export type ProPAP = Promise<PAP>;
14
+
15
+ export interface Actions {
16
+ hydrate(self: AP): PAP;
3
17
  }
@@ -0,0 +1,19 @@
1
+ export interface EndUserProps{
2
+ closeOnSelect: boolean;
3
+ eventName: string;
4
+ }
5
+
6
+ export interface AllProps extends EndUserProps {
7
+ enhancedElement: Element;
8
+ }
9
+
10
+ export type AP = AllProps;
11
+
12
+ export type PAP = Partial<AP>;
13
+
14
+ export type ProPAP = Promise<PAP>;
15
+
16
+ export interface Actions{
17
+ init(self: AllProps, enhancedElement: Element, initVals: PAP): void;
18
+ hydrate(self: AP): PAP | void
19
+ }
@@ -7,7 +7,7 @@ export interface EndUserProps{
7
7
  export interface AllProps extends EndUserProps{
8
8
  enhancedElement: Element;
9
9
  byob?: boolean;
10
- trigger?: HTMLButtonElement;
10
+ trigger: HTMLButtonElement;
11
11
  resolved: boolean;
12
12
  }
13
13
 
@@ -20,10 +20,11 @@ export type ProPAP = Promise<PAP>;
20
20
  export interface CustomData {
21
21
  triggerSettings: {
22
22
  type: string;
23
- //trigger.classList.add('be-clonable-trigger');
23
+ '?.classList?.add': string;
24
24
  ariaLabel: string;
25
25
  title: string;
26
- }
26
+ },
27
+ withMethods: string[]
27
28
 
28
29
  }
29
30
 
@@ -15,12 +15,8 @@ export type PAP = Partial<AP>;
15
15
 
16
16
  export type ProPAP = Promise<PAP>;
17
17
 
18
- export type BAP = AllProps // & BEAllProps & RoundaboutReady;
19
-
20
18
  export interface Actions{
21
19
 
22
- hydrate(self: BAP): ProPAP;
23
- init(self: BAP, initVals: PAP): Promise<void>
24
- // findTarget(self: this): Promise<void>;
25
- // handleCommit(self: this, e: KeyboardEvent): Promise<void>;
20
+ hydrate(self: AP): ProPAP;
21
+ init(self: AP, enhancedElement: Element, initVals: PAP): Promise<void>
26
22
  }
@@ -0,0 +1,25 @@
1
+ export interface EndUserProps{
2
+ triggerInsertPosition: InsertPosition;
3
+ buttonContent: string;
4
+ }
5
+
6
+ export interface AllProps extends EndUserProps{
7
+ enhancedElement: Element;
8
+ byob?: boolean,
9
+ trigger: HTMLButtonElement;
10
+ resolved: boolean,
11
+ }
12
+
13
+ export type AP = AllProps;
14
+
15
+ export type PAP = Partial<AP>;
16
+
17
+ export type ProPAP = Promise<PAP>;
18
+
19
+ export interface Actions{
20
+
21
+ addDeleteBtn(self: AP): ProPAP ;
22
+ setBtnContent(self: AP): void;
23
+ beDeleted(self: AP): void;
24
+ init(self: AP & Actions, enhancedElement: Element, initVals: PAP): Promise<void>;
25
+ }
@@ -0,0 +1,29 @@
1
+ export interface RenderingHTMLScriptElement extends HTMLScriptElement{
2
+ renderer: (vm: any, html: any) => any,
3
+ }
4
+
5
+ export interface EndUserProps{
6
+ vm: any,
7
+ with: Array<string>,
8
+ }
9
+
10
+ export type Renderer = (vm: any, html: any) => any;
11
+
12
+ export interface AllProps extends EndUserProps{
13
+ enhancedElement: Element;
14
+ renderer: Renderer,
15
+ absorbingObject: any
16
+ }
17
+
18
+ export type PAP = Partial<AllProps>;
19
+
20
+ export type AP = AllProps;
21
+
22
+ export type ProPAP = Promise<PAP>;
23
+
24
+ export interface Actions {
25
+ getRenderer(self: AP): PAP;
26
+ doRender(self: AP): void;
27
+ observe(self: AP): ProPAP;
28
+ absorb(self: AP, e?: Event): ProPAP;
29
+ }
@@ -0,0 +1,31 @@
1
+ export interface EndUserProps {
2
+ triggerInsertPosition: InsertPosition;
3
+ labelTextContainer: string;
4
+ buttonContent: string;
5
+ nudge?: boolean;
6
+ }
7
+
8
+ export interface AllProps extends EndUserProps{
9
+ enhancedElement: Element;
10
+ byob?: boolean;
11
+ trigger: WeakRef<HTMLButtonElement>
12
+ }
13
+
14
+ export type AP = AllProps;
15
+
16
+ export type PAP = Partial<AP>;
17
+
18
+ export type ProPAP = Promise<PAP>;
19
+
20
+
21
+
22
+ export interface Actions{
23
+ addTypeBtn(self: AP): ProPAP;
24
+ setBtnContent(self: AP): void;
25
+ openDialog(self: AP): Promise<void>
26
+ }
27
+
28
+ export interface ITyper{
29
+ showDialog(): void;
30
+ dispose(): void;
31
+ }
@@ -97,6 +97,8 @@ export interface RAConfig<TProps = unknown, TActions = TProps, ETProps = TProps,
97
97
  defaultPropVals?: Partial<{[key in keyof TProps & string]: unknown}>,
98
98
 
99
99
  customData?: TCustomData,
100
+
101
+ initialPropVals?: Partial<{[key in keyof TProps & string]: unknown}>,
100
102
  }
101
103
 
102
104
  export interface RoundaboutOptions<TProps = unknown, TActions = TProps, ETProps = TProps> extends RAConfig<TProps, TActions, ETProps> {