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 +258 -0
- package/Synthesizer.js +6 -4
- package/Synthesizer.ts +6 -4
- package/handlers/EMCParserScript.js +109 -0
- package/handlers/EMCParserScript.ts +132 -0
- package/handlers/EMCScript.js +65 -5
- package/handlers/EMCScript.ts +72 -5
- package/package.json +2 -2
- package/types/assign-gingerly/types.d.ts +55 -5
- package/types/be-a-beacon/types.d.ts +15 -1
- package/types/be-buttoned-up/types.d.ts +19 -0
- package/types/be-clonable/types.d.ts +4 -3
- package/types/be-committed/types.d.ts +2 -6
- package/types/be-delible/types.d.ts +25 -0
- package/types/be-render-neutral/types.d.ts +29 -0
- package/types/be-typed/types.d.ts +31 -0
- package/types/roundabout/types.d.ts +2 -0
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);
|
package/handlers/EMCScript.js
CHANGED
|
@@ -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
|
-
|
|
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';
|
package/handlers/EMCScript.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
104
|
-
*
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
23
|
-
init(self:
|
|
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> {
|