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
242 lines
8.4 KiB
JavaScript
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();
|