ui-dev-content/dist/core/ContentEditingRegistry.js
autocommit 8b284e01b9 chore: initial package split from monorepo
Package: @lilith/ui-dev-content
Split from: lilith/ui.git or lilith/build.git
Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main
2026-04-20 01:11:45 -07:00

242 lines
8.4 KiB
JavaScript

/**
* Content Editing Registry - Central plugin management
*
* This class manages all registered plugins (sources, transformers, sinks)
* and provides discovery and lookup methods.
*/
import { MANUAL_EDIT_TRANSFORMER_ID } from '../transformers/ManualTextEditTransformer';
export class ContentEditingRegistry {
constructor() {
this.sources = new Map();
this.transformers = new Map();
this.sinks = new Map();
}
// ============================================================================
// Registration Methods
// ============================================================================
/**
* Register a content source plugin
*/
registerSource(source) {
if (this.sources.has(source.id)) {
// Idempotent: silently skip re-registration (handles Strict Mode, HMR)
return;
}
this.sources.set(source.id, {
source,
registeredAt: new Date(),
enabled: true,
});
console.log(`[ContentEditingRegistry] Registered source: ${source.name} (${source.id})`);
}
/**
* Register a content transformer plugin
*/
registerTransformer(transformer) {
if (this.transformers.has(transformer.id)) {
// Idempotent: silently skip re-registration (handles Strict Mode, HMR)
return;
}
this.transformers.set(transformer.id, {
transformer,
registeredAt: new Date(),
enabled: true,
});
console.log(`[ContentEditingRegistry] Registered transformer: ${transformer.name} (${transformer.id})`);
}
/**
* Register a content sink plugin
*/
registerSink(sink) {
if (this.sinks.has(sink.id)) {
// Idempotent: silently skip re-registration (handles Strict Mode, HMR)
return;
}
this.sinks.set(sink.id, {
sink,
registeredAt: new Date(),
enabled: true,
});
console.log(`[ContentEditingRegistry] Registered sink: ${sink.name} (${sink.id})`);
}
// ============================================================================
// Discovery Methods
// ============================================================================
/**
* Detect all editable content from all registered sources
*/
async detectContent(root = document.body) {
const enabledSources = Array.from(this.sources.values())
.filter(reg => reg.enabled)
.map(reg => reg.source);
if (enabledSources.length === 0) {
console.warn('[ContentEditingRegistry] No sources registered, cannot detect content');
return [];
}
const results = await Promise.allSettled(enabledSources.map(source => source.detect(root)));
const handles = [];
for (const result of results) {
if (result.status === 'fulfilled') {
handles.push(...result.value);
}
else {
console.error('[ContentEditingRegistry] Source detection failed:', result.reason);
}
}
console.log(`[ContentEditingRegistry] Detected ${handles.length} editable content handles`);
return handles;
}
/**
* Get available transformers for a specific content handle
*
* Note: The 'manual-text-edit' transformer is always included as a fallback
* option, regardless of allowedTransformers restrictions. This ensures users
* can always manually edit content even when AI services are unavailable.
*/
getTransformers(handle, content) {
const enabledTransformers = Array.from(this.transformers.values())
.filter(reg => reg.enabled)
.map(reg => reg.transformer);
// Filter by canTransform and allowedTransformers
return enabledTransformers.filter(transformer => {
// Manual edit is always available as a fallback (bypasses allowedTransformers)
const isManualEdit = transformer.id === MANUAL_EDIT_TRANSFORMER_ID;
// Check if content handle restricts transformers (skip for manual edit)
if (!isManualEdit && handle.allowedTransformers && !handle.allowedTransformers.includes(transformer.id)) {
return false;
}
// Check if transformer can handle this content
try {
return transformer.canTransform(handle, content);
}
catch (error) {
console.error(`[ContentEditingRegistry] Transformer ${transformer.id} canTransform threw:`, error);
return false;
}
});
}
/**
* Get the appropriate sink for a content handle
*/
getSink(handle) {
const enabledSinks = Array.from(this.sinks.values())
.filter(reg => reg.enabled)
.map(reg => reg.sink);
// Find first sink that can handle this content
const sink = enabledSinks.find(s => {
try {
return s.canHandle(handle);
}
catch (error) {
console.error(`[ContentEditingRegistry] Sink ${s.id} canHandle threw:`, error);
return false;
}
});
if (!sink) {
console.warn(`[ContentEditingRegistry] No sink found for handle ${handle.sourceId}:${handle.identifier}`);
}
return sink || null;
}
// ============================================================================
// Lookup Methods
// ============================================================================
/**
* Get a registered source by ID
*/
getSource(id) {
return this.sources.get(id)?.source || null;
}
/**
* Get a registered transformer by ID
*/
getTransformer(id) {
return this.transformers.get(id)?.transformer || null;
}
/**
* Get a registered sink by ID
*/
getSinkById(id) {
return this.sinks.get(id)?.sink || null;
}
/**
* Get all registered sources
*/
getAllSources() {
return Array.from(this.sources.values())
.filter(reg => reg.enabled)
.map(reg => reg.source);
}
/**
* Get all registered transformers
*/
getAllTransformers() {
return Array.from(this.transformers.values())
.filter(reg => reg.enabled)
.map(reg => reg.transformer);
}
/**
* Get all registered sinks
*/
getAllSinks() {
return Array.from(this.sinks.values())
.filter(reg => reg.enabled)
.map(reg => reg.sink);
}
// ============================================================================
// Management Methods
// ============================================================================
/**
* Enable/disable a plugin
*/
setPluginEnabled(type, id, enabled) {
let registration;
switch (type) {
case 'source':
registration = this.sources.get(id);
break;
case 'transformer':
registration = this.transformers.get(id);
break;
case 'sink':
registration = this.sinks.get(id);
break;
}
if (registration) {
registration.enabled = enabled;
console.log(`[ContentEditingRegistry] ${type} '${id}' ${enabled ? 'enabled' : 'disabled'}`);
}
else {
console.warn(`[ContentEditingRegistry] ${type} '${id}' not found`);
}
}
/**
* Get registry statistics
*/
getStats() {
return {
sources: {
total: this.sources.size,
enabled: Array.from(this.sources.values()).filter(r => r.enabled).length,
},
transformers: {
total: this.transformers.size,
enabled: Array.from(this.transformers.values()).filter(r => r.enabled).length,
},
sinks: {
total: this.sinks.size,
enabled: Array.from(this.sinks.values()).filter(r => r.enabled).length,
},
};
}
/**
* Clear all registered plugins
*/
clear() {
this.sources.clear();
this.transformers.clear();
this.sinks.clear();
console.log('[ContentEditingRegistry] Cleared all plugins');
}
}
// Singleton instance for global access
export const contentEditingRegistry = new ContentEditingRegistry();