import { EvtRt } from '../EvtRt.js';
import { MountConfig, MountContext } from '../types/mount-observer/types.js';
import { getParserRegistry } from 'assign-gingerly/parserRegistry.js';
/**
* Handler for EMC Parser Script Elements.
* Processes script[type="emc-parser"] elements to load and register parser modules
* in the containing synthesizer element's scoped parser registry.
*
* Usage:
*
*
* The parser module should export a parser function as the default export:
* export default function myParser(v: string | null): any { ... }
*/
export class EMCParserScriptHandler extends EvtRt {
// Static properties define default MountConfig constraints
static matching = 'script[type="emc-parser"]';
static whereInstanceOf = HTMLScriptElement;
async mount(mountedElement: Element, mountConfig: MountConfig, context: MountContext): Promise {
this.abort(); // Clean up event listeners (one-time operation)
const scriptElement = mountedElement as HTMLScriptElement;
// Read required attributes
const src = scriptElement.getAttribute('src');
const parserName = scriptElement.getAttribute('parser-name');
// Validate required attributes
if (!src) {
const errorMsg = 'EMCParserScript: missing src attribute';
console.error(errorMsg, scriptElement);
scriptElement.setAttribute('data-parser-error', 'missing src attribute');
return;
}
if (!parserName) {
const errorMsg = 'EMCParserScript: missing parser-name attribute';
console.error(errorMsg, scriptElement);
scriptElement.setAttribute('data-parser-error', 'missing parser-name attribute');
return;
}
// Find containing synthesizer element
const synthesizerElement = this.findContainingSynthesizer(scriptElement);
if (!synthesizerElement) {
const errorMsg = 'EMCParserScript: no containing synthesizer element found';
console.error(errorMsg, scriptElement);
scriptElement.setAttribute('data-parser-error', 'no containing synthesizer element');
return;
}
try {
// Dynamic import the parser module
const module = await import(src);
// Get parser function from default export
const parser = module.default;
// Validate parser is a function
if (typeof parser !== 'function') {
throw new Error(
`Parser module "${src}" must export a function as default export. ` +
`Received: ${typeof parser}`
);
}
// Get scoped registry and register parser
const registry = getParserRegistry(synthesizerElement);
registry.register(parserName, parser);
// Dispatch parser-registered event
scriptElement.dispatchEvent(new CustomEvent('parser-registered', {
detail: { parserName },
bubbles: true
}));
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Failed to load parser "${parserName}" from "${src}":`, errorMessage);
scriptElement.setAttribute('data-parser-error', errorMessage);
}
}
/**
* Find the nearest ancestor synthesizer element.
* Traverses up through shadow root boundaries.
* Looks for elements with data-synthesizer attribute, be-hive tag, or __isSynthesizer property.
*/
private findContainingSynthesizer(element: Element): Element | undefined {
let current: Node | null = element;
while (current) {
if (current instanceof Element) {
// Check for synthesizer marker or known synthesizer tag names
if (current.hasAttribute('data-synthesizer') ||
current.localName === 'be-hive' ||
(current as any).__isSynthesizer === true) {
return current;
}
}
// Try parent element
if (current.parentElement) {
current = current.parentElement;
}
// Try shadow root host
else if (current instanceof ShadowRoot) {
current = (current as ShadowRoot).host;
}
// Try parent node (for document fragments)
else if (current.parentNode) {
current = current.parentNode;
}
else {
break;
}
}
return undefined;
}
}
// Register built-in handler
import { MountObserver } from '../MountObserver.js';
export const emcParser = 'builtIns.emcParserScript';
MountObserver.define(emcParser, EMCParserScriptHandler);