mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-23 23:10:17 +05:30
feat(plugins): add export-format registration hook to plugin API
Add context.formats.registerExportFormat(id, opts) to PluginContext,
backed by a new FormatRegistry (mirrors PluginRegistry's Map-based
shape). Plugins register namespaced (${pluginId}:${id}) export
formats with a label/extension/handler; the writing-studio built-in
plugin registers a trivial "sprint-summary" .txt export as a
worked example.
The plugin system lives entirely in the renderer process while the
Export menu is built in main.js, so wiring formats into the menu
required a small IPC round-trip: renderer sends format metadata to
main after plugin load (main rebuilds the menu via the already-
idempotent createMenu()), and a menu click sends the resolved save
path back to the renderer, which is the only process holding the
plugin's handler function.
Amit Haridas
This commit is contained in:
+60
@@ -419,6 +419,7 @@ let pandocVersionCache = null; // Cached parsed { major, minor } from `pandoc --
|
|||||||
let wordTemplatePath = null; // Path to selected Word template
|
let wordTemplatePath = null; // Path to selected Word template
|
||||||
let templateStartPage = 3; // Which page to start inserting content (default: page 3)
|
let templateStartPage = 3; // Which page to start inserting content (default: page 3)
|
||||||
let rendererReady = false; // Track if renderer is ready to receive file data
|
let rendererReady = false; // Track if renderer is ready to receive file data
|
||||||
|
let pluginExportFormats = []; // Export formats registered by plugins: [{ id, label, extension }]
|
||||||
|
|
||||||
// Header & Footer Settings
|
// Header & Footer Settings
|
||||||
let headerFooterSettings = {
|
let headerFooterSettings = {
|
||||||
@@ -997,6 +998,7 @@ function createMenu() {
|
|||||||
label: 'Jupyter Notebook (.ipynb)',
|
label: 'Jupyter Notebook (.ipynb)',
|
||||||
click: () => exportFile('ipynb'),
|
click: () => exportFile('ipynb'),
|
||||||
},
|
},
|
||||||
|
...buildPluginExportMenuItems(),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1895,6 +1897,42 @@ function exportFile(format) {
|
|||||||
function showExportOptionsDialog(format) {
|
function showExportOptionsDialog(format) {
|
||||||
mainWindow.webContents.send('show-export-dialog', format);
|
mainWindow.webContents.send('show-export-dialog', format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the dynamic tail of the Export submenu from plugin-registered
|
||||||
|
// formats (see plugin-export-formats-registered IPC handler below). Returns
|
||||||
|
// [] when no plugin has registered a format, so the Export menu is
|
||||||
|
// unchanged for a plain install.
|
||||||
|
function buildPluginExportMenuItems() {
|
||||||
|
if (!pluginExportFormats.length) return [];
|
||||||
|
const items = pluginExportFormats.map((fmt) => ({
|
||||||
|
label: fmt.label || fmt.id,
|
||||||
|
click: () => runPluginExportFormat(fmt),
|
||||||
|
}));
|
||||||
|
return [{ type: 'separator' }, ...items];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve an output path for a plugin-registered export format (main
|
||||||
|
// process owns save dialogs, same as every other export path in this
|
||||||
|
// file), then hand off to the renderer — the plugin's handler function
|
||||||
|
// only exists there, since that's the process that loaded the plugin.
|
||||||
|
function runPluginExportFormat(fmt) {
|
||||||
|
if (!currentFile) {
|
||||||
|
dialog.showErrorBox('Error', 'Please save the file first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ext = fmt.extension || 'txt';
|
||||||
|
const outputFile = dialog.showSaveDialogSync(mainWindow, {
|
||||||
|
defaultPath: currentFile.replace(/\.[^/.]+$/, `.${ext}`),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: fmt.label || fmt.id,
|
||||||
|
extensions: [ext],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (!outputFile) return; // User cancelled
|
||||||
|
mainWindow.webContents.send('run-plugin-export-format', { id: fmt.id, outputPath: outputFile });
|
||||||
|
}
|
||||||
function showBatchConversionDialog() {
|
function showBatchConversionDialog() {
|
||||||
mainWindow.webContents.send('show-batch-dialog');
|
mainWindow.webContents.send('show-batch-dialog');
|
||||||
}
|
}
|
||||||
@@ -4637,6 +4675,28 @@ ipcMain.on('clear-recent-files', (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Plugins (loaded in the renderer) report the export formats they've
|
||||||
|
// registered; rebuild the Export menu so they show up as entries.
|
||||||
|
// createMenu() is idempotent and already re-invoked elsewhere (e.g. after
|
||||||
|
// clearRecentFilesOnDisk() above) so calling it again here is safe.
|
||||||
|
ipcMain.on('plugin-export-formats-registered', (event, formats) => {
|
||||||
|
pluginExportFormats = Array.isArray(formats)
|
||||||
|
? formats.filter((f) => f && typeof f.id === 'string')
|
||||||
|
: [];
|
||||||
|
createMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Result of a plugin export handler running in the renderer (see
|
||||||
|
// runPluginExportFormat() / the Export menu wiring above).
|
||||||
|
ipcMain.on('plugin-export-format-result', (event, result) => {
|
||||||
|
const { outputPath, success, error } = result || {};
|
||||||
|
if (success) {
|
||||||
|
showExportSuccess(outputPath);
|
||||||
|
} else {
|
||||||
|
dialog.showErrorBox('Export Error', sanitizeErrorMessage(error || 'Unknown error'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Handle file opening on macOS
|
// Handle file opening on macOS
|
||||||
app.on('open-file', (event, filePath) => {
|
app.on('open-file', (event, filePath) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class WritingStudioPlugin extends PluginAPI {
|
|||||||
|
|
||||||
this._registerCommands(context);
|
this._registerCommands(context);
|
||||||
this._registerStatusBar(context);
|
this._registerStatusBar(context);
|
||||||
|
this._registerExportFormats(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
_registerCommands(context) {
|
_registerCommands(context) {
|
||||||
@@ -120,6 +121,42 @@ class WritingStudioPlugin extends PluginAPI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Example usage of context.formats.registerExportFormat (Task 17): adds
|
||||||
|
// a "Writing Studio Summary" entry to the Export menu that writes a
|
||||||
|
// plain-text snapshot of today's sprint/goal progress instead of going
|
||||||
|
// through Pandoc. Doubles as documentation for how a plugin can offer
|
||||||
|
// its own export target.
|
||||||
|
_registerExportFormats(context) {
|
||||||
|
const { sprintEngine, goalTracker } = this;
|
||||||
|
|
||||||
|
context.formats.registerExportFormat('sprint-summary', {
|
||||||
|
label: 'Writing Studio Summary (.txt)',
|
||||||
|
extension: 'txt',
|
||||||
|
handler: async (markdownContent, outputPath) => {
|
||||||
|
const fs = require('fs');
|
||||||
|
const goal = context.settings.get('dailyGoal') || 1000;
|
||||||
|
const progress = goalTracker.getDailyProgress(goal);
|
||||||
|
const streak = goalTracker.getStreak(goal);
|
||||||
|
const wordCount = (markdownContent || '').split(/\s+/).filter(Boolean).length;
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
'Writing Studio Summary',
|
||||||
|
'=======================',
|
||||||
|
`Generated: ${new Date().toISOString()}`,
|
||||||
|
'',
|
||||||
|
`Document word count: ${wordCount}`,
|
||||||
|
`Daily goal: ${goal}`,
|
||||||
|
`Words written today: ${progress.written} (${progress.pct}%)`,
|
||||||
|
`Current streak: ${streak} day(s)`,
|
||||||
|
`Sprint active: ${sprintEngine.isActive() ? 'yes' : 'no'}`,
|
||||||
|
'',
|
||||||
|
];
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, lines.join('\n'), 'utf-8');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
deactivate() {
|
deactivate() {
|
||||||
if (this._sprintInterval) clearInterval(this._sprintInterval);
|
if (this._sprintInterval) clearInterval(this._sprintInterval);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,14 @@
|
|||||||
"shortcut": "Ctrl+Alt+G"
|
"shortcut": "Ctrl+Alt+G"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"statusBar": { "indicators": ["sprint-timer", "word-goal"] }
|
"statusBar": { "indicators": ["sprint-timer", "word-goal"] },
|
||||||
|
"exportFormats": [
|
||||||
|
{
|
||||||
|
"id": "sprint-summary",
|
||||||
|
"label": "Writing Studio Summary (.txt)",
|
||||||
|
"extension": "txt"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"settings": [
|
"settings": [
|
||||||
{ "key": "dailyGoal", "type": "number", "default": 1000, "label": "Daily word goal" },
|
{ "key": "dailyGoal", "type": "number", "default": 1000, "label": "Daily word goal" },
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* FormatRegistry — tracks export formats registered by plugins via
|
||||||
|
* `context.formats.registerExportFormat(id, opts)`.
|
||||||
|
*
|
||||||
|
* Mirrors the simple Map-backed storage pattern used by PluginRegistry
|
||||||
|
* (see plugin-registry.js), scoped to a single concern: export format
|
||||||
|
* metadata + handler functions rather than whole plugin instances.
|
||||||
|
*/
|
||||||
|
class FormatRegistry {
|
||||||
|
constructor() {
|
||||||
|
this.formats = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register (or overwrite) an export format entry.
|
||||||
|
* @param {string} id - Fully-namespaced format id, e.g. "writing-studio:sprint-summary"
|
||||||
|
* @param {object} opts - { label, extension, handler: async (markdownContent, outputPath, options) => void }
|
||||||
|
*/
|
||||||
|
register(id, opts) {
|
||||||
|
this.formats.set(id, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a single registered format entry by its namespaced id.
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {object|undefined}
|
||||||
|
*/
|
||||||
|
get(id) {
|
||||||
|
return this.formats.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all registered formats as an array of { id, ...opts }.
|
||||||
|
*/
|
||||||
|
getAll() {
|
||||||
|
return Array.from(this.formats.entries()).map(([id, opts]) => ({ id, ...opts }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { FormatRegistry };
|
||||||
@@ -10,10 +10,21 @@ class PluginContext {
|
|||||||
* @param {object} deps.editor - { getContent, getSelection, insertAtCursor, onContentChanged }
|
* @param {object} deps.editor - { getContent, getSelection, insertAtCursor, onContentChanged }
|
||||||
* @param {object} deps.ipc - { invoke, on }
|
* @param {object} deps.ipc - { invoke, on }
|
||||||
* @param {object} deps.exportHooks - { preHooks: [], postHooks: [] }
|
* @param {object} deps.exportHooks - { preHooks: [], postHooks: [] }
|
||||||
|
* @param {object} deps.formatRegistry - FormatRegistry instance ({ register, get, getAll })
|
||||||
*/
|
*/
|
||||||
constructor(deps) {
|
constructor(deps) {
|
||||||
const { pluginId, sidebar, commands, statusBar, eventBus, settings, editor, ipc, exportHooks } =
|
const {
|
||||||
deps;
|
pluginId,
|
||||||
|
sidebar,
|
||||||
|
commands,
|
||||||
|
statusBar,
|
||||||
|
eventBus,
|
||||||
|
settings,
|
||||||
|
editor,
|
||||||
|
ipc,
|
||||||
|
exportHooks,
|
||||||
|
formatRegistry,
|
||||||
|
} = deps;
|
||||||
|
|
||||||
this.sidebar = {
|
this.sidebar = {
|
||||||
registerPanel: (id, opts) => sidebar.registerPanel(`${pluginId}:${id}`, opts),
|
registerPanel: (id, opts) => sidebar.registerPanel(`${pluginId}:${id}`, opts),
|
||||||
@@ -69,6 +80,19 @@ class PluginContext {
|
|||||||
if (exportHooks) exportHooks.postHooks.push(handler);
|
if (exportHooks) exportHooks.postHooks.push(handler);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.formats = {
|
||||||
|
/**
|
||||||
|
* Register a plugin-provided export format. It is namespaced as
|
||||||
|
* `${pluginId}:${id}` so plugins can't collide with each other or
|
||||||
|
* with the built-in Pandoc-backed formats.
|
||||||
|
* @param {string} id - Format id, unique within this plugin.
|
||||||
|
* @param {object} opts - { label, extension, handler: async (markdownContent, outputPath, options) => void }
|
||||||
|
*/
|
||||||
|
registerExportFormat: (id, opts) => {
|
||||||
|
if (formatRegistry) formatRegistry.register(`${pluginId}:${id}`, opts);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class PluginRegistry {
|
|||||||
editor: this.deps.editor,
|
editor: this.deps.editor,
|
||||||
ipc: this.deps.ipc,
|
ipc: this.deps.ipc,
|
||||||
exportHooks: this.exportHooks,
|
exportHooks: this.exportHooks,
|
||||||
|
formatRegistry: this.deps.formatRegistry,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1817,8 +1817,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
const { PluginRegistry } = require('./plugins/plugin-registry');
|
const { PluginRegistry } = require('./plugins/plugin-registry');
|
||||||
const { EventBus } = require('./plugins/event-bus');
|
const { EventBus } = require('./plugins/event-bus');
|
||||||
const { SettingsStore } = require('./plugins/settings-store');
|
const { SettingsStore } = require('./plugins/settings-store');
|
||||||
|
const { FormatRegistry } = require('./plugins/format-registry');
|
||||||
const pluginPath = require('path');
|
const pluginPath = require('path');
|
||||||
const pluginEventBus = new EventBus();
|
const pluginEventBus = new EventBus();
|
||||||
|
const pluginFormatRegistry = new FormatRegistry();
|
||||||
const pluginSettings = new SettingsStore({
|
const pluginSettings = new SettingsStore({
|
||||||
get: (key) => window.electronAPI.invoke('plugin-settings:get', key),
|
get: (key) => window.electronAPI.invoke('plugin-settings:get', key),
|
||||||
set: (key, value) =>
|
set: (key, value) =>
|
||||||
@@ -1863,12 +1865,51 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
invoke: (ch, data) => window.electronAPI.invoke(ch, data),
|
invoke: (ch, data) => window.electronAPI.invoke(ch, data),
|
||||||
on: (ch, cb) => window.electronAPI.on(ch, cb),
|
on: (ch, cb) => window.electronAPI.on(ch, cb),
|
||||||
},
|
},
|
||||||
|
formatRegistry: pluginFormatRegistry,
|
||||||
});
|
});
|
||||||
const builtInDir = pluginPath.join(__dirname, 'plugins', 'built-in');
|
const builtInDir = pluginPath.join(__dirname, 'plugins', 'built-in');
|
||||||
const loader = new PluginLoader([builtInDir]);
|
const loader = new PluginLoader([builtInDir]);
|
||||||
const discovered = loader.discoverPlugins();
|
const discovered = loader.discoverPlugins();
|
||||||
discovered.forEach((p) => pluginRegistry.register(p));
|
discovered.forEach((p) => pluginRegistry.register(p));
|
||||||
|
|
||||||
|
// Tell the main process which export formats plugins registered, so the
|
||||||
|
// Export menu (built in main.js, a separate process from this one) can
|
||||||
|
// grow entries for them. Only serializable metadata crosses the IPC
|
||||||
|
// boundary — the handler functions stay here in the renderer, since
|
||||||
|
// that's the only process that actually holds them.
|
||||||
|
ipcRenderer.send(
|
||||||
|
'plugin-export-formats-registered',
|
||||||
|
pluginFormatRegistry.getAll().map(({ id, label, extension }) => ({ id, label, extension }))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Main process resolved a save path (via the Export menu) for a
|
||||||
|
// plugin-registered format; run the plugin's handler with the live
|
||||||
|
// editor content and report back so main can show success/error UI.
|
||||||
|
ipcRenderer.on('run-plugin-export-format', async (event, { id, outputPath } = {}) => {
|
||||||
|
const entry = pluginFormatRegistry.get(id);
|
||||||
|
if (!entry || typeof entry.handler !== 'function') {
|
||||||
|
ipcRenderer.send('plugin-export-format-result', {
|
||||||
|
id,
|
||||||
|
outputPath,
|
||||||
|
success: false,
|
||||||
|
error: `Export format "${id}" is not registered.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const content = tabManager.getCurrentContent();
|
||||||
|
await entry.handler(content, outputPath, {});
|
||||||
|
ipcRenderer.send('plugin-export-format-result', { id, outputPath, success: true });
|
||||||
|
} catch (err) {
|
||||||
|
ipcRenderer.send('plugin-export-format-result', {
|
||||||
|
id,
|
||||||
|
outputPath,
|
||||||
|
success: false,
|
||||||
|
error: err && err.message ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize Zen Mode
|
// Initialize Zen Mode
|
||||||
const ZenModeClass = getZenMode();
|
const ZenModeClass = getZenMode();
|
||||||
const zenMode = new ZenModeClass(tabManager);
|
const zenMode = new ZenModeClass(tabManager);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const { FormatRegistry } = require('../src/plugins/format-registry');
|
||||||
|
|
||||||
|
describe('FormatRegistry', () => {
|
||||||
|
let registry;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
registry = new FormatRegistry();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('register — stores an entry retrievable by id', () => {
|
||||||
|
const handler = jest.fn();
|
||||||
|
registry.register('plugin-a:fmt', { label: 'Format A', extension: 'txt', handler });
|
||||||
|
const entry = registry.get('plugin-a:fmt');
|
||||||
|
expect(entry).toEqual({ label: 'Format A', extension: 'txt', handler });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get — returns undefined for unknown id', () => {
|
||||||
|
expect(registry.get('nope')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('register — overwrites an existing id', () => {
|
||||||
|
registry.register('plugin-a:fmt', { label: 'First' });
|
||||||
|
registry.register('plugin-a:fmt', { label: 'Second' });
|
||||||
|
expect(registry.get('plugin-a:fmt').label).toBe('Second');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAll — returns all entries with id merged in', () => {
|
||||||
|
registry.register('plugin-a:fmt', { label: 'Format A', extension: 'txt' });
|
||||||
|
registry.register('plugin-b:fmt', { label: 'Format B', extension: 'csv' });
|
||||||
|
const all = registry.getAll();
|
||||||
|
expect(all).toHaveLength(2);
|
||||||
|
expect(all).toContainEqual({ id: 'plugin-a:fmt', label: 'Format A', extension: 'txt' });
|
||||||
|
expect(all).toContainEqual({ id: 'plugin-b:fmt', label: 'Format B', extension: 'csv' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAll — returns empty array when nothing registered', () => {
|
||||||
|
expect(registry.getAll()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ describe('PluginContext', () => {
|
|||||||
},
|
},
|
||||||
ipc: { invoke: jest.fn(), on: jest.fn() },
|
ipc: { invoke: jest.fn(), on: jest.fn() },
|
||||||
exportHooks: { preHooks: [], postHooks: [] },
|
exportHooks: { preHooks: [], postHooks: [] },
|
||||||
|
formatRegistry: { register: jest.fn(), get: jest.fn(), getAll: jest.fn() },
|
||||||
};
|
};
|
||||||
context = new PluginContext(mockDeps);
|
context = new PluginContext(mockDeps);
|
||||||
});
|
});
|
||||||
@@ -87,4 +88,18 @@ describe('PluginContext', () => {
|
|||||||
context.exports.registerPostHook(handler);
|
context.exports.registerPostHook(handler);
|
||||||
expect(mockDeps.exportHooks.postHooks).toContain(handler);
|
expect(mockDeps.exportHooks.postHooks).toContain(handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('exposes formats.registerExportFormat with namespaced id', () => {
|
||||||
|
const handler = jest.fn();
|
||||||
|
const opts = { label: 'My Format', extension: 'txt', handler };
|
||||||
|
context.formats.registerExportFormat('my-format', opts);
|
||||||
|
expect(mockDeps.formatRegistry.register).toHaveBeenCalledWith('test-plugin:my-format', opts);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats.registerExportFormat is a no-op when no formatRegistry is injected', () => {
|
||||||
|
const noRegistryContext = new PluginContext({ ...mockDeps, formatRegistry: undefined });
|
||||||
|
expect(() =>
|
||||||
|
noRegistryContext.formats.registerExportFormat('x', { handler: jest.fn() })
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { PluginRegistry } = require('../src/plugins/plugin-registry');
|
const { PluginRegistry } = require('../src/plugins/plugin-registry');
|
||||||
const { PluginAPI } = require('../src/plugins/plugin-api');
|
const { PluginAPI } = require('../src/plugins/plugin-api');
|
||||||
const { EventBus } = require('../src/plugins/event-bus');
|
const { EventBus } = require('../src/plugins/event-bus');
|
||||||
|
const { FormatRegistry } = require('../src/plugins/format-registry');
|
||||||
|
|
||||||
class TestPlugin extends PluginAPI {
|
class TestPlugin extends PluginAPI {
|
||||||
init(context) {
|
init(context) {
|
||||||
@@ -153,4 +154,40 @@ describe('PluginRegistry', () => {
|
|||||||
registry.getPlugin('test').instance.ctx.exports.registerPreHook(handler);
|
registry.getPlugin('test').instance.ctx.exports.registerPreHook(handler);
|
||||||
expect(registry.exportHooks.preHooks).toContain(handler);
|
expect(registry.exportHooks.preHooks).toContain(handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a plugin calling context.formats.registerExportFormat populates the injected FormatRegistry with a namespaced entry', () => {
|
||||||
|
const formatRegistry = new FormatRegistry();
|
||||||
|
const registryWithFormats = new PluginRegistry({ ...mockDeps, formatRegistry });
|
||||||
|
|
||||||
|
class ExportingPlugin extends PluginAPI {
|
||||||
|
init(context) {
|
||||||
|
context.formats.registerExportFormat('sprint-summary', {
|
||||||
|
label: 'Writing Studio Summary (.txt)',
|
||||||
|
extension: 'txt',
|
||||||
|
handler: async () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registryWithFormats.register({
|
||||||
|
id: 'writing-studio',
|
||||||
|
name: 'Writing Studio',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'desc',
|
||||||
|
manifest: {},
|
||||||
|
PluginClass: ExportingPlugin,
|
||||||
|
dir: '/tmp/test',
|
||||||
|
});
|
||||||
|
|
||||||
|
const entry = formatRegistry.get('writing-studio:sprint-summary');
|
||||||
|
expect(entry).toBeDefined();
|
||||||
|
expect(entry.label).toBe('Writing Studio Summary (.txt)');
|
||||||
|
expect(entry.extension).toBe('txt');
|
||||||
|
expect(typeof entry.handler).toBe('function');
|
||||||
|
|
||||||
|
const all = formatRegistry.getAll();
|
||||||
|
expect(all).toContainEqual(
|
||||||
|
expect.objectContaining({ id: 'writing-studio:sprint-summary', extension: 'txt' })
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user