mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			main
			...
			feat/add-c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5332f015ef | ||
|  | a3d77421fd | 
							
								
								
									
										223
									
								
								apps/client/src/services/ckeditor_plugin_config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								apps/client/src/services/ckeditor_plugin_config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | ||||
| /** | ||||
|  * @module CKEditor Plugin Configuration Service | ||||
|  *  | ||||
|  * This service manages the dynamic configuration of CKEditor plugins based on user preferences. | ||||
|  * It handles plugin enablement, dependency resolution, and toolbar configuration. | ||||
|  */ | ||||
|  | ||||
| import server from "./server.js"; | ||||
| import type {  | ||||
|     PluginConfiguration,  | ||||
|     PluginMetadata,  | ||||
|     PluginRegistry, | ||||
|     PluginValidationResult | ||||
| } from "@triliumnext/commons"; | ||||
|  | ||||
| /** | ||||
|  * Cache for plugin registry and user configuration | ||||
|  */ | ||||
| let pluginRegistryCache: PluginRegistry | null = null; | ||||
| let userConfigCache: PluginConfiguration[] | null = null; | ||||
| let cacheTimestamp = 0; | ||||
| const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes | ||||
|  | ||||
| /** | ||||
|  * Get the plugin registry from server | ||||
|  */ | ||||
| export async function getPluginRegistry(): Promise<PluginRegistry> { | ||||
|     const now = Date.now(); | ||||
|     if (pluginRegistryCache && (now - cacheTimestamp) < CACHE_DURATION) { | ||||
|         return pluginRegistryCache; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|         pluginRegistryCache = await server.get<PluginRegistry>('ckeditor-plugins/registry'); | ||||
|         cacheTimestamp = now; | ||||
|         return pluginRegistryCache; | ||||
|     } catch (error) { | ||||
|         console.error('Failed to load CKEditor plugin registry:', error); | ||||
|         throw error; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the user's plugin configuration from server | ||||
|  */ | ||||
| export async function getUserPluginConfig(): Promise<PluginConfiguration[]> { | ||||
|     const now = Date.now(); | ||||
|     if (userConfigCache && (now - cacheTimestamp) < CACHE_DURATION) { | ||||
|         return userConfigCache; | ||||
|     } | ||||
|      | ||||
|     try { | ||||
|         userConfigCache = await server.get<PluginConfiguration[]>('ckeditor-plugins/config'); | ||||
|         cacheTimestamp = now; | ||||
|         return userConfigCache; | ||||
|     } catch (error) { | ||||
|         console.error('Failed to load user plugin configuration:', error); | ||||
|         throw error; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Clear the cache (call when configuration is updated) | ||||
|  */ | ||||
| export function clearCache(): void { | ||||
|     pluginRegistryCache = null; | ||||
|     userConfigCache = null; | ||||
|     cacheTimestamp = 0; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get the enabled plugins for the current user | ||||
|  */ | ||||
| export async function getEnabledPlugins(): Promise<Set<string>> { | ||||
|     const userConfig = await getUserPluginConfig(); | ||||
|     const enabledPlugins = new Set<string>(); | ||||
|      | ||||
|     // Add all enabled user plugins | ||||
|     userConfig.forEach(config => { | ||||
|         if (config.enabled) { | ||||
|             enabledPlugins.add(config.id); | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     // Always include core plugins | ||||
|     const registry = await getPluginRegistry(); | ||||
|     Object.values(registry.plugins).forEach(plugin => { | ||||
|         if (plugin.isCore) { | ||||
|             enabledPlugins.add(plugin.id); | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     return enabledPlugins; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get disabled plugin names for CKEditor config | ||||
|  */ | ||||
| export async function getDisabledPlugins(): Promise<string[]> { | ||||
|     try { | ||||
|         const registry = await getPluginRegistry(); | ||||
|         const enabledPlugins = await getEnabledPlugins(); | ||||
|         const disabledPlugins: string[] = []; | ||||
|          | ||||
|         // Find plugins that are disabled | ||||
|         Object.values(registry.plugins).forEach(plugin => { | ||||
|             if (!plugin.isCore && !enabledPlugins.has(plugin.id)) { | ||||
|                 // Map plugin ID to actual CKEditor plugin names if needed | ||||
|                 const pluginNames = getPluginNames(plugin.id); | ||||
|                 disabledPlugins.push(...pluginNames); | ||||
|             } | ||||
|         }); | ||||
|          | ||||
|         return disabledPlugins; | ||||
|     } catch (error) { | ||||
|         console.warn("Failed to get disabled plugins, returning empty list:", error); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Map plugin ID to actual CKEditor plugin names | ||||
|  * Some plugins might have multiple names or different names than their ID | ||||
|  */ | ||||
| function getPluginNames(pluginId: string): string[] { | ||||
|     const nameMap: Record<string, string[]> = { | ||||
|         "emoji": ["EmojiMention", "EmojiPicker"], | ||||
|         "math": ["Math", "AutoformatMath"], | ||||
|         "image": ["Image", "ImageCaption", "ImageInline", "ImageResize", "ImageStyle", "ImageToolbar", "ImageUpload"], | ||||
|         "table": ["Table", "TableToolbar", "TableProperties", "TableCellProperties", "TableSelection", "TableCaption", "TableColumnResize"], | ||||
|         "font": ["Font", "FontColor", "FontBackgroundColor"], | ||||
|         "list": ["List", "ListProperties"], | ||||
|         "specialcharacters": ["SpecialCharacters", "SpecialCharactersEssentials"], | ||||
|         "findandreplace": ["FindAndReplace"], | ||||
|         "horizontalline": ["HorizontalLine"], | ||||
|         "pagebreak": ["PageBreak"], | ||||
|         "removeformat": ["RemoveFormat"], | ||||
|         "alignment": ["Alignment"], | ||||
|         "indent": ["Indent", "IndentBlock"], | ||||
|         "codeblock": ["CodeBlock"], | ||||
|         "blockquote": ["BlockQuote"], | ||||
|         "todolist": ["TodoList"], | ||||
|         "heading": ["Heading", "HeadingButtonsUI"], | ||||
|         "paragraph": ["ParagraphButtonUI"], | ||||
|         // Add more mappings as needed | ||||
|     }; | ||||
|      | ||||
|     return nameMap[pluginId] || [pluginId.charAt(0).toUpperCase() + pluginId.slice(1)]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Validate the current plugin configuration | ||||
|  */ | ||||
| export async function validatePluginConfiguration(): Promise<PluginValidationResult> { | ||||
|     try { | ||||
|         const userConfig = await getUserPluginConfig(); | ||||
|         return await server.post<PluginValidationResult>('ckeditor-plugins/validate', { | ||||
|             plugins: userConfig | ||||
|         }); | ||||
|     } catch (error) { | ||||
|         console.error('Failed to validate plugin configuration:', error); | ||||
|         return { | ||||
|             valid: false, | ||||
|             errors: [{ | ||||
|                 type: "missing_dependency", | ||||
|                 pluginId: "unknown", | ||||
|                 message: `Validation failed: ${error}` | ||||
|             }], | ||||
|             warnings: [], | ||||
|             resolvedPlugins: [] | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get toolbar items that should be hidden based on disabled plugins | ||||
|  */ | ||||
| export async function getHiddenToolbarItems(): Promise<string[]> { | ||||
|     const registry = await getPluginRegistry(); | ||||
|     const enabledPlugins = await getEnabledPlugins(); | ||||
|     const hiddenItems: string[] = []; | ||||
|      | ||||
|     Object.values(registry.plugins).forEach(plugin => { | ||||
|         if (!enabledPlugins.has(plugin.id) && plugin.toolbarItems) { | ||||
|             hiddenItems.push(...plugin.toolbarItems); | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     return hiddenItems; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Update user plugin configuration | ||||
|  */ | ||||
| export async function updatePluginConfiguration(plugins: PluginConfiguration[]): Promise<void> { | ||||
|     try { | ||||
|         const response = await server.put('ckeditor-plugins/config', { | ||||
|             plugins, | ||||
|             validate: true | ||||
|         }); | ||||
|          | ||||
|         if (!response.success) { | ||||
|             throw new Error(response.errors?.join(", ") || "Update failed"); | ||||
|         } | ||||
|          | ||||
|         // Clear cache so next requests fetch fresh data | ||||
|         clearCache(); | ||||
|     } catch (error) { | ||||
|         console.error('Failed to update plugin configuration:', error); | ||||
|         throw error; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     getPluginRegistry, | ||||
|     getUserPluginConfig, | ||||
|     getEnabledPlugins, | ||||
|     getDisabledPlugins, | ||||
|     getHiddenToolbarItems, | ||||
|     validatePluginConfiguration, | ||||
|     updatePluginConfiguration, | ||||
|     clearCache | ||||
| }; | ||||
| @@ -1814,6 +1814,43 @@ | ||||
|       "multiline-toolbar": "Display the toolbar on multiple lines if it doesn't fit." | ||||
|     } | ||||
|   }, | ||||
|   "ckeditor_plugins": { | ||||
|     "title": "Editor Plugins", | ||||
|     "description": "Configure which CKEditor plugins are enabled. Changes take effect when the editor is reloaded.", | ||||
|     "loading": "Loading plugin configuration...", | ||||
|     "load_failed": "Failed to load plugin configuration.", | ||||
|     "load_error": "Error loading plugins", | ||||
|     "retry": "Retry", | ||||
|     "category_formatting": "Text Formatting", | ||||
|     "category_structure": "Document Structure", | ||||
|     "category_media": "Media & Files", | ||||
|     "category_tables": "Tables", | ||||
|     "category_advanced": "Advanced Features", | ||||
|     "category_trilium": "Trilium Features", | ||||
|     "category_external": "External Plugins", | ||||
|     "stats_enabled": "Enabled", | ||||
|     "stats_total": "Total", | ||||
|     "stats_core": "Core", | ||||
|     "stats_premium": "Premium", | ||||
|     "no_license": "no license", | ||||
|     "premium": "Premium", | ||||
|     "premium_required": "Requires premium CKEditor license", | ||||
|     "has_dependencies": "Dependencies", | ||||
|     "depends_on": "Depends on", | ||||
|     "toolbar_items": "Toolbar items", | ||||
|     "validate": "Validate", | ||||
|     "validation_error": "Validation failed", | ||||
|     "validation_errors": "Configuration Errors:", | ||||
|     "validation_warnings": "Configuration Warnings:", | ||||
|     "save": "Save Changes", | ||||
|     "save_success": "Plugin configuration saved successfully", | ||||
|     "save_error": "Failed to save configuration", | ||||
|     "reload_editor_notice": "Please reload any open text notes to apply changes", | ||||
|     "reset_defaults": "Reset to Defaults", | ||||
|     "reset_confirm": "Are you sure you want to reset all plugin settings to their default values?", | ||||
|     "reset_success": "Plugin configuration reset to defaults", | ||||
|     "reset_error": "Failed to reset configuration" | ||||
|   }, | ||||
|   "electron_context_menu": { | ||||
|     "add-term-to-dictionary": "Add \"{{term}}\" to dictionary", | ||||
|     "cut": "Cut", | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import noteAutocompleteService, { type Suggestion } from "../../../services/note | ||||
| import mimeTypesService from "../../../services/mime_types.js"; | ||||
| import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons"; | ||||
| import { buildToolbarConfig } from "./toolbar.js"; | ||||
| import ckeditorPluginConfigService from "../../../services/ckeditor_plugin_config.js"; | ||||
|  | ||||
| export const OPEN_SOURCE_LICENSE_KEY = "GPL"; | ||||
|  | ||||
| @@ -164,7 +165,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi | ||||
|         }, | ||||
|         // This value must be kept in sync with the language defined in webpack.config.js. | ||||
|         language: "en", | ||||
|         removePlugins: getDisabledPlugins() | ||||
|         removePlugins: await getDisabledPlugins() | ||||
|     }; | ||||
|  | ||||
|     // Set up content language. | ||||
| @@ -203,9 +204,11 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     const toolbarConfig = await buildToolbarConfig(opts.isClassicEditor); | ||||
|      | ||||
|     return { | ||||
|         ...config, | ||||
|         ...buildToolbarConfig(opts.isClassicEditor) | ||||
|         ...toolbarConfig | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @@ -237,9 +240,18 @@ function getLicenseKey() { | ||||
|     return premiumLicenseKey; | ||||
| } | ||||
|  | ||||
| function getDisabledPlugins() { | ||||
| async function getDisabledPlugins() { | ||||
|     let disabledPlugins: string[] = []; | ||||
|  | ||||
|     // Check user's plugin configuration | ||||
|     try { | ||||
|         const userDisabledPlugins = await ckeditorPluginConfigService.getDisabledPlugins(); | ||||
|         disabledPlugins.push(...userDisabledPlugins); | ||||
|     } catch (error) { | ||||
|         console.warn("Failed to load user plugin configuration, using defaults:", error); | ||||
|     } | ||||
|  | ||||
|     // Legacy emoji setting override | ||||
|     if (options.get("textNoteEmojiCompletionEnabled") !== "true") { | ||||
|         disabledPlugins.push("EmojiMention"); | ||||
|     } | ||||
|   | ||||
| @@ -1,33 +1,73 @@ | ||||
| import utils from "../../../services/utils.js"; | ||||
| import options from "../../../services/options.js"; | ||||
| import ckeditorPluginConfigService from "../../../services/ckeditor_plugin_config.js"; | ||||
|  | ||||
| const TEXT_FORMATTING_GROUP = { | ||||
|     label: "Text formatting", | ||||
|     icon: "text" | ||||
| }; | ||||
|  | ||||
| export function buildToolbarConfig(isClassicToolbar: boolean) { | ||||
| export async function buildToolbarConfig(isClassicToolbar: boolean) { | ||||
|     const hiddenItems = await getHiddenToolbarItems(); | ||||
|      | ||||
|     if (utils.isMobile()) { | ||||
|         return buildMobileToolbar(); | ||||
|         return buildMobileToolbar(hiddenItems); | ||||
|     } else if (isClassicToolbar) { | ||||
|         const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true"; | ||||
|         return buildClassicToolbar(multilineToolbar); | ||||
|         return buildClassicToolbar(multilineToolbar, hiddenItems); | ||||
|     } else { | ||||
|         return buildFloatingToolbar(); | ||||
|         return buildFloatingToolbar(hiddenItems); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export function buildMobileToolbar() { | ||||
|     const classicConfig = buildClassicToolbar(false); | ||||
| async function getHiddenToolbarItems(): Promise<Set<string>> { | ||||
|     try { | ||||
|         const hiddenItems = await ckeditorPluginConfigService.getHiddenToolbarItems(); | ||||
|         return new Set(hiddenItems); | ||||
|     } catch (error) { | ||||
|         console.warn("Failed to get hidden toolbar items, using empty set:", error); | ||||
|         return new Set(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Filter toolbar items based on disabled plugins | ||||
|  */ | ||||
| function filterToolbarItems(items: (string | object)[], hiddenItems: Set<string>): (string | object)[] { | ||||
|     return items.filter(item => { | ||||
|         if (typeof item === 'string') { | ||||
|             // Don't hide separators | ||||
|             if (item === '|') return true; | ||||
|             // Check if this item should be hidden | ||||
|             return !hiddenItems.has(item); | ||||
|         } else if (typeof item === 'object' && item !== null && 'items' in item) { | ||||
|             // Filter nested items recursively | ||||
|             const nestedItem = item as { items: (string | object)[] }; | ||||
|             const filteredNested = filterToolbarItems(nestedItem.items, hiddenItems); | ||||
|             // Only keep the group if it has at least one non-separator item | ||||
|             const hasNonSeparatorItems = filteredNested.some(subItem =>  | ||||
|                 typeof subItem === 'string' ? subItem !== '|' : true | ||||
|             ); | ||||
|             if (hasNonSeparatorItems) { | ||||
|                 return { ...item, items: filteredNested }; | ||||
|             } | ||||
|             return null; | ||||
|         } | ||||
|         return true; | ||||
|     }).filter(item => item !== null) as (string | object)[]; | ||||
| } | ||||
|  | ||||
| export function buildMobileToolbar(hiddenItems: Set<string>) { | ||||
|     const classicConfig = buildClassicToolbar(false, hiddenItems); | ||||
|     const items: string[] = []; | ||||
|  | ||||
|     for (const item of classicConfig.toolbar.items) { | ||||
|         if (typeof item === "object" && "items" in item) { | ||||
|             for (const subitem of item.items) { | ||||
|             for (const subitem of (item as any).items) { | ||||
|                 items.push(subitem); | ||||
|             } | ||||
|         } else { | ||||
|             items.push(item); | ||||
|             items.push(item as string); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -40,110 +80,115 @@ export function buildMobileToolbar() { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export function buildClassicToolbar(multilineToolbar: boolean) { | ||||
| export function buildClassicToolbar(multilineToolbar: boolean, hiddenItems: Set<string>) { | ||||
|     // For nested toolbars, refer to https://ckeditor.com/docs/ckeditor5/latest/getting-started/setup/toolbar.html#grouping-toolbar-items-in-dropdowns-nested-toolbars. | ||||
|     const items = [ | ||||
|         "heading", | ||||
|         "fontSize", | ||||
|         "|", | ||||
|         "bold", | ||||
|         "italic", | ||||
|         { | ||||
|             ...TEXT_FORMATTING_GROUP, | ||||
|             items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"] | ||||
|         }, | ||||
|         "|", | ||||
|         "fontColor", | ||||
|         "fontBackgroundColor", | ||||
|         "removeFormat", | ||||
|         "|", | ||||
|         "bulletedList", | ||||
|         "numberedList", | ||||
|         "todoList", | ||||
|         "|", | ||||
|         "blockQuote", | ||||
|         "admonition", | ||||
|         "insertTable", | ||||
|         "|", | ||||
|         "code", | ||||
|         "codeBlock", | ||||
|         "|", | ||||
|         "footnote", | ||||
|         { | ||||
|             label: "Insert", | ||||
|             icon: "plus", | ||||
|             items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] | ||||
|         }, | ||||
|         "|", | ||||
|         "alignment", | ||||
|         "outdent", | ||||
|         "indent", | ||||
|         "|", | ||||
|         "insertTemplate", | ||||
|         "markdownImport", | ||||
|         "cuttonote", | ||||
|         "findAndReplace" | ||||
|     ]; | ||||
|  | ||||
|     return { | ||||
|         toolbar: { | ||||
|             items: [ | ||||
|                 "heading", | ||||
|                 "fontSize", | ||||
|                 "|", | ||||
|                 "bold", | ||||
|                 "italic", | ||||
|                 { | ||||
|                     ...TEXT_FORMATTING_GROUP, | ||||
|                     items: ["underline", "strikethrough", "|", "superscript", "subscript", "|", "kbd"] | ||||
|                 }, | ||||
|                 "|", | ||||
|                 "fontColor", | ||||
|                 "fontBackgroundColor", | ||||
|                 "removeFormat", | ||||
|                 "|", | ||||
|                 "bulletedList", | ||||
|                 "numberedList", | ||||
|                 "todoList", | ||||
|                 "|", | ||||
|                 "blockQuote", | ||||
|                 "admonition", | ||||
|                 "insertTable", | ||||
|                 "|", | ||||
|                 "code", | ||||
|                 "codeBlock", | ||||
|                 "|", | ||||
|                 "footnote", | ||||
|                 { | ||||
|                     label: "Insert", | ||||
|                     icon: "plus", | ||||
|                     items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] | ||||
|                 }, | ||||
|                 "|", | ||||
|                 "alignment", | ||||
|                 "outdent", | ||||
|                 "indent", | ||||
|                 "|", | ||||
|                 "insertTemplate", | ||||
|                 "markdownImport", | ||||
|                 "cuttonote", | ||||
|                 "findAndReplace" | ||||
|             ], | ||||
|             items: filterToolbarItems(items, hiddenItems), | ||||
|             shouldNotGroupWhenFull: multilineToolbar | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export function buildFloatingToolbar() { | ||||
| export function buildFloatingToolbar(hiddenItems: Set<string>) { | ||||
|     const toolbarItems = [ | ||||
|         "fontSize", | ||||
|         "bold", | ||||
|         "italic", | ||||
|         "underline", | ||||
|         { | ||||
|             ...TEXT_FORMATTING_GROUP, | ||||
|             items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ] | ||||
|         }, | ||||
|         "|", | ||||
|         "fontColor", | ||||
|         "fontBackgroundColor", | ||||
|         "|", | ||||
|         "code", | ||||
|         "link", | ||||
|         "bookmark", | ||||
|         "removeFormat", | ||||
|         "internallink", | ||||
|         "cuttonote" | ||||
|     ]; | ||||
|  | ||||
|     const blockToolbarItems = [ | ||||
|         "heading", | ||||
|         "|", | ||||
|         "bulletedList", | ||||
|         "numberedList", | ||||
|         "todoList", | ||||
|         "|", | ||||
|         "blockQuote", | ||||
|         "admonition", | ||||
|         "codeBlock", | ||||
|         "insertTable", | ||||
|         "footnote", | ||||
|         { | ||||
|             label: "Insert", | ||||
|             icon: "plus", | ||||
|             items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] | ||||
|         }, | ||||
|         "|", | ||||
|         "alignment", | ||||
|         "outdent", | ||||
|         "indent", | ||||
|         "|", | ||||
|         "insertTemplate", | ||||
|         "imageUpload", | ||||
|         "markdownImport", | ||||
|         "specialCharacters", | ||||
|         "emoji", | ||||
|         "findAndReplace" | ||||
|     ]; | ||||
|  | ||||
|     return { | ||||
|         toolbar: { | ||||
|             items: [ | ||||
|                 "fontSize", | ||||
|                 "bold", | ||||
|                 "italic", | ||||
|                 "underline", | ||||
|                 { | ||||
|                     ...TEXT_FORMATTING_GROUP, | ||||
|                     items: [ "strikethrough", "|", "superscript", "subscript", "|", "kbd" ] | ||||
|                 }, | ||||
|                 "|", | ||||
|                 "fontColor", | ||||
|                 "fontBackgroundColor", | ||||
|                 "|", | ||||
|                 "code", | ||||
|                 "link", | ||||
|                 "bookmark", | ||||
|                 "removeFormat", | ||||
|                 "internallink", | ||||
|                 "cuttonote" | ||||
|             ] | ||||
|             items: filterToolbarItems(toolbarItems, hiddenItems) | ||||
|         }, | ||||
|  | ||||
|         blockToolbar: [ | ||||
|             "heading", | ||||
|             "|", | ||||
|             "bulletedList", | ||||
|             "numberedList", | ||||
|             "todoList", | ||||
|             "|", | ||||
|             "blockQuote", | ||||
|             "admonition", | ||||
|             "codeBlock", | ||||
|             "insertTable", | ||||
|             "footnote", | ||||
|             { | ||||
|                 label: "Insert", | ||||
|                 icon: "plus", | ||||
|                 items: ["link", "bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] | ||||
|             }, | ||||
|             "|", | ||||
|             "alignment", | ||||
|             "outdent", | ||||
|             "indent", | ||||
|             "|", | ||||
|             "insertTemplate", | ||||
|             "imageUpload", | ||||
|             "markdownImport", | ||||
|             "specialCharacters", | ||||
|             "emoji", | ||||
|             "findAndReplace" | ||||
|         ] | ||||
|         blockToolbar: filterToolbarItems(blockToolbarItems, hiddenItems) | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import PasswordSettings from "./options/password.jsx"; | ||||
| import ShortcutSettings from "./options/shortcuts.js"; | ||||
| import TextNoteSettings from "./options/text_notes.jsx"; | ||||
| import CodeNoteSettings from "./options/code_notes.jsx"; | ||||
| import CKEditorPluginSettings from "./options/ckeditor_plugins.jsx"; | ||||
| import OtherSettings from "./options/other.jsx"; | ||||
| import BackendLogWidget from "./content/backend_log.js"; | ||||
| import MultiFactorAuthenticationSettings from "./options/multi_factor_authentication.js"; | ||||
| @@ -45,13 +46,14 @@ const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printabl | ||||
|     <div class="note-detail-content-widget-content"></div> | ||||
| </div>`; | ||||
|  | ||||
| export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced"; | ||||
| export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsCKEditorPlugins" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced"; | ||||
|  | ||||
| const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextAwareWidget)[] | JSX.Element)> = { | ||||
|     _optionsAppearance: <AppearanceSettings />, | ||||
|     _optionsShortcuts: <ShortcutSettings />, | ||||
|     _optionsTextNotes: <TextNoteSettings />, | ||||
|     _optionsCodeNotes: <CodeNoteSettings />, | ||||
|     _optionsCKEditorPlugins: <CKEditorPluginSettings />, | ||||
|     _optionsImages: <ImageSettings />, | ||||
|     _optionsSpellcheck: <SpellcheckSettings />, | ||||
|     _optionsPassword: <PasswordSettings />, | ||||
|   | ||||
| @@ -0,0 +1,397 @@ | ||||
| import { useEffect, useState, useCallback, useMemo } from "preact/hooks"; | ||||
| import { t } from "../../../services/i18n"; | ||||
| import server from "../../../services/server"; | ||||
| import FormCheckbox from "../../react/FormCheckbox"; | ||||
| import FormGroup from "../../react/FormGroup"; | ||||
| import FormText from "../../react/FormText"; | ||||
| import OptionsSection from "./components/OptionsSection"; | ||||
| import Button from "../../react/Button"; | ||||
| import toast from "../../../services/toast"; | ||||
| import type {  | ||||
|     PluginMetadata,  | ||||
|     PluginConfiguration,  | ||||
|     PluginRegistry, | ||||
|     PluginValidationResult, | ||||
|     UpdatePluginConfigRequest, | ||||
|     UpdatePluginConfigResponse, | ||||
|     QueryPluginsResult, | ||||
|     PluginCategory | ||||
| } from "@triliumnext/commons"; | ||||
|  | ||||
| interface PluginStats { | ||||
|     enabled: number; | ||||
|     total: number; | ||||
|     core: number; | ||||
|     premium: number; | ||||
|     configurable: number; | ||||
|     categories: Record<string, number>; | ||||
|     hasPremiumLicense: boolean; | ||||
| } | ||||
|  | ||||
| const CATEGORY_DISPLAY_NAMES: Record<PluginCategory, string> = { | ||||
|     formatting: t("ckeditor_plugins.category_formatting"), | ||||
|     structure: t("ckeditor_plugins.category_structure"), | ||||
|     media: t("ckeditor_plugins.category_media"), | ||||
|     tables: t("ckeditor_plugins.category_tables"), | ||||
|     advanced: t("ckeditor_plugins.category_advanced"), | ||||
|     trilium: t("ckeditor_plugins.category_trilium"), | ||||
|     external: t("ckeditor_plugins.category_external") | ||||
| }; | ||||
|  | ||||
| export default function CKEditorPluginSettings() { | ||||
|     const [pluginRegistry, setPluginRegistry] = useState<PluginRegistry | null>(null); | ||||
|     const [userConfig, setUserConfig] = useState<PluginConfiguration[]>([]); | ||||
|     const [stats, setStats] = useState<PluginStats | null>(null); | ||||
|     const [loading, setLoading] = useState(true); | ||||
|     const [saving, setSaving] = useState(false); | ||||
|     const [validationResult, setValidationResult] = useState<PluginValidationResult | null>(null); | ||||
|     const [showValidation, setShowValidation] = useState(false); | ||||
|  | ||||
|     // Load initial data | ||||
|     useEffect(() => { | ||||
|         loadData(); | ||||
|     }, []); | ||||
|  | ||||
|     const loadData = useCallback(async () => { | ||||
|         setLoading(true); | ||||
|         try { | ||||
|             const [registry, config, statsData] = await Promise.all([ | ||||
|                 server.get<PluginRegistry>('ckeditor-plugins/registry'), | ||||
|                 server.get<PluginConfiguration[]>('ckeditor-plugins/config'), | ||||
|                 server.get<PluginStats>('ckeditor-plugins/stats') | ||||
|             ]); | ||||
|              | ||||
|             setPluginRegistry(registry); | ||||
|             setUserConfig(config); | ||||
|             setStats(statsData); | ||||
|         } catch (error) { | ||||
|             toast.showError(`${t("ckeditor_plugins.load_error")}: ${error}`); | ||||
|         } finally { | ||||
|             setLoading(false); | ||||
|         } | ||||
|     }, []); | ||||
|  | ||||
|     // Organize plugins by category | ||||
|     const pluginsByCategory = useMemo(() => { | ||||
|         if (!pluginRegistry) return {}; | ||||
|          | ||||
|         const categories: Record<PluginCategory, PluginMetadata[]> = { | ||||
|             formatting: [], | ||||
|             structure: [], | ||||
|             media: [], | ||||
|             tables: [], | ||||
|             advanced: [], | ||||
|             trilium: [], | ||||
|             external: [] | ||||
|         }; | ||||
|  | ||||
|         Object.values(pluginRegistry.plugins).forEach(plugin => { | ||||
|             if (!plugin.isCore) { // Don't show core plugins in settings | ||||
|                 categories[plugin.category].push(plugin); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Sort plugins within each category by name | ||||
|         Object.keys(categories).forEach(category => { | ||||
|             categories[category as PluginCategory].sort((a, b) => a.name.localeCompare(b.name)); | ||||
|         }); | ||||
|  | ||||
|         return categories; | ||||
|     }, [pluginRegistry]); | ||||
|  | ||||
|     // Get enabled status for a plugin | ||||
|     const isPluginEnabled = useCallback((pluginId: string): boolean => { | ||||
|         return userConfig.find(config => config.id === pluginId)?.enabled ?? false; | ||||
|     }, [userConfig]); | ||||
|  | ||||
|     // Toggle plugin enabled state | ||||
|     const togglePlugin = useCallback((pluginId: string) => { | ||||
|         setUserConfig(prev => prev.map(config =>  | ||||
|             config.id === pluginId  | ||||
|                 ? { ...config, enabled: !config.enabled } | ||||
|                 : config | ||||
|         )); | ||||
|     }, []); | ||||
|  | ||||
|     // Validate current configuration | ||||
|     const validateConfig = useCallback(async () => { | ||||
|         if (!userConfig.length) return; | ||||
|          | ||||
|         try { | ||||
|             const result = await server.post<PluginValidationResult>('ckeditor-plugins/validate', { | ||||
|                 plugins: userConfig | ||||
|             }); | ||||
|             setValidationResult(result); | ||||
|             setShowValidation(true); | ||||
|             return result; | ||||
|         } catch (error) { | ||||
|             toast.showError(`${t("ckeditor_plugins.validation_error")}: ${error}`); | ||||
|             return null; | ||||
|         } | ||||
|     }, [userConfig]); | ||||
|  | ||||
|     // Save configuration | ||||
|     const saveConfiguration = useCallback(async () => { | ||||
|         setSaving(true); | ||||
|         setShowValidation(false); | ||||
|          | ||||
|         try { | ||||
|             const request: UpdatePluginConfigRequest = { | ||||
|                 plugins: userConfig, | ||||
|                 validate: true | ||||
|             }; | ||||
|  | ||||
|             const response = await server.put<UpdatePluginConfigResponse>('ckeditor-plugins/config', request); | ||||
|              | ||||
|             if (response.success) { | ||||
|                 toast.showMessage(t("ckeditor_plugins.save_success")); | ||||
|                 await loadData(); // Reload stats | ||||
|                  | ||||
|                 // Notify user that editor reload might be needed | ||||
|                 toast.showMessage(t("ckeditor_plugins.reload_editor_notice"), { | ||||
|                     timeout: 5000 | ||||
|                 }); | ||||
|             } else { | ||||
|                 setValidationResult(response.validation); | ||||
|                 setShowValidation(true); | ||||
|                 toast.showError(`${t("ckeditor_plugins.save_error")}: ${response.errors?.join(", ")}`); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             toast.showError(`${t("ckeditor_plugins.save_error")}: ${error}`); | ||||
|         } finally { | ||||
|             setSaving(false); | ||||
|         } | ||||
|     }, [userConfig, loadData]); | ||||
|  | ||||
|     // Reset to defaults | ||||
|     const resetToDefaults = useCallback(async () => { | ||||
|         if (!confirm(t("ckeditor_plugins.reset_confirm"))) return; | ||||
|          | ||||
|         setSaving(true); | ||||
|         try { | ||||
|             const response = await server.post<UpdatePluginConfigResponse>('ckeditor-plugins/reset'); | ||||
|             if (response.success) { | ||||
|                 setUserConfig(response.plugins); | ||||
|                 toast.showMessage(t("ckeditor_plugins.reset_success")); | ||||
|                 await loadData(); | ||||
|             } else { | ||||
|                 toast.showError(`${t("ckeditor_plugins.reset_error")}: ${response.errors?.join(", ")}`); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             toast.showError(`${t("ckeditor_plugins.reset_error")}: ${error}`); | ||||
|         } finally { | ||||
|             setSaving(false); | ||||
|         } | ||||
|     }, [loadData]); | ||||
|  | ||||
|     if (loading) { | ||||
|         return ( | ||||
|             <OptionsSection title={t("ckeditor_plugins.title")}> | ||||
|                 <FormText>{t("ckeditor_plugins.loading")}</FormText> | ||||
|             </OptionsSection> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     if (!pluginRegistry || !stats) { | ||||
|         return ( | ||||
|             <OptionsSection title={t("ckeditor_plugins.title")}> | ||||
|                 <FormText>{t("ckeditor_plugins.load_failed")}</FormText> | ||||
|                 <Button text={t("ckeditor_plugins.retry")} onClick={loadData} /> | ||||
|             </OptionsSection> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <div> | ||||
|             <OptionsSection title={t("ckeditor_plugins.title")}> | ||||
|                 <FormText>{t("ckeditor_plugins.description")}</FormText> | ||||
|                  | ||||
|                 {/* Stats overview */} | ||||
|                 <div className="plugin-stats" style={{  | ||||
|                     backgroundColor: 'var(--accented-background-color)',  | ||||
|                     padding: '12px',  | ||||
|                     borderRadius: '4px', | ||||
|                     marginBottom: '20px' | ||||
|                 }}> | ||||
|                     <div className="row"> | ||||
|                         <div className="col-md-3"> | ||||
|                             <strong>{t("ckeditor_plugins.stats_enabled")}</strong><br /> | ||||
|                             <span style={{ fontSize: '1.2em', color: 'var(--main-text-color)' }}> | ||||
|                                 {stats.enabled}/{stats.configurable} | ||||
|                             </span> | ||||
|                         </div> | ||||
|                         <div className="col-md-3"> | ||||
|                             <strong>{t("ckeditor_plugins.stats_total")}</strong><br /> | ||||
|                             <span style={{ fontSize: '1.2em', color: 'var(--main-text-color)' }}> | ||||
|                                 {stats.total} | ||||
|                             </span> | ||||
|                         </div> | ||||
|                         <div className="col-md-3"> | ||||
|                             <strong>{t("ckeditor_plugins.stats_core")}</strong><br /> | ||||
|                             <span style={{ fontSize: '1.2em', color: 'var(--main-text-color)' }}> | ||||
|                                 {stats.core} | ||||
|                             </span> | ||||
|                         </div> | ||||
|                         <div className="col-md-3"> | ||||
|                             <strong>{t("ckeditor_plugins.stats_premium")}</strong><br /> | ||||
|                             <span style={{ fontSize: '1.2em', color: stats.hasPremiumLicense ? 'var(--success-color)' : 'var(--muted-text-color)' }}> | ||||
|                                 {stats.premium} {!stats.hasPremiumLicense && `(${t("ckeditor_plugins.no_license")})`} | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 {/* Validation results */} | ||||
|                 {showValidation && validationResult && ( | ||||
|                     <div className="validation-results" style={{ marginBottom: '20px' }}> | ||||
|                         {!validationResult.valid && ( | ||||
|                             <div className="alert alert-danger"> | ||||
|                                 <strong>{t("ckeditor_plugins.validation_errors")}</strong> | ||||
|                                 <ul> | ||||
|                                     {validationResult.errors.map((error, index) => ( | ||||
|                                         <li key={index}>{error.message}</li> | ||||
|                                     ))} | ||||
|                                 </ul> | ||||
|                             </div> | ||||
|                         )} | ||||
|                         {validationResult.warnings.length > 0 && ( | ||||
|                             <div className="alert alert-warning"> | ||||
|                                 <strong>{t("ckeditor_plugins.validation_warnings")}</strong> | ||||
|                                 <ul> | ||||
|                                     {validationResult.warnings.map((warning, index) => ( | ||||
|                                         <li key={index}>{warning.message}</li> | ||||
|                                     ))} | ||||
|                                 </ul> | ||||
|                             </div> | ||||
|                         )} | ||||
|                     </div> | ||||
|                 )} | ||||
|  | ||||
|                 {/* Action buttons */} | ||||
|                 <div className="plugin-actions" style={{ marginBottom: '20px' }}> | ||||
|                     <Button  | ||||
|                         text={t("ckeditor_plugins.validate")}  | ||||
|                         onClick={validateConfig} | ||||
|                         disabled={saving} | ||||
|                         className="btn-secondary" | ||||
|                         size="small" | ||||
|                     /> | ||||
|                     <Button  | ||||
|                         text={t("ckeditor_plugins.save")}  | ||||
|                         onClick={saveConfiguration} | ||||
|                         disabled={saving} | ||||
|                         className="btn-primary" | ||||
|                         size="small" | ||||
|                         style={{ marginLeft: '10px' }} | ||||
|                     /> | ||||
|                     <Button  | ||||
|                         text={t("ckeditor_plugins.reset_defaults")}  | ||||
|                         onClick={resetToDefaults} | ||||
|                         disabled={saving} | ||||
|                         className="btn-secondary" | ||||
|                         size="small" | ||||
|                         style={{ marginLeft: '10px' }} | ||||
|                     /> | ||||
|                 </div> | ||||
|             </OptionsSection> | ||||
|  | ||||
|             {/* Plugin categories */} | ||||
|             {Object.entries(pluginsByCategory).map(([categoryKey, plugins]) => { | ||||
|                 if (plugins.length === 0) return null; | ||||
|                  | ||||
|                 const category = categoryKey as PluginCategory; | ||||
|                 return ( | ||||
|                     <OptionsSection key={category} title={CATEGORY_DISPLAY_NAMES[category]} level={2}> | ||||
|                         <div className="plugin-category"> | ||||
|                             {plugins.map(plugin => ( | ||||
|                                 <PluginConfigItem  | ||||
|                                     key={plugin.id} | ||||
|                                     plugin={plugin} | ||||
|                                     enabled={isPluginEnabled(plugin.id)} | ||||
|                                     onToggle={() => togglePlugin(plugin.id)} | ||||
|                                     hasPremiumLicense={stats.hasPremiumLicense} | ||||
|                                 /> | ||||
|                             ))} | ||||
|                         </div> | ||||
|                     </OptionsSection> | ||||
|                 ); | ||||
|             })} | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| interface PluginConfigItemProps { | ||||
|     plugin: PluginMetadata; | ||||
|     enabled: boolean; | ||||
|     onToggle: () => void; | ||||
|     hasPremiumLicense: boolean; | ||||
| } | ||||
|  | ||||
| function PluginConfigItem({ plugin, enabled, onToggle, hasPremiumLicense }: PluginConfigItemProps) { | ||||
|     const canEnable = !plugin.requiresPremium || hasPremiumLicense; | ||||
|      | ||||
|     return ( | ||||
|         <FormGroup name={`plugin-${plugin.id}`} style={{ marginBottom: '15px' }}> | ||||
|             <div className="plugin-item" style={{ | ||||
|                 display: 'flex', | ||||
|                 alignItems: 'flex-start', | ||||
|                 opacity: canEnable ? 1 : 0.6 | ||||
|             }}> | ||||
|                 <FormCheckbox | ||||
|                     label="" | ||||
|                     currentValue={enabled && canEnable} | ||||
|                     onChange={canEnable ? onToggle : undefined} | ||||
|                     disabled={!canEnable} | ||||
|                     containerStyle={{ marginRight: '10px', marginTop: '2px' }} | ||||
|                 /> | ||||
|                 <div style={{ flex: 1 }}> | ||||
|                     <div style={{  | ||||
|                         fontWeight: 'bold', | ||||
|                         marginBottom: '4px', | ||||
|                         display: 'flex', | ||||
|                         alignItems: 'center', | ||||
|                         gap: '8px' | ||||
|                     }}> | ||||
|                         <span>{plugin.name}</span> | ||||
|                         {plugin.requiresPremium && ( | ||||
|                             <span className="badge badge-warning" style={{ fontSize: '0.75em' }}> | ||||
|                                 {t("ckeditor_plugins.premium")} | ||||
|                             </span> | ||||
|                         )} | ||||
|                         {plugin.dependencies.length > 0 && ( | ||||
|                             <span className="badge badge-info" style={{ fontSize: '0.75em' }}> | ||||
|                                 {t("ckeditor_plugins.has_dependencies")} | ||||
|                             </span> | ||||
|                         )} | ||||
|                     </div> | ||||
|                     <div style={{  | ||||
|                         fontSize: '0.9em',  | ||||
|                         color: 'var(--muted-text-color)', | ||||
|                         marginBottom: '4px' | ||||
|                     }}> | ||||
|                         {plugin.description} | ||||
|                     </div> | ||||
|                     {plugin.dependencies.length > 0 && ( | ||||
|                         <div style={{ fontSize: '0.8em', color: 'var(--muted-text-color)' }}> | ||||
|                             {t("ckeditor_plugins.depends_on")}: {plugin.dependencies.join(', ')} | ||||
|                         </div> | ||||
|                     )} | ||||
|                     {plugin.toolbarItems && plugin.toolbarItems.length > 0 && ( | ||||
|                         <div style={{ fontSize: '0.8em', color: 'var(--muted-text-color)' }}> | ||||
|                             {t("ckeditor_plugins.toolbar_items")}: {plugin.toolbarItems.join(', ')} | ||||
|                         </div> | ||||
|                     )} | ||||
|                     {!canEnable && ( | ||||
|                         <div style={{  | ||||
|                             fontSize: '0.8em',  | ||||
|                             color: 'var(--error-color)', | ||||
|                             fontStyle: 'italic' | ||||
|                         }}> | ||||
|                             {t("ckeditor_plugins.premium_required")} | ||||
|                         </div> | ||||
|                     )} | ||||
|                 </div> | ||||
|             </div> | ||||
|         </FormGroup> | ||||
|     ); | ||||
| } | ||||
							
								
								
									
										388
									
								
								apps/server/src/routes/api/ckeditor_plugins.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								apps/server/src/routes/api/ckeditor_plugins.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,388 @@ | ||||
| /** | ||||
|  * @module CKEditor Plugins API | ||||
|  *  | ||||
|  * This module provides REST endpoints for managing CKEditor plugin configuration | ||||
|  * in Trilium Notes. It handles plugin enablement, validation, and user preferences. | ||||
|  */ | ||||
|  | ||||
| import options from "../../services/options.js"; | ||||
| import type { Request, Response } from "express"; | ||||
| import type {  | ||||
|     PluginConfiguration,  | ||||
|     PluginRegistry,  | ||||
|     PluginValidationResult,  | ||||
|     UpdatePluginConfigRequest, | ||||
|     UpdatePluginConfigResponse, | ||||
|     QueryPluginsOptions, | ||||
|     QueryPluginsResult, | ||||
|     PluginMetadata | ||||
| } from "@triliumnext/commons"; | ||||
| import { PLUGIN_REGISTRY, getPluginMetadata, getPluginsByCategory, getConfigurablePlugins } from "@triliumnext/ckeditor5"; | ||||
| import log from "../../services/log.js"; | ||||
|  | ||||
| /** | ||||
|  * Get the complete plugin registry with metadata | ||||
|  */ | ||||
| export function getPluginRegistry(): PluginRegistry { | ||||
|     return PLUGIN_REGISTRY; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get current user's plugin configuration | ||||
|  */ | ||||
| export function getUserPluginConfig(): PluginConfiguration[] { | ||||
|     const enabledPluginsJson = options.getOptionOrNull("ckeditorEnabledPlugins"); | ||||
|      | ||||
|     if (!enabledPluginsJson) { | ||||
|         // Return default configuration if none exists | ||||
|         return getDefaultPluginConfiguration(); | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|         const enabledPlugins = JSON.parse(enabledPluginsJson) as string[]; | ||||
|          | ||||
|         // Convert to PluginConfiguration array | ||||
|         return Object.keys(PLUGIN_REGISTRY.plugins).map(pluginId => ({ | ||||
|             id: pluginId, | ||||
|             enabled: enabledPlugins.includes(pluginId) | ||||
|         })); | ||||
|     } catch (error) { | ||||
|         log.error(`Failed to parse CKEditor plugin configuration: ${error}`); | ||||
|         return getDefaultPluginConfiguration(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get default plugin configuration (all non-premium plugins enabled) | ||||
|  */ | ||||
| function getDefaultPluginConfiguration(): PluginConfiguration[] { | ||||
|     return Object.values(PLUGIN_REGISTRY.plugins).map(plugin => ({ | ||||
|         id: plugin.id, | ||||
|         enabled: plugin.defaultEnabled && !plugin.requiresPremium | ||||
|     })); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Update user's plugin configuration | ||||
|  */ | ||||
| export function updateUserPluginConfig(request: UpdatePluginConfigRequest): UpdatePluginConfigResponse { | ||||
|     const { plugins, validate = true } = request; | ||||
|      | ||||
|     try { | ||||
|         // Validate if requested | ||||
|         let validation: PluginValidationResult | undefined; | ||||
|         if (validate) { | ||||
|             validation = validatePluginConfiguration(plugins); | ||||
|             if (!validation.valid) { | ||||
|                 return { | ||||
|                     success: false, | ||||
|                     validation, | ||||
|                     plugins: [], | ||||
|                     errors: validation.errors.map(err => err.message) | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Save configuration | ||||
|         const enabledPluginIds = plugins | ||||
|             .filter(plugin => plugin.enabled) | ||||
|             .map(plugin => plugin.id); | ||||
|  | ||||
|         options.setOption("ckeditorEnabledPlugins", JSON.stringify(enabledPluginIds)); | ||||
|  | ||||
|         log.info(`Updated CKEditor plugin configuration: ${enabledPluginIds.length} plugins enabled`); | ||||
|  | ||||
|         return { | ||||
|             success: true, | ||||
|             validation, | ||||
|             plugins: plugins, | ||||
|             errors: [] | ||||
|         }; | ||||
|     } catch (error) { | ||||
|         log.error(`Failed to update CKEditor plugin configuration: ${error}`); | ||||
|         return { | ||||
|             success: false, | ||||
|             plugins: [], | ||||
|             errors: [`Failed to update configuration: ${error}`] | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Query plugins with filtering options | ||||
|  */ | ||||
| export function queryPlugins(options: QueryPluginsOptions = {}): QueryPluginsResult { | ||||
|     const { category, enabled, coreOnly, includeConfig } = options; | ||||
|      | ||||
|     let plugins = Object.values(PLUGIN_REGISTRY.plugins); | ||||
|  | ||||
|     // Apply filters | ||||
|     if (category) { | ||||
|         plugins = plugins.filter(plugin => plugin.category === category); | ||||
|     } | ||||
|  | ||||
|     if (coreOnly === true) { | ||||
|         plugins = plugins.filter(plugin => plugin.isCore); | ||||
|     } else if (coreOnly === false) { | ||||
|         plugins = plugins.filter(plugin => !plugin.isCore); | ||||
|     } | ||||
|  | ||||
|     // Get user configuration if requested or filtering by enabled status | ||||
|     let userConfig: PluginConfiguration[] = []; | ||||
|     if (includeConfig || enabled !== undefined) { | ||||
|         userConfig = getUserPluginConfig(); | ||||
|     } | ||||
|  | ||||
|     // Filter by enabled status | ||||
|     if (enabled !== undefined) { | ||||
|         const enabledPluginIds = new Set( | ||||
|             userConfig.filter(config => config.enabled).map(config => config.id) | ||||
|         ); | ||||
|         plugins = plugins.filter(plugin =>  | ||||
|             enabled ? enabledPluginIds.has(plugin.id) : !enabledPluginIds.has(plugin.id) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // Add user configuration if requested | ||||
|     const result = plugins.map(plugin => { | ||||
|         if (includeConfig) { | ||||
|             const config = userConfig.find(config => config.id === plugin.id); | ||||
|             return { | ||||
|                 ...plugin, | ||||
|                 enabled: config?.enabled ?? false, | ||||
|                 config: config?.config | ||||
|             }; | ||||
|         } | ||||
|         return plugin; | ||||
|     }); | ||||
|  | ||||
|     // Get available categories | ||||
|     const categories = [...new Set(Object.values(PLUGIN_REGISTRY.plugins).map(plugin => plugin.category))]; | ||||
|  | ||||
|     return { | ||||
|         plugins: result, | ||||
|         totalCount: Object.keys(PLUGIN_REGISTRY.plugins).length, | ||||
|         categories: categories as any[] | ||||
|     }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Validate plugin configuration for dependencies and conflicts | ||||
|  */ | ||||
| export function validatePluginConfiguration(plugins: PluginConfiguration[]): PluginValidationResult { | ||||
|     const errors: any[] = []; | ||||
|     const warnings: any[] = []; | ||||
|     const enabledPlugins = new Set(plugins.filter(p => p.enabled).map(p => p.id)); | ||||
|     const resolvedPlugins = new Set<string>(); | ||||
|  | ||||
|     // Check each enabled plugin | ||||
|     for (const plugin of plugins.filter(p => p.enabled)) { | ||||
|         const metadata = getPluginMetadata(plugin.id); | ||||
|          | ||||
|         if (!metadata) { | ||||
|             errors.push({ | ||||
|                 type: "missing_dependency", | ||||
|                 pluginId: plugin.id, | ||||
|                 message: `Plugin '${plugin.id}' not found in registry` | ||||
|             }); | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         // Check premium requirements | ||||
|         if (metadata.requiresPremium && !hasPremiumLicense()) { | ||||
|             errors.push({ | ||||
|                 type: "premium_required", | ||||
|                 pluginId: plugin.id, | ||||
|                 message: `Plugin '${metadata.name}' requires a premium CKEditor license` | ||||
|             }); | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         // Check dependencies | ||||
|         for (const depId of metadata.dependencies) { | ||||
|             if (!enabledPlugins.has(depId)) { | ||||
|                 const depMetadata = getPluginMetadata(depId); | ||||
|                 errors.push({ | ||||
|                     type: "missing_dependency", | ||||
|                     pluginId: plugin.id, | ||||
|                     message: `Plugin '${metadata.name}' requires '${depMetadata?.name || depId}' to be enabled`, | ||||
|                     details: { dependency: depId } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Check conflicts | ||||
|         for (const conflictId of metadata.conflicts) { | ||||
|             if (enabledPlugins.has(conflictId)) { | ||||
|                 const conflictMetadata = getPluginMetadata(conflictId); | ||||
|                 errors.push({ | ||||
|                     type: "plugin_conflict", | ||||
|                     pluginId: plugin.id, | ||||
|                     message: `Plugin '${metadata.name}' conflicts with '${conflictMetadata?.name || conflictId}'`, | ||||
|                     details: { conflict: conflictId } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         resolvedPlugins.add(plugin.id); | ||||
|     } | ||||
|  | ||||
|     // Add core plugins to resolved list (they're always enabled) | ||||
|     for (const plugin of Object.values(PLUGIN_REGISTRY.plugins)) { | ||||
|         if (plugin.isCore) { | ||||
|             resolvedPlugins.add(plugin.id); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Check for circular dependencies (simplified check) | ||||
|     const visited = new Set<string>(); | ||||
|     const visiting = new Set<string>(); | ||||
|  | ||||
|     function hasCircularDependency(pluginId: string): boolean { | ||||
|         if (visiting.has(pluginId)) { | ||||
|             return true; | ||||
|         } | ||||
|         if (visited.has(pluginId)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         visiting.add(pluginId); | ||||
|         const metadata = getPluginMetadata(pluginId); | ||||
|          | ||||
|         if (metadata) { | ||||
|             for (const depId of metadata.dependencies) { | ||||
|                 if (enabledPlugins.has(depId) && hasCircularDependency(depId)) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         visiting.delete(pluginId); | ||||
|         visited.add(pluginId); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     for (const pluginId of enabledPlugins) { | ||||
|         if (hasCircularDependency(pluginId)) { | ||||
|             errors.push({ | ||||
|                 type: "circular_dependency", | ||||
|                 pluginId: pluginId, | ||||
|                 message: `Circular dependency detected for plugin '${getPluginMetadata(pluginId)?.name || pluginId}'` | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         valid: errors.length === 0, | ||||
|         errors, | ||||
|         warnings, | ||||
|         resolvedPlugins: Array.from(resolvedPlugins) | ||||
|     }; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if user has premium CKEditor license | ||||
|  */ | ||||
| function hasPremiumLicense(): boolean { | ||||
|     // This would check the actual license key | ||||
|     // For now, assume no premium license | ||||
|     return process.env.VITE_CKEDITOR_KEY !== undefined && process.env.VITE_CKEDITOR_KEY !== ""; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Reset plugin configuration to defaults | ||||
|  */ | ||||
| export function resetPluginConfigToDefaults(): UpdatePluginConfigResponse { | ||||
|     const defaultConfig = getDefaultPluginConfiguration(); | ||||
|      | ||||
|     return updateUserPluginConfig({ | ||||
|         plugins: defaultConfig, | ||||
|         validate: false | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get plugin statistics | ||||
|  */ | ||||
| export function getPluginStats() { | ||||
|     const userConfig = getUserPluginConfig(); | ||||
|     const enabledCount = userConfig.filter(p => p.enabled).length; | ||||
|     const totalCount = Object.keys(PLUGIN_REGISTRY.plugins).length; | ||||
|     const coreCount = Object.values(PLUGIN_REGISTRY.plugins).filter(p => p.isCore).length; | ||||
|     const premiumCount = Object.values(PLUGIN_REGISTRY.plugins).filter(p => p.requiresPremium).length; | ||||
|  | ||||
|     const categoryCounts = Object.values(PLUGIN_REGISTRY.plugins).reduce((acc, plugin) => { | ||||
|         acc[plugin.category] = (acc[plugin.category] || 0) + 1; | ||||
|         return acc; | ||||
|     }, {} as Record<string, number>); | ||||
|  | ||||
|     return { | ||||
|         enabled: enabledCount, | ||||
|         total: totalCount, | ||||
|         core: coreCount, | ||||
|         premium: premiumCount, | ||||
|         configurable: totalCount - coreCount, | ||||
|         categories: categoryCounts, | ||||
|         hasPremiumLicense: hasPremiumLicense() | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // Express route handlers | ||||
| function getPluginRegistryHandler(req: Request, res: Response) { | ||||
|     res.json(getPluginRegistry()); | ||||
| } | ||||
|  | ||||
| function getUserPluginConfigHandler(req: Request, res: Response) { | ||||
|     res.json(getUserPluginConfig()); | ||||
| } | ||||
|  | ||||
| function updateUserPluginConfigHandler(req: Request, res: Response) { | ||||
|     const updateRequest: UpdatePluginConfigRequest = req.body; | ||||
|     const result = updateUserPluginConfig(updateRequest); | ||||
|      | ||||
|     if (!result.success) { | ||||
|         res.status(400).json(result); | ||||
|     } else { | ||||
|         res.json(result); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function queryPluginsHandler(req: Request, res: Response) { | ||||
|     const queryOptions: QueryPluginsOptions = { | ||||
|         category: req.query.category as string, | ||||
|         enabled: req.query.enabled === 'true' ? true : req.query.enabled === 'false' ? false : undefined, | ||||
|         coreOnly: req.query.coreOnly === 'true' ? true : req.query.coreOnly === 'false' ? false : undefined, | ||||
|         includeConfig: req.query.includeConfig === 'true' | ||||
|     }; | ||||
|      | ||||
|     res.json(queryPlugins(queryOptions)); | ||||
| } | ||||
|  | ||||
| function validatePluginConfigurationHandler(req: Request, res: Response) { | ||||
|     const plugins: PluginConfiguration[] = req.body.plugins || []; | ||||
|     res.json(validatePluginConfiguration(plugins)); | ||||
| } | ||||
|  | ||||
| function resetPluginConfigToDefaultsHandler(req: Request, res: Response) { | ||||
|     const result = resetPluginConfigToDefaults(); | ||||
|      | ||||
|     if (!result.success) { | ||||
|         res.status(400).json(result); | ||||
|     } else { | ||||
|         res.json(result); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function getPluginStatsHandler(req: Request, res: Response) { | ||||
|     res.json(getPluginStats()); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     getPluginRegistry: getPluginRegistryHandler, | ||||
|     getUserPluginConfig: getUserPluginConfigHandler, | ||||
|     updateUserPluginConfig: updateUserPluginConfigHandler, | ||||
|     queryPlugins: queryPluginsHandler, | ||||
|     validatePluginConfiguration: validatePluginConfigurationHandler, | ||||
|     resetPluginConfigToDefaults: resetPluginConfigToDefaultsHandler, | ||||
|     getPluginStats: getPluginStatsHandler | ||||
| }; | ||||
| @@ -97,6 +97,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([ | ||||
|     "showLoginInShareTheme", | ||||
|     "splitEditorOrientation", | ||||
|     "seenCallToActions", | ||||
|     "ckeditorEnabledPlugins", | ||||
|  | ||||
|     // AI/LLM integration options | ||||
|     "aiEnabled", | ||||
|   | ||||
| @@ -59,6 +59,7 @@ import openaiRoute from "./api/openai.js"; | ||||
| import anthropicRoute from "./api/anthropic.js"; | ||||
| import llmRoute from "./api/llm.js"; | ||||
| import systemInfoRoute from "./api/system_info.js"; | ||||
| import ckeditorPluginsRoute from "./api/ckeditor_plugins.js"; | ||||
|  | ||||
| import etapiAuthRoutes from "../etapi/auth.js"; | ||||
| import etapiAppInfoRoutes from "../etapi/app_info.js"; | ||||
| @@ -220,6 +221,15 @@ function register(app: express.Application) { | ||||
|     apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions); | ||||
|     apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes); | ||||
|     apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales); | ||||
|      | ||||
|     // CKEditor plugins management | ||||
|     apiRoute(GET, "/api/ckeditor-plugins/registry", ckeditorPluginsRoute.getPluginRegistry); | ||||
|     apiRoute(GET, "/api/ckeditor-plugins/config", ckeditorPluginsRoute.getUserPluginConfig); | ||||
|     apiRoute(PUT, "/api/ckeditor-plugins/config", ckeditorPluginsRoute.updateUserPluginConfig); | ||||
|     apiRoute(GET, "/api/ckeditor-plugins/query", ckeditorPluginsRoute.queryPlugins); | ||||
|     apiRoute(PST, "/api/ckeditor-plugins/validate", ckeditorPluginsRoute.validatePluginConfiguration); | ||||
|     apiRoute(PST, "/api/ckeditor-plugins/reset", ckeditorPluginsRoute.resetPluginConfigToDefaults); | ||||
|     apiRoute(GET, "/api/ckeditor-plugins/stats", ckeditorPluginsRoute.getPluginStats); | ||||
|  | ||||
|     apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword); | ||||
|     apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword); | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import log from "./log.js"; | ||||
| import dateUtils from "./date_utils.js"; | ||||
| import keyboardActions from "./keyboard_actions.js"; | ||||
| import { SANITIZER_DEFAULT_ALLOWED_TAGS, type KeyboardShortcutWithRequiredActionName, type OptionMap, type OptionNames } from "@triliumnext/commons"; | ||||
| import { getDefaultPluginConfiguration } from "@triliumnext/ckeditor5"; | ||||
|  | ||||
| function initDocumentOptions() { | ||||
|     optionService.createOption("documentId", randomSecureToken(16), false); | ||||
| @@ -184,6 +185,9 @@ const defaultOptions: DefaultOption[] = [ | ||||
|     { name: "textNoteEditorMultilineToolbar", value: "false", isSynced: true }, | ||||
|     { name: "textNoteEmojiCompletionEnabled", value: "true", isSynced: true }, | ||||
|     { name: "textNoteCompletionEnabled", value: "true", isSynced: true }, | ||||
|      | ||||
|     // CKEditor plugin configuration | ||||
|     { name: "ckeditorEnabledPlugins", value: getDefaultPluginConfiguration, isSynced: true }, | ||||
|  | ||||
|     // HTML import configuration | ||||
|     { name: "layoutOrientation", value: "vertical", isSynced: false }, | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins.js | ||||
| import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5"; | ||||
| import "./translation_overrides.js"; | ||||
| export { EditorWatchdog } from "ckeditor5"; | ||||
| export { PREMIUM_PLUGINS } from "./plugins.js"; | ||||
| export { PREMIUM_PLUGINS, PLUGIN_REGISTRY, getPluginMetadata, getPluginsByCategory, getConfigurablePlugins, getDefaultPluginConfiguration } from "./plugins.js"; | ||||
| export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, WatchdogConfig } from "ckeditor5"; | ||||
| export type { TemplateDefinition } from "ckeditor5-premium-features"; | ||||
| export { default as buildExtraCommands } from "./extra_slash_commands.js"; | ||||
|   | ||||
| @@ -30,6 +30,8 @@ import CodeBlockLanguageDropdown from "./plugins/code_block_language_dropdown.js | ||||
| import MoveBlockUpDownPlugin from "./plugins/move_block_updown.js"; | ||||
| import ScrollOnUndoRedoPlugin from "./plugins/scroll_on_undo_redo.js" | ||||
|  | ||||
| import type { PluginMetadata, PluginRegistry } from "@triliumnext/commons"; | ||||
|  | ||||
| /** | ||||
|  * Plugins that are specific to Trilium and not part of the CKEditor 5 core, included in both text editors but not in the attribute editor. | ||||
|  */ | ||||
| @@ -159,3 +161,613 @@ export const POPUP_EDITOR_PLUGINS: typeof Plugin[] = [ | ||||
|     ...COMMON_PLUGINS, | ||||
|     BlockToolbar, | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|  * Plugin metadata registry for CKEditor plugins in Trilium. | ||||
|  * This defines the configurable plugins with their metadata, dependencies, and categorization. | ||||
|  */ | ||||
| export const PLUGIN_REGISTRY: PluginRegistry = { | ||||
|     plugins: { | ||||
|         // Core plugins (cannot be disabled) | ||||
|         "clipboard": { | ||||
|             id: "clipboard", | ||||
|             name: "Clipboard", | ||||
|             description: "Basic clipboard operations (copy, paste, cut)", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: true, | ||||
|             commands: ["copy", "paste", "cut"] | ||||
|         }, | ||||
|         "enter": { | ||||
|             id: "enter", | ||||
|             name: "Enter Key", | ||||
|             description: "Enter key handling for line breaks and paragraphs", | ||||
|             category: "structure", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: true, | ||||
|         }, | ||||
|         "typing": { | ||||
|             id: "typing", | ||||
|             name: "Typing", | ||||
|             description: "Basic text input and keyboard handling", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: true, | ||||
|         }, | ||||
|         "undo": { | ||||
|             id: "undo", | ||||
|             name: "Undo/Redo", | ||||
|             description: "Undo and redo functionality", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: true, | ||||
|             commands: ["undo", "redo"], | ||||
|             toolbarItems: ["undo", "redo"] | ||||
|         }, | ||||
|         "paragraph": { | ||||
|             id: "paragraph", | ||||
|             name: "Paragraph", | ||||
|             description: "Basic paragraph formatting", | ||||
|             category: "structure", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: true, | ||||
|             commands: ["paragraph"] | ||||
|         }, | ||||
|  | ||||
|         // Formatting plugins | ||||
|         "bold": { | ||||
|             id: "bold", | ||||
|             name: "Bold", | ||||
|             description: "Bold text formatting", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["bold"], | ||||
|             toolbarItems: ["bold"] | ||||
|         }, | ||||
|         "italic": { | ||||
|             id: "italic", | ||||
|             name: "Italic", | ||||
|             description: "Italic text formatting", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["italic"], | ||||
|             toolbarItems: ["italic"] | ||||
|         }, | ||||
|         "underline": { | ||||
|             id: "underline", | ||||
|             name: "Underline", | ||||
|             description: "Underline text formatting", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["underline"], | ||||
|             toolbarItems: ["underline"] | ||||
|         }, | ||||
|         "strikethrough": { | ||||
|             id: "strikethrough", | ||||
|             name: "Strikethrough", | ||||
|             description: "Strikethrough text formatting", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["strikethrough"], | ||||
|             toolbarItems: ["strikethrough"] | ||||
|         }, | ||||
|         "code": { | ||||
|             id: "code", | ||||
|             name: "Inline Code", | ||||
|             description: "Inline code formatting", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["code"], | ||||
|             toolbarItems: ["code"] | ||||
|         }, | ||||
|         "subscript": { | ||||
|             id: "subscript", | ||||
|             name: "Subscript", | ||||
|             description: "Subscript text formatting", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["subscript"], | ||||
|             toolbarItems: ["subscript"] | ||||
|         }, | ||||
|         "superscript": { | ||||
|             id: "superscript", | ||||
|             name: "Superscript", | ||||
|             description: "Superscript text formatting", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["superscript"], | ||||
|             toolbarItems: ["superscript"] | ||||
|         }, | ||||
|  | ||||
|         // Structure plugins | ||||
|         "heading": { | ||||
|             id: "heading", | ||||
|             name: "Headings", | ||||
|             description: "Heading levels (H2-H6)", | ||||
|             category: "structure", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["heading"], | ||||
|             toolbarItems: ["heading"] | ||||
|         }, | ||||
|         "blockquote": { | ||||
|             id: "blockquote", | ||||
|             name: "Block Quote", | ||||
|             description: "Block quote formatting", | ||||
|             category: "structure", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["blockQuote"], | ||||
|             toolbarItems: ["blockQuote"] | ||||
|         }, | ||||
|         "list": { | ||||
|             id: "list", | ||||
|             name: "Lists", | ||||
|             description: "Bulleted and numbered lists", | ||||
|             category: "structure", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["bulletedList", "numberedList"], | ||||
|             toolbarItems: ["bulletedList", "numberedList"] | ||||
|         }, | ||||
|         "todolist": { | ||||
|             id: "todolist", | ||||
|             name: "Todo List", | ||||
|             description: "Checkable todo list items", | ||||
|             category: "structure", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: ["list"], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["todoList"], | ||||
|             toolbarItems: ["todoList"] | ||||
|         }, | ||||
|         "alignment": { | ||||
|             id: "alignment", | ||||
|             name: "Text Alignment", | ||||
|             description: "Text alignment (left, center, right, justify)", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["alignment"], | ||||
|             toolbarItems: ["alignment"] | ||||
|         }, | ||||
|         "indent": { | ||||
|             id: "indent", | ||||
|             name: "Indentation", | ||||
|             description: "Text and block indentation", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["indent", "outdent"], | ||||
|             toolbarItems: ["indent", "outdent"] | ||||
|         }, | ||||
|  | ||||
|         // Media plugins | ||||
|         "image": { | ||||
|             id: "image", | ||||
|             name: "Images", | ||||
|             description: "Image insertion, resizing and styling", | ||||
|             category: "media", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["insertImage"], | ||||
|             toolbarItems: ["insertImage"] | ||||
|         }, | ||||
|         "link": { | ||||
|             id: "link", | ||||
|             name: "Links", | ||||
|             description: "Hyperlinks and internal note links", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["link", "unlink"], | ||||
|             toolbarItems: ["link", "unlink"] | ||||
|         }, | ||||
|  | ||||
|         // Table plugins | ||||
|         "table": { | ||||
|             id: "table", | ||||
|             name: "Tables", | ||||
|             description: "Table creation and editing", | ||||
|             category: "tables", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["insertTable"], | ||||
|             toolbarItems: ["insertTable"] | ||||
|         }, | ||||
|  | ||||
|         // Advanced plugins | ||||
|         "codeblock": { | ||||
|             id: "codeblock", | ||||
|             name: "Code Blocks", | ||||
|             description: "Syntax-highlighted code blocks", | ||||
|             category: "advanced", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["codeBlock"], | ||||
|             toolbarItems: ["codeBlock"] | ||||
|         }, | ||||
|         "math": { | ||||
|             id: "math", | ||||
|             name: "Math Formulas", | ||||
|             description: "Mathematical formulas using KaTeX", | ||||
|             category: "advanced", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["math"], | ||||
|             toolbarItems: ["math"] | ||||
|         }, | ||||
|         "mermaid": { | ||||
|             id: "mermaid", | ||||
|             name: "Mermaid Diagrams", | ||||
|             description: "Diagram creation using Mermaid syntax", | ||||
|             category: "advanced", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["mermaid"], | ||||
|             toolbarItems: ["mermaid"] | ||||
|         }, | ||||
|         "admonition": { | ||||
|             id: "admonition", | ||||
|             name: "Admonitions", | ||||
|             description: "Callout boxes and admonition blocks", | ||||
|             category: "advanced", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["admonition"], | ||||
|             toolbarItems: ["admonition"] | ||||
|         }, | ||||
|         "footnotes": { | ||||
|             id: "footnotes", | ||||
|             name: "Footnotes", | ||||
|             description: "Footnote references and definitions", | ||||
|             category: "advanced", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["footnote"], | ||||
|             toolbarItems: ["footnote"] | ||||
|         }, | ||||
|         "keyboard": { | ||||
|             id: "keyboard", | ||||
|             name: "Keyboard Shortcuts", | ||||
|             description: "Visual keyboard shortcut markers", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["kbd"], | ||||
|             toolbarItems: ["kbd"] | ||||
|         }, | ||||
|         "horizontalline": { | ||||
|             id: "horizontalline", | ||||
|             name: "Horizontal Line", | ||||
|             description: "Horizontal rule/divider line", | ||||
|             category: "structure", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["horizontalLine"], | ||||
|             toolbarItems: ["horizontalLine"] | ||||
|         }, | ||||
|         "pagebreak": { | ||||
|             id: "pagebreak", | ||||
|             name: "Page Break", | ||||
|             description: "Page break for printing", | ||||
|             category: "structure", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["pageBreak"], | ||||
|             toolbarItems: ["pageBreak"] | ||||
|         }, | ||||
|         "removeformat": { | ||||
|             id: "removeformat", | ||||
|             name: "Remove Formatting", | ||||
|             description: "Remove text formatting", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["removeFormat"], | ||||
|             toolbarItems: ["removeFormat"] | ||||
|         }, | ||||
|         "findandreplace": { | ||||
|             id: "findandreplace", | ||||
|             name: "Find and Replace", | ||||
|             description: "Text search and replace functionality", | ||||
|             category: "advanced", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["findAndReplace"], | ||||
|             toolbarItems: ["findAndReplace"] | ||||
|         }, | ||||
|         "font": { | ||||
|             id: "font", | ||||
|             name: "Font Styling", | ||||
|             description: "Font family, size, color, and background color", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["fontFamily", "fontSize", "fontColor", "fontBackgroundColor"], | ||||
|             toolbarItems: ["fontFamily", "fontSize", "fontColor", "fontBackgroundColor"] | ||||
|         }, | ||||
|         "specialcharacters": { | ||||
|             id: "specialcharacters", | ||||
|             name: "Special Characters", | ||||
|             description: "Insert special characters and symbols", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["specialCharacters"], | ||||
|             toolbarItems: ["specialCharacters"] | ||||
|         }, | ||||
|         "emoji": { | ||||
|             id: "emoji", | ||||
|             name: "Emoji Support", | ||||
|             description: "Emoji insertion and autocomplete", | ||||
|             category: "formatting", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["emoji"], | ||||
|             toolbarItems: ["emoji"] | ||||
|         }, | ||||
|  | ||||
|         // Premium plugins | ||||
|         "slashcommand": { | ||||
|             id: "slashcommand", | ||||
|             name: "Slash Commands", | ||||
|             description: "Quick command insertion with / key", | ||||
|             category: "advanced", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: true, | ||||
|             isCore: false, | ||||
|             commands: ["slashCommand"] | ||||
|         }, | ||||
|         "template": { | ||||
|             id: "template", | ||||
|             name: "Templates", | ||||
|             description: "Text templates and snippets", | ||||
|             category: "advanced", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: true, | ||||
|             isCore: false, | ||||
|             commands: ["template"], | ||||
|             toolbarItems: ["template"] | ||||
|         }, | ||||
|  | ||||
|         // Trilium-specific plugins | ||||
|         "uploadimage": { | ||||
|             id: "uploadimage", | ||||
|             name: "Image Upload", | ||||
|             description: "Trilium-specific image upload handling", | ||||
|             category: "trilium", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: ["image"], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|         }, | ||||
|         "cuttonote": { | ||||
|             id: "cuttonote", | ||||
|             name: "Cut to Note", | ||||
|             description: "Cut selected text to create a new note", | ||||
|             category: "trilium", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["cutToNote"] | ||||
|         }, | ||||
|         "internallink": { | ||||
|             id: "internallink", | ||||
|             name: "Internal Links", | ||||
|             description: "Trilium-specific internal note linking", | ||||
|             category: "trilium", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: ["link"], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|         }, | ||||
|         "insertdatetime": { | ||||
|             id: "insertdatetime", | ||||
|             name: "Insert Date/Time", | ||||
|             description: "Insert current date and time", | ||||
|             category: "trilium", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["insertDateTime"] | ||||
|         }, | ||||
|         "includenote": { | ||||
|             id: "includenote", | ||||
|             name: "Include Note", | ||||
|             description: "Include content from other notes", | ||||
|             category: "trilium", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["includeNote"] | ||||
|         }, | ||||
|         "uploadfile": { | ||||
|             id: "uploadfile", | ||||
|             name: "File Upload", | ||||
|             description: "Upload and attach files", | ||||
|             category: "trilium", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["uploadFile"] | ||||
|         }, | ||||
|         "markdownimport": { | ||||
|             id: "markdownimport", | ||||
|             name: "Markdown Import", | ||||
|             description: "Import markdown content", | ||||
|             category: "trilium", | ||||
|             defaultEnabled: true, | ||||
|             dependencies: [], | ||||
|             conflicts: [], | ||||
|             requiresPremium: false, | ||||
|             isCore: false, | ||||
|             commands: ["markdownImport"] | ||||
|         } | ||||
|     }, | ||||
|     version: "1.0.0", | ||||
|     lastModified: new Date().toISOString() | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Get plugin metadata by ID | ||||
|  */ | ||||
| export function getPluginMetadata(pluginId: string): PluginMetadata | undefined { | ||||
|     return PLUGIN_REGISTRY.plugins[pluginId]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get all plugins in a category | ||||
|  */ | ||||
| export function getPluginsByCategory(category: string): PluginMetadata[] { | ||||
|     return Object.values(PLUGIN_REGISTRY.plugins).filter(plugin => plugin.category === category); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get core plugins (cannot be disabled) | ||||
|  */ | ||||
| export function getCorePlugins(): PluginMetadata[] { | ||||
|     return Object.values(PLUGIN_REGISTRY.plugins).filter(plugin => plugin.isCore); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get configurable plugins (can be enabled/disabled) | ||||
|  */ | ||||
| export function getConfigurablePlugins(): PluginMetadata[] { | ||||
|     return Object.values(PLUGIN_REGISTRY.plugins).filter(plugin => !plugin.isCore); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Get default enabled plugins configuration as JSON string | ||||
|  */ | ||||
| export function getDefaultPluginConfiguration(): string { | ||||
|     const defaultConfig: Record<string, boolean> = {}; | ||||
|     Object.values(PLUGIN_REGISTRY.plugins).forEach(plugin => { | ||||
|         if (!plugin.isCore) { | ||||
|             defaultConfig[plugin.id] = plugin.defaultEnabled; | ||||
|         } | ||||
|     }); | ||||
|     return JSON.stringify(defaultConfig); | ||||
| } | ||||
|   | ||||
| @@ -4,20 +4,23 @@ | ||||
|   "include": [], | ||||
|   "references": [ | ||||
|     { | ||||
|       "path": "../ckeditor5-footnotes" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-math" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-admonition" | ||||
|       "path": "../commons" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-mermaid" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-math" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-keyboard-marker" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-footnotes" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-admonition" | ||||
|     }, | ||||
|     { | ||||
|       "path": "./tsconfig.lib.json" | ||||
|     } | ||||
|   | ||||
| @@ -20,19 +20,22 @@ | ||||
|   ], | ||||
|   "references": [ | ||||
|     { | ||||
|       "path": "../ckeditor5-footnotes" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-math" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-admonition" | ||||
|       "path": "../commons/tsconfig.lib.json" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-mermaid" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-math" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-keyboard-marker" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-footnotes" | ||||
|     }, | ||||
|     { | ||||
|       "path": "../ckeditor5-admonition" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| export * from "./lib/i18n.js"; | ||||
| export * from "./lib/options_interface.js"; | ||||
| export * from "./lib/ckeditor_plugin_interface.js"; | ||||
| export * from "./lib/keyboard_actions_interface.js"; | ||||
| export * from "./lib/hidden_subtree.js"; | ||||
| export * from "./lib/rows.js"; | ||||
|   | ||||
							
								
								
									
										163
									
								
								packages/commons/src/lib/ckeditor_plugin_interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								packages/commons/src/lib/ckeditor_plugin_interface.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| /** | ||||
|  * @module CKEditor Plugin Interface | ||||
|  *  | ||||
|  * This module defines the TypeScript interfaces and types for managing | ||||
|  * CKEditor plugins in Trilium Notes. It provides type-safe configuration | ||||
|  * for plugin enablement, metadata, and dependency management. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Defines the categories of CKEditor plugins available in Trilium. | ||||
|  */ | ||||
| export type PluginCategory =  | ||||
|     | "formatting"    // Text formatting (bold, italic, etc.) | ||||
|     | "structure"     // Document structure (headings, lists, etc.) | ||||
|     | "media"        // Images, files, embeds | ||||
|     | "tables"       // Table-related functionality | ||||
|     | "advanced"     // Advanced features (math, mermaid, etc.) | ||||
|     | "trilium"      // Trilium-specific plugins | ||||
|     | "external";    // Third-party plugins | ||||
|  | ||||
| /** | ||||
|  * Represents the metadata for a CKEditor plugin. | ||||
|  */ | ||||
| export interface PluginMetadata { | ||||
|     /** Unique identifier for the plugin */ | ||||
|     id: string; | ||||
|     /** Human-readable display name */ | ||||
|     name: string; | ||||
|     /** Brief description of the plugin's functionality */ | ||||
|     description: string; | ||||
|     /** Category this plugin belongs to */ | ||||
|     category: PluginCategory; | ||||
|     /** Whether this plugin is enabled by default for new users */ | ||||
|     defaultEnabled: boolean; | ||||
|     /** Array of plugin IDs that this plugin depends on */ | ||||
|     dependencies: string[]; | ||||
|     /** Array of plugin IDs that conflict with this plugin */ | ||||
|     conflicts: string[]; | ||||
|     /** Whether this plugin requires a premium CKEditor license */ | ||||
|     requiresPremium: boolean; | ||||
|     /** Whether this plugin is part of the core editor functionality (cannot be disabled) */ | ||||
|     isCore: boolean; | ||||
|     /** Toolbar items/commands provided by this plugin */ | ||||
|     toolbarItems?: string[]; | ||||
|     /** Commands provided by this plugin */ | ||||
|     commands?: string[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Configuration for a user's CKEditor plugin preferences. | ||||
|  */ | ||||
| export interface PluginConfiguration { | ||||
|     /** Plugin ID */ | ||||
|     id: string; | ||||
|     /** Whether the plugin is enabled for this user */ | ||||
|     enabled: boolean; | ||||
|     /** User-specific configuration for the plugin (if any) */ | ||||
|     config?: Record<string, unknown>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * The complete registry of available CKEditor plugins. | ||||
|  */ | ||||
| export interface PluginRegistry { | ||||
|     /** Map of plugin ID to plugin metadata */ | ||||
|     plugins: Record<string, PluginMetadata>; | ||||
|     /** Version of the plugin registry (for cache invalidation) */ | ||||
|     version: string; | ||||
|     /** Last modified timestamp */ | ||||
|     lastModified: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Result of plugin dependency validation. | ||||
|  */ | ||||
| export interface PluginValidationResult { | ||||
|     /** Whether the configuration is valid */ | ||||
|     valid: boolean; | ||||
|     /** Array of validation errors */ | ||||
|     errors: PluginValidationError[]; | ||||
|     /** Array of warnings (non-blocking issues) */ | ||||
|     warnings: PluginValidationWarning[]; | ||||
|     /** Resolved list of plugins that should be enabled */ | ||||
|     resolvedPlugins: string[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Validation error for plugin configuration. | ||||
|  */ | ||||
| export interface PluginValidationError { | ||||
|     /** Type of error */ | ||||
|     type: "missing_dependency" | "circular_dependency" | "plugin_conflict" | "premium_required"; | ||||
|     /** Plugin ID that caused the error */ | ||||
|     pluginId: string; | ||||
|     /** Human-readable error message */ | ||||
|     message: string; | ||||
|     /** Additional context about the error */ | ||||
|     details?: Record<string, unknown>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Validation warning for plugin configuration. | ||||
|  */ | ||||
| export interface PluginValidationWarning { | ||||
|     /** Type of warning */ | ||||
|     type: "dependency_disabled" | "unused_dependency" | "performance_impact"; | ||||
|     /** Plugin ID that caused the warning */ | ||||
|     pluginId: string; | ||||
|     /** Human-readable warning message */ | ||||
|     message: string; | ||||
|     /** Additional context about the warning */ | ||||
|     details?: Record<string, unknown>; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Request to update plugin configuration. | ||||
|  */ | ||||
| export interface UpdatePluginConfigRequest { | ||||
|     /** Array of plugin configurations to update */ | ||||
|     plugins: PluginConfiguration[]; | ||||
|     /** Whether to validate dependencies before saving */ | ||||
|     validate?: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Response from updating plugin configuration. | ||||
|  */ | ||||
| export interface UpdatePluginConfigResponse { | ||||
|     /** Whether the update was successful */ | ||||
|     success: boolean; | ||||
|     /** Validation result (if validation was requested) */ | ||||
|     validation?: PluginValidationResult; | ||||
|     /** Updated plugin configurations */ | ||||
|     plugins: PluginConfiguration[]; | ||||
|     /** Any errors that occurred during the update */ | ||||
|     errors?: string[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Options for querying the plugin registry. | ||||
|  */ | ||||
| export interface QueryPluginsOptions { | ||||
|     /** Filter by category */ | ||||
|     category?: PluginCategory; | ||||
|     /** Filter by enabled status */ | ||||
|     enabled?: boolean; | ||||
|     /** Filter by core status */ | ||||
|     coreOnly?: boolean; | ||||
|     /** Include user configuration in results */ | ||||
|     includeConfig?: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Result of querying plugins. | ||||
|  */ | ||||
| export interface QueryPluginsResult { | ||||
|     /** Array of plugin metadata */ | ||||
|     plugins: (PluginMetadata & { enabled?: boolean; config?: Record<string, unknown> })[]; | ||||
|     /** Total count of plugins (before filtering) */ | ||||
|     totalCount: number; | ||||
|     /** Categories available in the registry */ | ||||
|     categories: PluginCategory[]; | ||||
| } | ||||
| @@ -150,6 +150,9 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi | ||||
|     codeOpenAiModel: string; | ||||
|     aiSelectedProvider: string; | ||||
|     seenCallToActions: string; | ||||
|      | ||||
|     // CKEditor plugin options | ||||
|     ckeditorEnabledPlugins: string; | ||||
| } | ||||
|  | ||||
| export type OptionNames = keyof OptionDefinitions; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user