diff --git a/.eslintignore b/.eslintignore index 24c5141a0..9c9b83ea5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -638,6 +638,8 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js packages/lib/services/interop/InteropService_Importer_Raw.js packages/lib/services/interop/InteropService_Importer_Raw.test.js +packages/lib/services/interop/Module.js +packages/lib/services/interop/Module.test.js packages/lib/services/interop/types.js packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js packages/lib/services/keychain/KeychainService.js diff --git a/.gitignore b/.gitignore index 0a2753fdf..708a69f16 100644 --- a/.gitignore +++ b/.gitignore @@ -623,6 +623,8 @@ packages/lib/services/interop/InteropService_Importer_Md_frontmatter.js packages/lib/services/interop/InteropService_Importer_Md_frontmatter.test.js packages/lib/services/interop/InteropService_Importer_Raw.js packages/lib/services/interop/InteropService_Importer_Raw.test.js +packages/lib/services/interop/Module.js +packages/lib/services/interop/Module.test.js packages/lib/services/interop/types.js packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js packages/lib/services/keychain/KeychainService.js diff --git a/packages/app-desktop/InteropServiceHelper.ts b/packages/app-desktop/InteropServiceHelper.ts index ce7361db2..31293fbe0 100644 --- a/packages/app-desktop/InteropServiceHelper.ts +++ b/packages/app-desktop/InteropServiceHelper.ts @@ -1,7 +1,8 @@ import InteropService from '@joplin/lib/services/interop/InteropService'; import CommandService from '@joplin/lib/services/CommandService'; import shim from '@joplin/lib/shim'; -import { ExportOptions, FileSystemItem, Module } from '@joplin/lib/services/interop/types'; +import { ExportOptions, FileSystemItem } from '@joplin/lib/services/interop/types'; +import { ExportModule } from '@joplin/lib/services/interop/Module'; import { _ } from '@joplin/lib/locale'; import { PluginStates } from '@joplin/lib/services/plugins/reducer'; @@ -152,7 +153,7 @@ export default class InteropServiceHelper { } // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied - public static async export(_dispatch: Function, module: Module, options: ExportNoteOptions = null) { + public static async export(_dispatch: Function, module: ExportModule, options: ExportNoteOptions = null) { if (!options) options = {}; let path = null; diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 042f02bec..7743c969b 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -9,7 +9,7 @@ import { PluginStates, utils as pluginUtils } from '@joplin/lib/services/plugins import shim from '@joplin/lib/shim'; import Setting from '@joplin/lib/models/Setting'; import versionInfo from '@joplin/lib/versionInfo'; -import { Module } from '@joplin/lib/services/interop/types'; +import { ImportModule } from '@joplin/lib/services/interop/Module'; import InteropServiceHelper from '../InteropServiceHelper'; import { _ } from '@joplin/lib/locale'; import { isContextMenuItemLocation, MenuItem, MenuItemLocation } from '@joplin/lib/services/plugins/api/types'; @@ -230,7 +230,7 @@ function useMenu(props: Props) { void CommandService.instance().execute(commandName); }, []); - const onImportModuleClick = useCallback(async (module: Module, moduleSource: string) => { + const onImportModuleClick = useCallback(async (module: ImportModule, moduleSource: string) => { let path = null; if (moduleSource === 'file') { diff --git a/packages/lib/services/interop/InteropService.test.ts b/packages/lib/services/interop/InteropService.test.ts index bde39f017..107f9e605 100644 --- a/packages/lib/services/interop/InteropService.test.ts +++ b/packages/lib/services/interop/InteropService.test.ts @@ -1,5 +1,5 @@ import InteropService from '../../services/interop/InteropService'; -import { CustomExportContext, CustomImportContext, Module, ModuleType } from '../../services/interop/types'; +import { CustomExportContext, CustomImportContext, ModuleType } from '../../services/interop/types'; import shim from '../../shim'; import { fileContentEqual, setupDatabaseAndSynchronizer, switchClient, checkThrowAsync, exportDir, supportDir } from '../../testing/test-utils'; import Folder from '../../models/Folder'; @@ -10,6 +10,9 @@ import * as fs from 'fs-extra'; import { FolderEntity, NoteEntity, ResourceEntity } from '../database/types'; import { ModelType } from '../../BaseModel'; import * as ArrayUtils from '../../ArrayUtils'; +import InteropService_Importer_Custom from './InteropService_Importer_Custom'; +import InteropService_Exporter_Custom from './InteropService_Exporter_Custom'; +import Module, { makeExportModule, makeImportModule } from './Module'; async function recreateExportDir() { const dir = exportDir(); @@ -47,35 +50,35 @@ function memoryExportModule() { resources: [], }; - const module: Module = { - type: ModuleType.Exporter, + const module: Module = makeExportModule({ description: 'Memory Export Module', format: 'memory', fileExtensions: ['memory'], - isCustom: true, + }, () => { + return new InteropService_Exporter_Custom({ + onInit: async (context: CustomExportContext) => { + result.destPath = context.destPath; + }, - onInit: async (context: CustomExportContext) => { - result.destPath = context.destPath; - }, + onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => { + result.items.push({ + type: itemType, + object: item, + }); + }, - onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => { - result.items.push({ - type: itemType, - object: item, - }); - }, + onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => { + result.resources.push({ + filePath: filePath, + object: resource, + }); + }, - onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => { - result.resources.push({ - filePath: filePath, - object: resource, - }); - }, - - onClose: async (_context: CustomExportContext) => { - // nothing - }, - }; + onClose: async (_context: CustomExportContext) => { + // nothing + }, + }); + }); return { result, module }; } @@ -555,18 +558,19 @@ describe('services_InteropService', () => { sourcePath: '', }; - const module: Module = { + const module = makeImportModule({ type: ModuleType.Importer, description: 'Test Import Module', format: 'testing', fileExtensions: ['test'], - isCustom: true, - - onExec: async (context: CustomImportContext) => { - result.hasBeenExecuted = true; - result.sourcePath = context.sourcePath; - }, - }; + }, () => { + return new InteropService_Importer_Custom({ + onExec: async (context: CustomImportContext) => { + result.hasBeenExecuted = true; + result.sourcePath = context.sourcePath; + }, + }); + }); const service = InteropService.instance(); service.registerModule(module); @@ -596,31 +600,32 @@ describe('services_InteropService', () => { closeCalled: false, }; - const module: Module = { + const module: Module = makeExportModule({ type: ModuleType.Exporter, description: 'Test Export Module', format: 'testing', fileExtensions: ['test'], - isCustom: true, + }, () => { + return new InteropService_Exporter_Custom({ + onInit: async (context: CustomExportContext) => { + result.destPath = context.destPath; + }, - onInit: async (context: CustomExportContext) => { - result.destPath = context.destPath; - }, + onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => { + result.itemTypes.push(itemType); + result.items.push(item); + }, - onProcessItem: async (_context: CustomExportContext, itemType: number, item: any) => { - result.itemTypes.push(itemType); - result.items.push(item); - }, + onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => { + result.resources.push(resource); + result.filePaths.push(filePath); + }, - onProcessResource: async (_context: CustomExportContext, resource: any, filePath: string) => { - result.resources.push(resource); - result.filePaths.push(filePath); - }, - - onClose: async (_context: CustomExportContext) => { - result.closeCalled = true; - }, - }; + onClose: async (_context: CustomExportContext) => { + result.closeCalled = true; + }, + }); + }); const service = InteropService.instance(); service.registerModule(module); diff --git a/packages/lib/services/interop/InteropService.ts b/packages/lib/services/interop/InteropService.ts index de111ef9d..7163a3f40 100644 --- a/packages/lib/services/interop/InteropService.ts +++ b/packages/lib/services/interop/InteropService.ts @@ -1,6 +1,4 @@ -import { ModuleType, FileSystemItem, ImportModuleOutputFormat, Module, ImportOptions, ExportOptions, ImportExportResult, defaultImportExportModule } from './types'; -import InteropService_Importer_Custom from './InteropService_Importer_Custom'; -import InteropService_Exporter_Custom from './InteropService_Exporter_Custom'; +import { ModuleType, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ExportOptions, ImportExportResult } from './types'; import shim from '../../shim'; import { _ } from '../../locale'; import BaseItem from '../../models/BaseItem'; @@ -10,9 +8,22 @@ import Folder from '../../models/Folder'; import NoteTag from '../../models/NoteTag'; import Note from '../../models/Note'; import * as ArrayUtils from '../../ArrayUtils'; +import InteropService_Importer_Jex from './InteropService_Importer_Jex'; +import InteropService_Importer_Md from './InteropService_Importer_Md'; +import InteropService_Importer_Md_frontmatter from './InteropService_Importer_Md_frontmatter'; +import InteropService_Importer_Raw from './InteropService_Importer_Raw'; +import InteropService_Importer_EnexToMd from './InteropService_Importer_EnexToMd'; +import InteropService_Importer_EnexToHtml from './InteropService_Importer_EnexToHtml'; +import InteropService_Exporter_Jex from './InteropService_Exporter_Jex'; +import InteropService_Exporter_Raw from './InteropService_Exporter_Raw'; +import InteropService_Exporter_Md from './InteropService_Exporter_Md'; +import InteropService_Exporter_Md_frontmatter from './InteropService_Exporter_Md_frontmatter'; +import InteropService_Exporter_Html from './InteropService_Exporter_Html'; +import InteropService_Importer_Base from './InteropService_Importer_Base'; +import InteropService_Exporter_Base from './InteropService_Exporter_Base'; +import Module, { makeExportModule, makeImportModule } from './Module'; const { sprintf } = require('sprintf-js'); const { fileExtension } = require('../../path-utils'); -const { toTitleCase } = require('../../string-utils'); const EventEmitter = require('events'); export default class InteropService { @@ -43,47 +54,46 @@ export default class InteropService { public modules() { if (!this.defaultModules_) { - const importModules: Module[] = [ - { - ...defaultImportExportModule(ModuleType.Importer), + const importModules = [ + makeImportModule({ format: 'jex', fileExtensions: ['jex'], sources: [FileSystemItem.File], description: _('Joplin Export File'), - }, - { - ...defaultImportExportModule(ModuleType.Importer), + }, () => new InteropService_Importer_Jex()), + + makeImportModule({ format: 'md', fileExtensions: ['md', 'markdown', 'txt', 'html'], sources: [FileSystemItem.File, FileSystemItem.Directory], isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format) description: _('Markdown'), - }, - { - ...defaultImportExportModule(ModuleType.Importer), + }, () => new InteropService_Importer_Md()), + + makeImportModule({ format: 'md_frontmatter', fileExtensions: ['md', 'markdown', 'txt', 'html'], sources: [FileSystemItem.File, FileSystemItem.Directory], isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format) description: _('Markdown + Front Matter'), - }, - { - ...defaultImportExportModule(ModuleType.Importer), + }, () => new InteropService_Importer_Md_frontmatter()), + + makeImportModule({ format: 'raw', sources: [FileSystemItem.Directory], description: _('Joplin Export Directory'), - }, - { - ...defaultImportExportModule(ModuleType.Importer), + }, () => new InteropService_Importer_Raw()), + + makeImportModule({ format: 'enex', fileExtensions: ['enex'], sources: [FileSystemItem.File], description: _('Evernote Export File (as Markdown)'), importerClass: 'InteropService_Importer_EnexToMd', isDefault: true, - }, - { - ...defaultImportExportModule(ModuleType.Importer), + }, () => new InteropService_Importer_EnexToMd()), + + makeImportModule({ format: 'enex', fileExtensions: ['enex'], sources: [FileSystemItem.File], @@ -91,65 +101,58 @@ export default class InteropService { // TODO: Consider doing this the same way as the multiple `md` importers are handled importerClass: 'InteropService_Importer_EnexToHtml', outputFormat: ImportModuleOutputFormat.Html, - }, + }, () => new InteropService_Importer_EnexToHtml()), ]; - const exportModules: Module[] = [ - { - ...defaultImportExportModule(ModuleType.Exporter), + const exportModules = [ + makeExportModule({ format: 'jex', fileExtensions: ['jex'], target: FileSystemItem.File, description: _('Joplin Export File'), - }, - { - ...defaultImportExportModule(ModuleType.Exporter), + }, () => new InteropService_Exporter_Jex()), + + makeExportModule({ format: 'raw', target: FileSystemItem.Directory, description: _('Joplin Export Directory'), - }, - { - ...defaultImportExportModule(ModuleType.Exporter), + }, () => new InteropService_Exporter_Raw()), + + makeExportModule({ format: 'md', target: FileSystemItem.Directory, description: _('Markdown'), - }, - { - ...defaultImportExportModule(ModuleType.Exporter), + }, () => new InteropService_Exporter_Md()), + + makeExportModule({ format: 'md_frontmatter', target: FileSystemItem.Directory, description: _('Markdown + Front Matter'), - }, - { - ...defaultImportExportModule(ModuleType.Exporter), + }, () => new InteropService_Exporter_Md_frontmatter()), + + makeExportModule({ format: 'html', fileExtensions: ['html', 'htm'], target: FileSystemItem.File, isNoteArchive: false, description: _('HTML File'), - }, - { - ...defaultImportExportModule(ModuleType.Exporter), + }, () => new InteropService_Exporter_Html()), + + makeExportModule({ format: 'html', target: FileSystemItem.Directory, description: _('HTML Directory'), - }, + }, () => new InteropService_Exporter_Html()), ]; - this.defaultModules_ = importModules.concat(exportModules); + this.defaultModules_ = (importModules as Module[]).concat(exportModules); } return this.defaultModules_.concat(this.userModules_); } public registerModule(module: Module) { - module = { - ...defaultImportExportModule(module.type), - ...module, - }; - this.userModules_.push(module); - this.eventEmitter_.emit('modulesChanged'); } @@ -166,9 +169,13 @@ export default class InteropService { if (m.format === format && m.type === type) { if (!target && !outputFormat) { matches.push(m); - } else if (target && target === m.target) { + } else if ( + m.type === ModuleType.Exporter && target && target === m.target + ) { matches.push(m); - } else if (outputFormat && outputFormat === m.outputFormat) { + } else if ( + m.type === ModuleType.Importer && outputFormat && outputFormat === m.outputFormat + ) { matches.push(m); } } @@ -180,24 +187,6 @@ export default class InteropService { return matches.length ? matches[0] : null; } - private modulePath(module: Module) { - let className = ''; - if (module.type === ModuleType.Importer) { - className = module.importerClass || `InteropService_Importer_${toTitleCase(module.format)}`; - } else { - className = `InteropService_Exporter_${toTitleCase(module.format)}`; - } - return `./${className}`; - } - - private newModuleFromCustomFactory(module: Module) { - if (module.type === ModuleType.Importer) { - return new InteropService_Importer_Custom(module); - } else { - return new InteropService_Exporter_Custom(module); - } - } - // NOTE TO FUTURE SELF: It might make sense to simply move all the existing // formatters to the `newModuleFromPath_` approach, so that there's only one way // to do this mapping. This isn't a priority right now (per the convo in: @@ -207,18 +196,7 @@ export default class InteropService { const moduleMetadata = this.findModuleByFormat_(type, format, null, outputFormat); if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and output "%s"', type, format, outputFormat)); - let output = null; - - if (moduleMetadata.isCustom) { - output = this.newModuleFromCustomFactory(moduleMetadata); - } else { - const ModuleClass = shim.requireDynamic(this.modulePath(moduleMetadata)).default; - output = new ModuleClass(); - } - - output.setMetadata(moduleMetadata); - - return output; + return moduleMetadata.factory(); } // The existing `newModuleByFormat_` fn would load by the input format. This @@ -231,19 +209,7 @@ export default class InteropService { const moduleMetadata = this.findModuleByFormat_(type, options.format, options.target); if (!moduleMetadata) throw new Error(_('Cannot load "%s" module for format "%s" and target "%s"', type, options.format, options.target)); - let output = null; - - if (moduleMetadata.isCustom) { - output = this.newModuleFromCustomFactory(moduleMetadata); - } else { - const modulePath = this.modulePath(moduleMetadata); - const ModuleClass = shim.requireDynamic(modulePath).default; - output = new ModuleClass(); - } - - output.setMetadata({ options, ...moduleMetadata }); - - return output; + return moduleMetadata.factory(options); } private moduleByFileExtension_(type: ModuleType, ext: string) { @@ -254,7 +220,7 @@ export default class InteropService { for (let i = 0; i < modules.length; i++) { const m = modules[i]; if (type !== m.type) continue; - if (m.fileExtensions && m.fileExtensions.indexOf(ext) >= 0) return m; + if (m.fileExtensions.includes(ext)) return m; } return null; @@ -273,14 +239,12 @@ export default class InteropService { if (options.format === 'auto') { const module = this.moduleByFileExtension_(ModuleType.Importer, fileExtension(options.path)); if (!module) throw new Error(_('Please specify import format for %s', options.path)); - // eslint-disable-next-line require-atomic-updates options.format = module.format; } if (options.destinationFolderId) { const folder = await Folder.load(options.destinationFolderId); if (!folder) throw new Error(_('Cannot find "%s".', options.destinationFolderId)); - // eslint-disable-next-line require-atomic-updates options.destinationFolder = folder; } @@ -288,6 +252,10 @@ export default class InteropService { const importer = this.newModuleByFormat_(ModuleType.Importer, options.format, options.outputFormat); + if (!(importer instanceof InteropService_Importer_Base)) { + throw new Error('Resolved importer is not an importer'); + } + await importer.init(options.path, options); result = await importer.exec(result); @@ -386,6 +354,10 @@ export default class InteropService { } const exporter = this.newModuleFromPath_(ModuleType.Exporter, options); + if (!(exporter instanceof InteropService_Exporter_Base)) { + throw new Error('Resolved exporter is not an exporter'); + } + await exporter.init(exportPath, options); const typeOrder = [BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE, BaseModel.TYPE_NOTE, BaseModel.TYPE_TAG, BaseModel.TYPE_NOTE_TAG]; diff --git a/packages/lib/services/interop/InteropService_Exporter_Custom.ts b/packages/lib/services/interop/InteropService_Exporter_Custom.ts index edd1c947e..cb53e0225 100644 --- a/packages/lib/services/interop/InteropService_Exporter_Custom.ts +++ b/packages/lib/services/interop/InteropService_Exporter_Custom.ts @@ -1,13 +1,20 @@ import { ExportContext } from '../plugins/api/types'; import InteropService_Exporter_Base from './InteropService_Exporter_Base'; -import { ExportOptions, Module } from './types'; +import { ExportOptions } from './types'; + +interface CustomImporter { + onInit(context: any): Promise; + onProcessItem(context: any, itemType: number, item: any): Promise; + onProcessResource(context: any, resource: any, filePath: string): Promise; + onClose(context: any): Promise; +} export default class InteropService_Exporter_Custom extends InteropService_Exporter_Base { private customContext_: ExportContext; - private module_: Module = null; + private module_: CustomImporter = null; - public constructor(module: Module) { + public constructor(module: CustomImporter) { super(); this.module_ = module; } diff --git a/packages/lib/services/interop/InteropService_Importer_Custom.ts b/packages/lib/services/interop/InteropService_Importer_Custom.ts index 4d0c5641c..6c7c46bd8 100644 --- a/packages/lib/services/interop/InteropService_Importer_Custom.ts +++ b/packages/lib/services/interop/InteropService_Importer_Custom.ts @@ -1,11 +1,17 @@ import InteropService_Importer_Base from './InteropService_Importer_Base'; -import { ImportExportResult, Module } from './types'; +import { ImportExportResult } from './types'; + +interface CustomImporter { + onExec( + context: { sourcePath: string; options: any; warnings: string[] } + ): Promise; +} export default class InteropService_Importer_Custom extends InteropService_Importer_Base { - private module_: Module = null; + private module_: CustomImporter = null; - public constructor(handler: Module) { + public constructor(handler: CustomImporter) { super(); this.module_ = handler; } @@ -23,10 +29,12 @@ export default class InteropService_Importer_Custom extends InteropService_Impor } } - return this.module_.onExec({ + await this.module_.onExec({ sourcePath: this.sourcePath_, options: processedOptions, warnings: result.warnings, }); + + return result; } } diff --git a/packages/lib/services/interop/Module.test.ts b/packages/lib/services/interop/Module.test.ts new file mode 100644 index 000000000..7b90f0b0f --- /dev/null +++ b/packages/lib/services/interop/Module.test.ts @@ -0,0 +1,53 @@ +import InteropService_Exporter_Base from './InteropService_Exporter_Base'; +import InteropService_Importer_Base from './InteropService_Importer_Base'; +import { makeExportModule, makeImportModule } from './Module'; +import { FileSystemItem } from './types'; + +describe('Module', () => { + it('should return correct default fullLabel for an ImportModule', () => { + const baseMetadata = { + format: 'Foo_test', + description: 'Some description here', + sources: [FileSystemItem.File, FileSystemItem.Directory], + }; + + const importModuleMultiSource = makeImportModule( + baseMetadata, + () => new InteropService_Importer_Base() + ); + + const importModuleSingleSource = makeImportModule({ + ...baseMetadata, + sources: [FileSystemItem.File], + }, () => new InteropService_Importer_Base()); + + // The two modules should have the same data, except for their sources. + expect(importModuleMultiSource.format).toBe('Foo_test'); + expect(importModuleSingleSource.format).toBe(importModuleMultiSource.format); + expect(importModuleMultiSource.sources).toHaveLength(2); + expect(importModuleSingleSource.sources).toHaveLength(1); + + const baseLabel = 'FOO - Some description here'; + expect(importModuleMultiSource.fullLabel()).toBe(baseLabel); + expect(importModuleSingleSource.fullLabel()).toBe(baseLabel); + + // Should only include (File) if the import module has more than one source + expect(importModuleMultiSource.fullLabel(FileSystemItem.File)).toBe(`${baseLabel} (File)`); + expect(importModuleSingleSource.fullLabel(FileSystemItem.File)).toBe(baseLabel); + }); + + it('should return correct default fullLabel for an ExportModule', () => { + const testExportModule = makeExportModule({ + format: 'format_test_______TEST', + description: 'Testing...', + }, () => new InteropService_Exporter_Base()); + + // Should only include the portion of format before the first underscore + const label = 'FORMAT - Testing...'; + expect(testExportModule.fullLabel()).toBe(label); + + // Sources should only be shown for import modules + expect(testExportModule.fullLabel(FileSystemItem.File)).toBe(label); + expect(testExportModule.fullLabel(FileSystemItem.Directory)).toBe(label); + }); +}); diff --git a/packages/lib/services/interop/Module.ts b/packages/lib/services/interop/Module.ts new file mode 100644 index 000000000..9db10c021 --- /dev/null +++ b/packages/lib/services/interop/Module.ts @@ -0,0 +1,123 @@ +import { _ } from '../../locale'; +import InteropService_Exporter_Base from './InteropService_Exporter_Base'; +import InteropService_Importer_Base from './InteropService_Importer_Base'; +import { ExportOptions, FileSystemItem, ImportModuleOutputFormat, ImportOptions, ModuleType } from './types'; + +// Metadata shared between importers and exporters. +interface BaseMetadata { + format: string; + fileExtensions: string[]; + description: string; + isDefault: boolean; + + // Returns the full label to be displayed in the UI. + fullLabel(moduleSource?: FileSystemItem): string; + + // Only applies to single file exporters or importers + // It tells whether the format can package multiple notes into one file. + // For example JEX or ENEX can, but HTML cannot. + // Default: true. + isNoteArchive: boolean; +} + +interface ImportMetadata extends BaseMetadata { + type: ModuleType.Importer; + + sources: FileSystemItem[]; + importerClass: string; + outputFormat: ImportModuleOutputFormat; +} + +export interface ImportModule extends ImportMetadata { + factory(options?: ImportOptions): InteropService_Importer_Base; +} + +interface ExportMetadata extends BaseMetadata { + type: ModuleType.Exporter; + + target: FileSystemItem; +} + +export interface ExportModule extends ExportMetadata { + factory(options?: ExportOptions): InteropService_Exporter_Base; +} + +const defaultBaseMetadata = { + format: '', + fileExtensions: [] as string[], + description: '', + isNoteArchive: true, + isDefault: false, +}; + +const moduleFullLabel = (metadata: ImportMetadata|ExportMetadata, moduleSource: FileSystemItem = null) => { + const format = metadata.format.split('_')[0]; + const label = [`${format.toUpperCase()} - ${metadata.description}`]; + if (moduleSource && metadata.type === ModuleType.Importer && metadata.sources.length > 1) { + label.push(`(${moduleSource === FileSystemItem.File ? _('File') : _('Directory')})`); + } + return label.join(' '); +}; + +export const makeImportModule = ( + metadata: Partial, factory: ()=> InteropService_Importer_Base +): ImportModule => { + const importerDefaults: ImportMetadata = { + ...defaultBaseMetadata, + type: ModuleType.Importer, + sources: [], + importerClass: '', + outputFormat: ImportModuleOutputFormat.Markdown, + + fullLabel: (moduleSource?: FileSystemItem) => { + return moduleFullLabel(fullMetadata, moduleSource); + }, + }; + + const fullMetadata = { + ...importerDefaults, + ...metadata, + }; + + return { + ...fullMetadata, + factory: (options: ImportOptions = {}) => { + const result = factory(); + result.setMetadata({ ...fullMetadata, ...(options ?? {}) }); + + return result; + }, + }; +}; + +export const makeExportModule = ( + metadata: Partial, factory: ()=> InteropService_Exporter_Base +): ExportModule => { + const exporterDefaults: ExportMetadata = { + ...defaultBaseMetadata, + type: ModuleType.Exporter, + target: FileSystemItem.File, + + fullLabel: (moduleSource?: FileSystemItem) => { + return moduleFullLabel(fullMetadata, moduleSource); + }, + }; + + const fullMetadata = { + ...exporterDefaults, + ...metadata, + }; + + return { + ...fullMetadata, + factory: (options: ExportOptions = {}) => { + const result = factory(); + result.setMetadata({ ...fullMetadata, ...(options ?? {}) }); + + return result; + }, + }; +}; + +type Module = ImportModule|ExportModule; +export default Module; diff --git a/packages/lib/services/interop/types.ts b/packages/lib/services/interop/types.ts index c5925a83a..d91ce96cd 100644 --- a/packages/lib/services/interop/types.ts +++ b/packages/lib/services/interop/types.ts @@ -1,4 +1,3 @@ -import { _ } from '../../locale'; import { PluginStates } from '../plugins/reducer'; export interface CustomImportContext { @@ -27,58 +26,6 @@ export enum ImportModuleOutputFormat { Html = 'html', } -// For historical reasons the import and export modules share the same -// interface, except that some properties are used only for import -// and others only for export. -export interface Module { - // --------------------------------------- - // Shared properties - // --------------------------------------- - - type: ModuleType; - format: string; - fileExtensions: string[]; - description: string; - // path?: string; - - // Only applies to single file exporters or importers - // It tells whether the format can package multiple notes into one file. - // For example JEX or ENEX can, but HTML cannot. - // Default: true. - isNoteArchive?: boolean; - - // A custom module is one that was not hard-coded, that was created at runtime - // by a plugin for example. If `isCustom` is `true` if it is expected that all - // the event handlers below are defined (it's enforced by the plugin API). - isCustom?: boolean; - - // --------------------------------------- - // Import-only properties - // --------------------------------------- - - sources?: FileSystemItem[]; - importerClass?: string; - outputFormat?: ImportModuleOutputFormat; - isDefault?: boolean; - // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied - fullLabel?: Function; - - // Used only if `isCustom` is true - onExec?(context: any): Promise; - - // --------------------------------------- - // Export-only properties - // --------------------------------------- - - target?: FileSystemItem; - - // Used only if `isCustom` is true - onInit?(context: any): Promise; - onProcessItem?(context: any, itemType: number, item: any): Promise; - onProcessResource?(context: any, resource: any, filePath: string): Promise; - onClose?(context: any): Promise; -} - export interface ImportOptions { path?: string; format?: string; @@ -119,29 +66,3 @@ export interface MdFrontMatterExport { 'created'?: string; 'tags'?: string[]; } - -function moduleFullLabel(moduleSource: FileSystemItem = null): string { - const format = this.format.split('_')[0]; - const label = [`${format.toUpperCase()} - ${this.description}`]; - if (moduleSource && this.sources.length > 1) { - label.push(`(${moduleSource === 'file' ? _('File') : _('Directory')})`); - } - return label.join(' '); -} - -export function defaultImportExportModule(type: ModuleType): Module { - return { - type: type, - format: '', - fileExtensions: [], - sources: [], - description: '', - isNoteArchive: true, - importerClass: '', - outputFormat: ImportModuleOutputFormat.Markdown, - isDefault: false, - fullLabel: moduleFullLabel, - isCustom: false, - target: FileSystemItem.File, - }; -} diff --git a/packages/lib/services/plugins/api/JoplinInterop.ts b/packages/lib/services/plugins/api/JoplinInterop.ts index 0f283e2e1..9076a881e 100644 --- a/packages/lib/services/plugins/api/JoplinInterop.ts +++ b/packages/lib/services/plugins/api/JoplinInterop.ts @@ -1,7 +1,10 @@ /* eslint-disable multiline-comment-style */ import InteropService from '../../interop/InteropService'; -import { Module, ModuleType } from '../../interop/types'; +import InteropService_Exporter_Custom from '../../interop/InteropService_Exporter_Custom'; +import InteropService_Importer_Custom from '../../interop/InteropService_Importer_Custom'; +import { makeExportModule, makeImportModule } from '../../interop/Module'; +import { ModuleType } from '../../interop/types'; import { ExportModule, ImportModule } from './types'; /** @@ -19,23 +22,23 @@ import { ExportModule, ImportModule } from './types'; export default class JoplinInterop { public async registerExportModule(module: ExportModule) { - const internalModule: Module = { + const internalModule = makeExportModule({ ...module, type: ModuleType.Exporter, - isCustom: true, fileExtensions: module.fileExtensions ? module.fileExtensions : [], - }; + }, () => new InteropService_Exporter_Custom(module)); return InteropService.instance().registerModule(internalModule); } public async registerImportModule(module: ImportModule) { - const internalModule: Module = { + const internalModule = makeImportModule({ ...module, type: ModuleType.Importer, - isCustom: true, fileExtensions: module.fileExtensions ? module.fileExtensions : [], - }; + }, () => { + return new InteropService_Importer_Custom(module); + }); return InteropService.instance().registerModule(internalModule); }