UNPKG

@jupyterlab/docregistry

Version:
847 lines 31.4 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { PathExt } from '@jupyterlab/coreutils'; import { nullTranslator } from '@jupyterlab/translation'; import { fileIcon, folderIcon, imageIcon, jsonIcon, juliaIcon, markdownIcon, notebookIcon, pdfIcon, pythonIcon, rKernelIcon, spreadsheetIcon, yamlIcon } from '@jupyterlab/ui-components'; import { ArrayExt, ArrayIterator, each, empty, find, map } from '@lumino/algorithm'; import { DisposableDelegate } from '@lumino/disposable'; import { Signal } from '@lumino/signaling'; import { TextModelFactory } from './default'; /** * The document registry. */ export class DocumentRegistry { /** * Construct a new document registry. */ constructor(options = {}) { this._modelFactories = Object.create(null); this._widgetFactories = Object.create(null); this._defaultWidgetFactory = ''; this._defaultWidgetFactoryOverrides = Object.create(null); this._defaultWidgetFactories = Object.create(null); this._defaultRenderedWidgetFactories = Object.create(null); this._widgetFactoriesForFileType = Object.create(null); this._fileTypes = []; this._extenders = Object.create(null); this._changed = new Signal(this); this._isDisposed = false; const factory = options.textModelFactory; this.translator = options.translator || nullTranslator; if (factory && factory.name !== 'text') { throw new Error('Text model factory must have the name `text`'); } this._modelFactories['text'] = factory || new TextModelFactory(true); const fts = options.initialFileTypes || DocumentRegistry.getDefaultFileTypes(this.translator); fts.forEach(ft => { const value = Object.assign(Object.assign({}, DocumentRegistry.getFileTypeDefaults(this.translator)), ft); this._fileTypes.push(value); }); } /** * A signal emitted when the registry has changed. */ get changed() { return this._changed; } /** * Get whether the document registry has been disposed. */ get isDisposed() { return this._isDisposed; } /** * Dispose of the resources held by the document registry. */ dispose() { if (this.isDisposed) { return; } this._isDisposed = true; for (const modelName in this._modelFactories) { this._modelFactories[modelName].dispose(); } for (const widgetName in this._widgetFactories) { this._widgetFactories[widgetName].dispose(); } for (const widgetName in this._extenders) { this._extenders[widgetName].length = 0; } this._fileTypes.length = 0; Signal.clearData(this); } /** * Add a widget factory to the registry. * * @param factory - The factory instance to register. * * @returns A disposable which will unregister the factory. * * #### Notes * If a factory with the given `'name'` is already registered, * a warning will be logged, and this will be a no-op. * If `'*'` is given as a default extension, the factory will be registered * as the global default. * If an extension or global default is already registered, this factory * will override the existing default. * The factory cannot be named an empty string or the string `'default'`. */ addWidgetFactory(factory) { const name = factory.name.toLowerCase(); if (!name || name === 'default') { throw Error('Invalid factory name'); } if (this._widgetFactories[name]) { console.warn(`Duplicate registered factory ${name}`); return new DisposableDelegate(Private.noOp); } this._widgetFactories[name] = factory; for (const ft of factory.defaultFor || []) { if (factory.fileTypes.indexOf(ft) === -1) { continue; } if (ft === '*') { this._defaultWidgetFactory = name; } else { this._defaultWidgetFactories[ft] = name; } } for (const ft of factory.defaultRendered || []) { if (factory.fileTypes.indexOf(ft) === -1) { continue; } this._defaultRenderedWidgetFactories[ft] = name; } // For convenience, store a mapping of file type name -> name for (const ft of factory.fileTypes) { if (!this._widgetFactoriesForFileType[ft]) { this._widgetFactoriesForFileType[ft] = []; } this._widgetFactoriesForFileType[ft].push(name); } this._changed.emit({ type: 'widgetFactory', name, change: 'added' }); return new DisposableDelegate(() => { delete this._widgetFactories[name]; if (this._defaultWidgetFactory === name) { this._defaultWidgetFactory = ''; } for (const ext of Object.keys(this._defaultWidgetFactories)) { if (this._defaultWidgetFactories[ext] === name) { delete this._defaultWidgetFactories[ext]; } } for (const ext of Object.keys(this._defaultRenderedWidgetFactories)) { if (this._defaultRenderedWidgetFactories[ext] === name) { delete this._defaultRenderedWidgetFactories[ext]; } } for (const ext of Object.keys(this._widgetFactoriesForFileType)) { ArrayExt.removeFirstOf(this._widgetFactoriesForFileType[ext], name); if (this._widgetFactoriesForFileType[ext].length === 0) { delete this._widgetFactoriesForFileType[ext]; } } for (const ext of Object.keys(this._defaultWidgetFactoryOverrides)) { if (this._defaultWidgetFactoryOverrides[ext] === name) { delete this._defaultWidgetFactoryOverrides[ext]; } } this._changed.emit({ type: 'widgetFactory', name, change: 'removed' }); }); } /** * Add a model factory to the registry. * * @param factory - The factory instance. * * @returns A disposable which will unregister the factory. * * #### Notes * If a factory with the given `name` is already registered, or * the given factory is already registered, a warning will be logged * and this will be a no-op. */ addModelFactory(factory) { const name = factory.name.toLowerCase(); if (this._modelFactories[name]) { console.warn(`Duplicate registered factory ${name}`); return new DisposableDelegate(Private.noOp); } this._modelFactories[name] = factory; this._changed.emit({ type: 'modelFactory', name, change: 'added' }); return new DisposableDelegate(() => { delete this._modelFactories[name]; this._changed.emit({ type: 'modelFactory', name, change: 'removed' }); }); } /** * Add a widget extension to the registry. * * @param widgetName - The name of the widget factory. * * @param extension - A widget extension. * * @returns A disposable which will unregister the extension. * * #### Notes * If the extension is already registered for the given * widget name, a warning will be logged and this will be a no-op. */ addWidgetExtension(widgetName, extension) { widgetName = widgetName.toLowerCase(); if (!(widgetName in this._extenders)) { this._extenders[widgetName] = []; } const extenders = this._extenders[widgetName]; const index = ArrayExt.firstIndexOf(extenders, extension); if (index !== -1) { console.warn(`Duplicate registered extension for ${widgetName}`); return new DisposableDelegate(Private.noOp); } this._extenders[widgetName].push(extension); this._changed.emit({ type: 'widgetExtension', name: widgetName, change: 'added' }); return new DisposableDelegate(() => { ArrayExt.removeFirstOf(this._extenders[widgetName], extension); this._changed.emit({ type: 'widgetExtension', name: widgetName, change: 'removed' }); }); } /** * Add a file type to the document registry. * * @param fileType - The file type object to register. * @param factories - Optional factories to use for the file type. * * @returns A disposable which will unregister the command. * * #### Notes * These are used to populate the "Create New" dialog. * * If no default factory exists for the file type, the first factory will * be defined as default factory. */ addFileType(fileType, factories) { const value = Object.assign(Object.assign(Object.assign({}, DocumentRegistry.getFileTypeDefaults(this.translator)), fileType), (!(fileType.icon || fileType.iconClass) && { icon: fileIcon })); this._fileTypes.push(value); // Add the filetype to the factory - filetype mapping // We do not change the factory itself if (factories) { const fileTypeName = value.name.toLowerCase(); factories .map(factory => factory.toLowerCase()) .forEach(factory => { if (!this._widgetFactoriesForFileType[fileTypeName]) { this._widgetFactoriesForFileType[fileTypeName] = []; } if (!this._widgetFactoriesForFileType[fileTypeName].includes(factory)) { this._widgetFactoriesForFileType[fileTypeName].push(factory); } }); if (!this._defaultWidgetFactories[fileTypeName]) { this._defaultWidgetFactories[fileTypeName] = this._widgetFactoriesForFileType[fileTypeName][0]; } } this._changed.emit({ type: 'fileType', name: value.name, change: 'added' }); return new DisposableDelegate(() => { ArrayExt.removeFirstOf(this._fileTypes, value); if (factories) { const fileTypeName = value.name.toLowerCase(); for (const name of factories.map(factory => factory.toLowerCase())) { ArrayExt.removeFirstOf(this._widgetFactoriesForFileType[fileTypeName], name); } if (this._defaultWidgetFactories[fileTypeName] === factories[0].toLowerCase()) { delete this._defaultWidgetFactories[fileTypeName]; } } this._changed.emit({ type: 'fileType', name: fileType.name, change: 'removed' }); }); } /** * Get a list of the preferred widget factories. * * @param path - The file path to filter the results. * * @returns A new array of widget factories. * * #### Notes * Only the widget factories whose associated model factory have * been registered will be returned. * The first item is considered the default. The returned array * has widget factories in the following order: * - path-specific default factory * - path-specific default rendered factory * - global default factory * - all other path-specific factories * - all other global factories */ preferredWidgetFactories(path) { const factories = new Set(); // Get the ordered matching file types. const fts = this.getFileTypesForPath(PathExt.basename(path)); // Start with any user overrides for the defaults. fts.forEach(ft => { if (ft.name in this._defaultWidgetFactoryOverrides) { factories.add(this._defaultWidgetFactoryOverrides[ft.name]); } }); // Next add the file type default factories. fts.forEach(ft => { if (ft.name in this._defaultWidgetFactories) { factories.add(this._defaultWidgetFactories[ft.name]); } }); // Add the file type default rendered factories. fts.forEach(ft => { if (ft.name in this._defaultRenderedWidgetFactories) { factories.add(this._defaultRenderedWidgetFactories[ft.name]); } }); // Add the global default factory. if (this._defaultWidgetFactory) { factories.add(this._defaultWidgetFactory); } // Add the file type factories in registration order. fts.forEach(ft => { if (ft.name in this._widgetFactoriesForFileType) { each(this._widgetFactoriesForFileType[ft.name], n => { factories.add(n); }); } }); // Add the rest of the global factories, in registration order. if ('*' in this._widgetFactoriesForFileType) { each(this._widgetFactoriesForFileType['*'], n => { factories.add(n); }); } // Construct the return list, checking to make sure the corresponding // model factories are registered. const factoryList = []; factories.forEach(name => { const factory = this._widgetFactories[name]; if (!factory) { return; } const modelName = factory.modelName || 'text'; if (modelName in this._modelFactories) { factoryList.push(factory); } }); return factoryList; } /** * Get the default rendered widget factory for a path. * * @param path - The path to for which to find a widget factory. * * @returns The default rendered widget factory for the path. * * ### Notes * If the widget factory has registered a separate set of `defaultRendered` * file types and there is a match in that set, this returns that. * Otherwise, this returns the same widget factory as * [[defaultWidgetFactory]]. * * The user setting `defaultViewers` took precedence on this one too. */ defaultRenderedWidgetFactory(path) { // Get the matching file types. const ftNames = this.getFileTypesForPath(PathExt.basename(path)).map(ft => ft.name); // Start with any user overrides for the defaults. for (const name in ftNames) { if (name in this._defaultWidgetFactoryOverrides) { return this._widgetFactories[this._defaultWidgetFactoryOverrides[name]]; } } // Find if a there is a default rendered factory for this type. for (const name in ftNames) { if (name in this._defaultRenderedWidgetFactories) { return this._widgetFactories[this._defaultRenderedWidgetFactories[name]]; } } // Fallback to the default widget factory return this.defaultWidgetFactory(path); } /** * Get the default widget factory for a path. * * @param path - An optional file path to filter the results. * * @returns The default widget factory for an path. * * #### Notes * This is equivalent to the first value in [[preferredWidgetFactories]]. */ defaultWidgetFactory(path) { if (!path) { return this._widgetFactories[this._defaultWidgetFactory]; } return this.preferredWidgetFactories(path)[0]; } /** * Set overrides for the default widget factory for a file type. * * Normally, a widget factory informs the document registry which file types * it should be the default for using the `defaultFor` option in the * IWidgetFactoryOptions. This function can be used to override that after * the fact. * * @param fileType: The name of the file type. * * @param factory: The name of the factory. * * #### Notes * If `factory` is undefined, then any override will be unset, and the * default factory will revert to the original value. * * If `factory` or `fileType` are not known to the docregistry, or * if `factory` cannot open files of type `fileType`, this will throw * an error. */ setDefaultWidgetFactory(fileType, factory) { fileType = fileType.toLowerCase(); if (!this.getFileType(fileType)) { throw Error(`Cannot find file type ${fileType}`); } if (!factory) { if (this._defaultWidgetFactoryOverrides[fileType]) { delete this._defaultWidgetFactoryOverrides[fileType]; } return; } if (!this.getWidgetFactory(factory)) { throw Error(`Cannot find widget factory ${factory}`); } factory = factory.toLowerCase(); const factories = this._widgetFactoriesForFileType[fileType]; if (factory !== this._defaultWidgetFactory && !(factories && factories.includes(factory))) { throw Error(`Factory ${factory} cannot view file type ${fileType}`); } this._defaultWidgetFactoryOverrides[fileType] = factory; } /** * Create an iterator over the widget factories that have been registered. * * @returns A new iterator of widget factories. */ widgetFactories() { return map(Object.keys(this._widgetFactories), name => { return this._widgetFactories[name]; }); } /** * Create an iterator over the model factories that have been registered. * * @returns A new iterator of model factories. */ modelFactories() { return map(Object.keys(this._modelFactories), name => { return this._modelFactories[name]; }); } /** * Create an iterator over the registered extensions for a given widget. * * @param widgetName - The name of the widget factory. * * @returns A new iterator over the widget extensions. */ widgetExtensions(widgetName) { widgetName = widgetName.toLowerCase(); if (!(widgetName in this._extenders)) { return empty(); } return new ArrayIterator(this._extenders[widgetName]); } /** * Create an iterator over the file types that have been registered. * * @returns A new iterator of file types. */ fileTypes() { return new ArrayIterator(this._fileTypes); } /** * Get a widget factory by name. * * @param widgetName - The name of the widget factory. * * @returns A widget factory instance. */ getWidgetFactory(widgetName) { return this._widgetFactories[widgetName.toLowerCase()]; } /** * Get a model factory by name. * * @param name - The name of the model factory. * * @returns A model factory instance. */ getModelFactory(name) { return this._modelFactories[name.toLowerCase()]; } /** * Get a file type by name. */ getFileType(name) { name = name.toLowerCase(); return find(this._fileTypes, fileType => { return fileType.name.toLowerCase() === name; }); } /** * Get a kernel preference. * * @param path - The file path. * * @param widgetName - The name of the widget factory. * * @param kernel - An optional existing kernel model. * * @returns A kernel preference. */ getKernelPreference(path, widgetName, kernel) { widgetName = widgetName.toLowerCase(); const widgetFactory = this._widgetFactories[widgetName]; if (!widgetFactory) { return void 0; } const modelFactory = this.getModelFactory(widgetFactory.modelName || 'text'); if (!modelFactory) { return void 0; } const language = modelFactory.preferredLanguage(PathExt.basename(path)); const name = kernel && kernel.name; const id = kernel && kernel.id; return { id, name, language, shouldStart: widgetFactory.preferKernel, canStart: widgetFactory.canStartKernel, shutdownOnDispose: widgetFactory.shutdownOnClose }; } /** * Get the best file type given a contents model. * * @param model - The contents model of interest. * * @returns The best matching file type. */ getFileTypeForModel(model) { switch (model.type) { case 'directory': return (find(this._fileTypes, ft => ft.contentType === 'directory') || DocumentRegistry.getDefaultDirectoryFileType(this.translator)); case 'notebook': return (find(this._fileTypes, ft => ft.contentType === 'notebook') || DocumentRegistry.getDefaultNotebookFileType(this.translator)); default: // Find the best matching extension. if (model.name || model.path) { const name = model.name || PathExt.basename(model.path); const fts = this.getFileTypesForPath(name); if (fts.length > 0) { return fts[0]; } } return (this.getFileType('text') || DocumentRegistry.getDefaultTextFileType(this.translator)); } } /** * Get the file types that match a file name. * * @param path - The path of the file. * * @returns An ordered list of matching file types. */ getFileTypesForPath(path) { const fts = []; const name = PathExt.basename(path); // Look for a pattern match first. let ft = find(this._fileTypes, ft => { return !!(ft.pattern && name.match(ft.pattern) !== null); }); if (ft) { fts.push(ft); } // Then look by extension name, starting with the longest let ext = Private.extname(name); while (ext.length > 1) { const ftSubset = this._fileTypes.filter(ft => // In Private.extname, the extension is transformed to lower case ft.extensions.map(extension => extension.toLowerCase()).includes(ext)); fts.push(...ftSubset); ext = '.' + ext.split('.').slice(2).join('.'); } return fts; } } /** * The namespace for the `DocumentRegistry` class statics. */ (function (DocumentRegistry) { /** * The defaults used for a file type. * * @param translator - The application language translator. * * @returns The default file type. */ function getFileTypeDefaults(translator) { translator = translator || nullTranslator; const trans = translator === null || translator === void 0 ? void 0 : translator.load('jupyterlab'); return { name: 'default', displayName: trans.__('default'), extensions: [], mimeTypes: [], contentType: 'file', fileFormat: 'text' }; } DocumentRegistry.getFileTypeDefaults = getFileTypeDefaults; /** * The default text file type used by the document registry. * * @param translator - The application language translator. * * @returns The default text file type. */ function getDefaultTextFileType(translator) { translator = translator || nullTranslator; const trans = translator === null || translator === void 0 ? void 0 : translator.load('jupyterlab'); const fileTypeDefaults = getFileTypeDefaults(translator); return Object.assign(Object.assign({}, fileTypeDefaults), { name: 'text', displayName: trans.__('Text'), mimeTypes: ['text/plain'], extensions: ['.txt'], icon: fileIcon }); } DocumentRegistry.getDefaultTextFileType = getDefaultTextFileType; /** * The default notebook file type used by the document registry. * * @param translator - The application language translator. * * @returns The default notebook file type. */ function getDefaultNotebookFileType(translator) { translator = translator || nullTranslator; const trans = translator === null || translator === void 0 ? void 0 : translator.load('jupyterlab'); return Object.assign(Object.assign({}, getFileTypeDefaults(translator)), { name: 'notebook', displayName: trans.__('Notebook'), mimeTypes: ['application/x-ipynb+json'], extensions: ['.ipynb'], contentType: 'notebook', fileFormat: 'json', icon: notebookIcon }); } DocumentRegistry.getDefaultNotebookFileType = getDefaultNotebookFileType; /** * The default directory file type used by the document registry. * * @param translator - The application language translator. * * @returns The default directory file type. */ function getDefaultDirectoryFileType(translator) { translator = translator || nullTranslator; const trans = translator === null || translator === void 0 ? void 0 : translator.load('jupyterlab'); return Object.assign(Object.assign({}, getFileTypeDefaults(translator)), { name: 'directory', displayName: trans.__('Directory'), extensions: [], mimeTypes: ['text/directory'], contentType: 'directory', icon: folderIcon }); } DocumentRegistry.getDefaultDirectoryFileType = getDefaultDirectoryFileType; /** * The default file types used by the document registry. * * @param translator - The application language translator. * * @returns The default directory file types. */ function getDefaultFileTypes(translator) { translator = translator || nullTranslator; const trans = translator === null || translator === void 0 ? void 0 : translator.load('jupyterlab'); return [ getDefaultTextFileType(translator), getDefaultNotebookFileType(translator), getDefaultDirectoryFileType(translator), { name: 'markdown', displayName: trans.__('Markdown File'), extensions: ['.md'], mimeTypes: ['text/markdown'], icon: markdownIcon }, { name: 'PDF', displayName: trans.__('PDF File'), extensions: ['.pdf'], mimeTypes: ['application/pdf'], icon: pdfIcon }, { name: 'python', displayName: trans.__('Python File'), extensions: ['.py'], mimeTypes: ['text/x-python'], icon: pythonIcon }, { name: 'json', displayName: trans.__('JSON File'), extensions: ['.json'], mimeTypes: ['application/json'], icon: jsonIcon }, { name: 'julia', displayName: trans.__('Julia File'), extensions: ['.jl'], mimeTypes: ['text/x-julia'], icon: juliaIcon }, { name: 'csv', displayName: trans.__('CSV File'), extensions: ['.csv'], mimeTypes: ['text/csv'], icon: spreadsheetIcon }, { name: 'tsv', displayName: trans.__('TSV File'), extensions: ['.tsv'], mimeTypes: ['text/csv'], icon: spreadsheetIcon }, { name: 'r', displayName: trans.__('R File'), mimeTypes: ['text/x-rsrc'], extensions: ['.R'], icon: rKernelIcon }, { name: 'yaml', displayName: trans.__('YAML File'), mimeTypes: ['text/x-yaml', 'text/yaml'], extensions: ['.yaml', '.yml'], icon: yamlIcon }, { name: 'svg', displayName: trans.__('Image'), mimeTypes: ['image/svg+xml'], extensions: ['.svg'], icon: imageIcon, fileFormat: 'base64' }, { name: 'tiff', displayName: trans.__('Image'), mimeTypes: ['image/tiff'], extensions: ['.tif', '.tiff'], icon: imageIcon, fileFormat: 'base64' }, { name: 'jpeg', displayName: trans.__('Image'), mimeTypes: ['image/jpeg'], extensions: ['.jpg', '.jpeg'], icon: imageIcon, fileFormat: 'base64' }, { name: 'gif', displayName: trans.__('Image'), mimeTypes: ['image/gif'], extensions: ['.gif'], icon: imageIcon, fileFormat: 'base64' }, { name: 'png', displayName: trans.__('Image'), mimeTypes: ['image/png'], extensions: ['.png'], icon: imageIcon, fileFormat: 'base64' }, { name: 'bmp', displayName: trans.__('Image'), mimeTypes: ['image/bmp'], extensions: ['.bmp'], icon: imageIcon, fileFormat: 'base64' }, { name: 'webp', displayName: trans.__('Image'), mimeTypes: ['image/webp'], extensions: ['.webp'], icon: imageIcon, fileFormat: 'base64' } ]; } DocumentRegistry.getDefaultFileTypes = getDefaultFileTypes; })(DocumentRegistry || (DocumentRegistry = {})); /** * A private namespace for DocumentRegistry data. */ var Private; (function (Private) { /** * Get the extension name of a path. * * @param file - string. * * #### Notes * Dotted filenames (e.g. `".table.json"` are allowed). */ function extname(path) { const parts = PathExt.basename(path).split('.'); parts.shift(); const ext = '.' + parts.join('.'); return ext.toLowerCase(); } Private.extname = extname; /** * A no-op function. */ function noOp() { /* no-op */ } Private.noOp = noOp; })(Private || (Private = {})); //# sourceMappingURL=registry.js.map