UNPKG

@jupyterlab/settingregistry

Version:
1,209 lines 45.6 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { CommandRegistry } from '@lumino/commands'; import { JSONExt } from '@lumino/coreutils'; import { DisposableDelegate } from '@lumino/disposable'; import { Signal } from '@lumino/signaling'; import Ajv from 'ajv'; import * as json5 from 'json5'; import SCHEMA from './plugin-schema.json'; /** * An alias for the JSON deep copy function. */ const copy = JSONExt.deepCopy; /** Default arguments for Ajv instances. * * https://ajv.js.org/options.html */ const AJV_DEFAULT_OPTIONS = { /** * @todo the implications of enabling strict mode are beyond the scope of * the initial PR */ strict: false }; /** * The ASCII record separator character. */ const RECORD_SEPARATOR = String.fromCharCode(30); /** * The default implementation of a schema validator. */ export class DefaultSchemaValidator { /** * Instantiate a schema validator. */ constructor() { this._composer = new Ajv({ useDefaults: true, ...AJV_DEFAULT_OPTIONS }); this._validator = new Ajv({ ...AJV_DEFAULT_OPTIONS }); this._composer.addSchema(SCHEMA, 'jupyterlab-plugin-schema'); this._validator.addSchema(SCHEMA, 'jupyterlab-plugin-schema'); } /** * Validate a plugin's schema and user data; populate the `composite` data. * * @param plugin - The plugin being validated. Its `composite` data will be * populated by reference. * * @param populate - Whether plugin data should be populated, defaults to * `true`. * * @returns A list of errors if either the schema or data fail to validate or * `null` if there are no errors. */ validateData(plugin, populate = true) { const validate = this._validator.getSchema(plugin.id); const compose = this._composer.getSchema(plugin.id); // If the schemas do not exist, add them to the validator and continue. if (!validate || !compose) { if (plugin.schema.type !== 'object') { const keyword = 'schema'; const message = `Setting registry schemas' root-level type must be ` + `'object', rejecting type: ${plugin.schema.type}`; return [{ instancePath: 'type', keyword, schemaPath: '', message }]; } const errors = this._addSchema(plugin.id, plugin.schema); return errors || this.validateData(plugin); } // Parse the raw commented JSON into a user map. let user; try { user = json5.parse(plugin.raw); } catch (error) { if (error instanceof SyntaxError) { return [ { instancePath: '', keyword: 'syntax', schemaPath: '', message: error.message } ]; } const { column, description } = error; const line = error.lineNumber; return [ { instancePath: '', keyword: 'parse', schemaPath: '', message: `${description} (line ${line} column ${column})` } ]; } if (!validate(user)) { return validate.errors; } // Copy the user data before merging defaults into composite map. const composite = copy(user); if (!compose(composite)) { return compose.errors; } if (populate) { plugin.data = { composite, user }; } return null; } /** * Add a schema to the validator. * * @param plugin - The plugin ID. * * @param schema - The schema being added. * * @returns A list of errors if the schema fails to validate or `null` if there * are no errors. * * #### Notes * It is safe to call this function multiple times with the same plugin name. */ _addSchema(plugin, schema) { const composer = this._composer; const validator = this._validator; const validate = validator.getSchema('jupyterlab-plugin-schema'); // Validate against the main schema. if (!validate(schema)) { return validate.errors; } // Validate against the JSON schema meta-schema. if (!validator.validateSchema(schema)) { return validator.errors; } // Remove if schema already exists. composer.removeSchema(plugin); validator.removeSchema(plugin); // Add schema to the validator and composer. composer.addSchema(schema, plugin); validator.addSchema(schema, plugin); return null; } } /** * The default concrete implementation of a setting registry. */ export class SettingRegistry { /** * Create a new setting registry. */ constructor(options) { /** * The schema of the setting registry. */ this.schema = SCHEMA; /** * The collection of setting registry plugins. */ this.plugins = Object.create(null); this._pluginChanged = new Signal(this); this._ready = Promise.resolve(); this._transformers = Object.create(null); this._unloadedPlugins = new Map(); this.connector = options.connector; this.validator = options.validator || new DefaultSchemaValidator(); // Plugins with transformation may not be loaded if the transformation function is // not yet available. To avoid fetching again the associated data when the transformation // function is available, the plugin data is kept in cache. if (options.plugins) { options.plugins .filter(plugin => plugin.schema['jupyter.lab.transform']) .forEach(plugin => this._unloadedPlugins.set(plugin.id, plugin)); // Preload with any available data at instantiation-time. this._ready = this._preload(options.plugins); } } /** * A signal that emits the name of a plugin when its settings change. */ get pluginChanged() { return this._pluginChanged; } /** * Get an individual setting. * * @param plugin - The name of the plugin whose settings are being retrieved. * * @param key - The name of the setting being retrieved. * * @returns A promise that resolves when the setting is retrieved. */ async get(plugin, key) { // Wait for data preload before allowing normal operation. await this._ready; const plugins = this.plugins; if (plugin in plugins) { const { composite, user } = plugins[plugin].data; return { composite: composite[key] !== undefined ? copy(composite[key]) : undefined, user: user[key] !== undefined ? copy(user[key]) : undefined }; } return this.load(plugin).then(() => this.get(plugin, key)); } /** * Load a plugin's settings into the setting registry. * * @param plugin - The name of the plugin whose settings are being loaded. * * @param forceTransform - An optional parameter to force replay the transforms methods. * * @returns A promise that resolves with a plugin settings object or rejects * if the plugin is not found. */ async load(plugin, forceTransform = false) { // Wait for data preload before allowing normal operation. await this._ready; const plugins = this.plugins; const registry = this; // eslint-disable-line // If the plugin exists, resolve. if (plugin in plugins) { // Force replaying the transform function if expected. if (forceTransform) { // Empty the composite and user data before replaying the transforms. plugins[plugin].data = { composite: {}, user: {} }; await this._load(await this._transform('fetch', plugins[plugin])); this._pluginChanged.emit(plugin); } return new Settings({ plugin: plugins[plugin], registry }); } // If the plugin is not loaded but has already been fetched. if (this._unloadedPlugins.has(plugin) && plugin in this._transformers) { await this._load(await this._transform('fetch', this._unloadedPlugins.get(plugin))); if (plugin in plugins) { this._pluginChanged.emit(plugin); this._unloadedPlugins.delete(plugin); return new Settings({ plugin: plugins[plugin], registry }); } } // If the plugin needs to be loaded from the data connector, fetch. return this.reload(plugin); } /** * Reload a plugin's settings into the registry even if they already exist. * * @param plugin - The name of the plugin whose settings are being reloaded. * * @returns A promise that resolves with a plugin settings object or rejects * with a list of `ISchemaValidator.IError` objects if it fails. */ async reload(plugin) { // Wait for data preload before allowing normal operation. await this._ready; const fetched = await this.connector.fetch(plugin); const plugins = this.plugins; // eslint-disable-line const registry = this; // eslint-disable-line if (fetched === undefined) { throw [ { instancePath: '', keyword: 'id', message: `Could not fetch settings for ${plugin}.`, schemaPath: '' } ]; } await this._load(await this._transform('fetch', fetched)); this._pluginChanged.emit(plugin); return new Settings({ plugin: plugins[plugin], registry }); } /** * Remove a single setting in the registry. * * @param plugin - The name of the plugin whose setting is being removed. * * @param key - The name of the setting being removed. * * @returns A promise that resolves when the setting is removed. */ async remove(plugin, key) { // Wait for data preload before allowing normal operation. await this._ready; const plugins = this.plugins; if (!(plugin in plugins)) { return; } const raw = json5.parse(plugins[plugin].raw); // Delete both the value and any associated comment. delete raw[key]; delete raw[`// ${key}`]; plugins[plugin].raw = Private.annotatedPlugin(plugins[plugin], raw); return this._save(plugin); } /** * Set a single setting in the registry. * * @param plugin - The name of the plugin whose setting is being set. * * @param key - The name of the setting being set. * * @param value - The value of the setting being set. * * @returns A promise that resolves when the setting has been saved. * */ async set(plugin, key, value) { // Wait for data preload before allowing normal operation. await this._ready; const plugins = this.plugins; if (!(plugin in plugins)) { return this.load(plugin).then(() => this.set(plugin, key, value)); } // Parse the raw JSON string removing all comments and return an object. const raw = json5.parse(plugins[plugin].raw); plugins[plugin].raw = Private.annotatedPlugin(plugins[plugin], { ...raw, [key]: value }); return this._save(plugin); } /** * Register a plugin transform function to act on a specific plugin. * * @param plugin - The name of the plugin whose settings are transformed. * * @param transforms - The transform functions applied to the plugin. * * @returns A disposable that removes the transforms from the registry. * * #### Notes * - `compose` transformations: The registry automatically overwrites a * plugin's default values with user overrides, but a plugin may instead wish * to merge values. This behavior can be accomplished in a `compose` * transformation. * - `fetch` transformations: The registry uses the plugin data that is * fetched from its connector. If a plugin wants to override, e.g. to update * its schema with dynamic defaults, a `fetch` transformation can be applied. */ transform(plugin, transforms) { const transformers = this._transformers; if (plugin in transformers) { const error = new Error(`${plugin} already has a transformer.`); error.name = 'TransformError'; throw error; } transformers[plugin] = { fetch: transforms.fetch || (plugin => plugin), compose: transforms.compose || (plugin => plugin) }; return new DisposableDelegate(() => { delete transformers[plugin]; }); } /** * Upload a plugin's settings. * * @param plugin - The name of the plugin whose settings are being set. * * @param raw - The raw plugin settings being uploaded. * * @returns A promise that resolves when the settings have been saved. */ async upload(plugin, raw) { // Wait for data preload before allowing normal operation. await this._ready; const plugins = this.plugins; if (!(plugin in plugins)) { return this.load(plugin).then(() => this.upload(plugin, raw)); } // Set the local copy. plugins[plugin].raw = raw; return this._save(plugin); } /** * A promise which resolves when the pre-fetched plugins passed to the registry finished pre-loading. */ get ready() { return this._ready; } /** * Load a plugin into the registry. */ async _load(data) { const plugin = data.id; // Validate and preload the item. try { await this._validate(data); } catch (errors) { const output = [`Validating ${plugin} failed:`]; errors.forEach((error, index) => { const { instancePath, schemaPath, keyword, message } = error; if (instancePath || schemaPath) { output.push(`${index} - schema @ ${schemaPath}, data @ ${instancePath}`); } output.push(`{${keyword}} ${message}`); }); console.warn(output.join('\n')); throw errors; } } /** * Preload a list of plugins and fail gracefully. */ async _preload(plugins) { await Promise.all(plugins.map(async (plugin) => { var _a; try { // Apply a transformation to the plugin if necessary. await this._load(await this._transform('fetch', plugin)); } catch (errors) { /* Ignore silently if no transformers. */ if (((_a = errors[0]) === null || _a === void 0 ? void 0 : _a.keyword) !== 'unset') { console.warn('Ignored setting registry preload errors.', errors); } } })); } /** * Save a plugin in the registry. */ async _save(plugin) { const plugins = this.plugins; if (!(plugin in plugins)) { throw new Error(`${plugin} does not exist in setting registry.`); } try { await this._validate(plugins[plugin]); } catch (errors) { console.warn(`${plugin} validation errors:`, errors); throw new Error(`${plugin} failed to validate; check console.`); } await this.connector.save(plugin, plugins[plugin].raw); // Fetch and reload the data to guarantee server and client are in sync. const fetched = await this.connector.fetch(plugin); if (fetched === undefined) { throw [ { instancePath: '', keyword: 'id', message: `Could not fetch settings for ${plugin}.`, schemaPath: '' } ]; } await this._load(await this._transform('fetch', fetched)); this._pluginChanged.emit(plugin); } /** * Transform the plugin if necessary. */ async _transform(phase, plugin) { const id = plugin.id; const transformers = this._transformers; if (!plugin.schema['jupyter.lab.transform']) { return plugin; } if (id in transformers) { const transformed = transformers[id][phase].call(null, plugin); if (transformed.id !== id) { throw [ { instancePath: '', keyword: 'id', message: 'Plugin transformations cannot change plugin IDs.', schemaPath: '' } ]; } return transformed; } // If the plugin has no transformers, throw an error and bail. throw [ { instancePath: '', keyword: 'unset', message: `${plugin.id} has no transformers yet.`, schemaPath: '' } ]; } /** * Validate and preload a plugin, compose the `composite` data. */ async _validate(plugin) { // Validate the user data and create the composite data. const errors = this.validator.validateData(plugin); if (errors) { throw errors; } // Apply a transformation if necessary and set the local copy. this.plugins[plugin.id] = await this._transform('compose', plugin); } } /** * Base settings specified by a JSON schema. */ export class BaseSettings { constructor(options) { this._schema = options.schema; } /** * The plugin's schema. */ get schema() { return this._schema; } /** * Checks if any fields are different from the default value. */ isDefault(user) { for (const key in this.schema.properties) { const value = user[key]; const defaultValue = this.default(key); if (value === undefined || defaultValue === undefined || JSONExt.deepEqual(value, JSONExt.emptyObject) || JSONExt.deepEqual(value, JSONExt.emptyArray)) { continue; } if (!JSONExt.deepEqual(value, defaultValue)) { return false; } } return true; } /** * Calculate the default value of a setting by iterating through the schema. * * @param key - The name of the setting whose default value is calculated. * * @returns A calculated default JSON value for a specific setting. */ default(key) { return Private.reifyDefault(this.schema, key); } } /** * A manager for a specific plugin's settings. */ export class Settings extends BaseSettings { /** * Instantiate a new plugin settings manager. */ constructor(options) { super({ schema: options.plugin.schema }); this._changed = new Signal(this); this._isDisposed = false; this.id = options.plugin.id; this.registry = options.registry; this.registry.pluginChanged.connect(this._onPluginChanged, this); } /** * A signal that emits when the plugin's settings have changed. */ get changed() { return this._changed; } /** * The composite of user settings and extension defaults. */ get composite() { return this.plugin.data.composite; } /** * Test whether the plugin settings manager disposed. */ get isDisposed() { return this._isDisposed; } get plugin() { return this.registry.plugins[this.id]; } /** * The plugin settings raw text value. */ get raw() { return this.plugin.raw; } /** * Whether the settings have been modified by the user or not. */ get isModified() { return !this.isDefault(this.user); } /** * The user settings. */ get user() { return this.plugin.data.user; } /** * The published version of the NPM package containing these settings. */ get version() { return this.plugin.version; } /** * Return the defaults in a commented JSON format. */ annotatedDefaults() { return Private.annotatedDefaults(this.schema, this.id); } /** * Dispose of the plugin settings resources. */ dispose() { if (this._isDisposed) { return; } this._isDisposed = true; Signal.clearData(this); } /** * Get an individual setting. * * @param key - The name of the setting being retrieved. * * @returns The setting value. * * #### Notes * This method returns synchronously because it uses a cached copy of the * plugin settings that is synchronized with the registry. */ get(key) { const { composite, user } = this; return { composite: composite[key] !== undefined ? copy(composite[key]) : undefined, user: user[key] !== undefined ? copy(user[key]) : undefined }; } /** * Remove a single setting. * * @param key - The name of the setting being removed. * * @returns A promise that resolves when the setting is removed. * * #### Notes * This function is asynchronous because it writes to the setting registry. */ remove(key) { return this.registry.remove(this.plugin.id, key); } /** * Save all of the plugin's user settings at once. */ save(raw) { return this.registry.upload(this.plugin.id, raw); } /** * Set a single setting. * * @param key - The name of the setting being set. * * @param value - The value of the setting. * * @returns A promise that resolves when the setting has been saved. * * #### Notes * This function is asynchronous because it writes to the setting registry. */ set(key, value) { return this.registry.set(this.plugin.id, key, value); } /** * Validates raw settings with comments. * * @param raw - The JSON with comments string being validated. * * @returns A list of errors or `null` if valid. */ validate(raw) { const data = { composite: {}, user: {} }; const { id, schema } = this.plugin; const validator = this.registry.validator; const version = this.version; return validator.validateData({ data, id, raw, schema, version }, false); } /** * Handle plugin changes in the setting registry. */ _onPluginChanged(sender, plugin) { if (plugin === this.plugin.id) { this._changed.emit(undefined); } } } /** * A namespace for `SettingRegistry` statics. */ (function (SettingRegistry) { /** * Reconcile the menus. * * @param reference The reference list of menus. * @param addition The list of menus to add. * @param warn Warn if the command items are duplicated within the same menu. * @returns The reconciled list of menus. */ function reconcileMenus(reference, addition, warn = false, addNewItems = true) { if (!reference) { return addition && addNewItems ? JSONExt.deepCopy(addition) : []; } if (!addition) { return JSONExt.deepCopy(reference); } const merged = JSONExt.deepCopy(reference); addition.forEach(menu => { const refIndex = merged.findIndex(ref => ref.id === menu.id); if (refIndex >= 0) { merged[refIndex] = { ...merged[refIndex], ...menu, items: reconcileItems(merged[refIndex].items, menu.items, warn, addNewItems) }; } else { if (addNewItems) { merged.push(menu); } } }); return merged; } SettingRegistry.reconcileMenus = reconcileMenus; /** * Merge two set of menu items. * * @param reference Reference set of menu items * @param addition New items to add * @param warn Whether to warn if item is duplicated; default to false * @returns The merged set of items */ function reconcileItems(reference, addition, warn = false, addNewItems = true) { if (!reference) { return addition ? JSONExt.deepCopy(addition) : undefined; } if (!addition) { return JSONExt.deepCopy(reference); } const items = JSONExt.deepCopy(reference); // Merge array element depending on the type addition.forEach(item => { var _a; switch ((_a = item.type) !== null && _a !== void 0 ? _a : 'command') { case 'separator': if (addNewItems) { items.push({ ...item }); } break; case 'submenu': if (item.submenu) { const refIndex = items.findIndex(ref => { var _a, _b; return ref.type === 'submenu' && ((_a = ref.submenu) === null || _a === void 0 ? void 0 : _a.id) === ((_b = item.submenu) === null || _b === void 0 ? void 0 : _b.id); }); if (refIndex < 0) { if (addNewItems) { items.push(JSONExt.deepCopy(item)); } } else { items[refIndex] = { ...items[refIndex], ...item, submenu: reconcileMenus(items[refIndex].submenu ? [items[refIndex].submenu] : null, [item.submenu], warn, addNewItems)[0] }; } } break; case 'command': if (item.command) { const refIndex = items.findIndex(ref => { var _a, _b; return ref.command === item.command && ref.selector === item.selector && JSONExt.deepEqual((_a = ref.args) !== null && _a !== void 0 ? _a : {}, (_b = item.args) !== null && _b !== void 0 ? _b : {}); }); if (refIndex < 0) { if (addNewItems) { items.push({ ...item }); } } else { if (warn) { console.warn(`Menu entry for command '${item.command}' is duplicated.`); } items[refIndex] = { ...items[refIndex], ...item }; } } } }); return items; } SettingRegistry.reconcileItems = reconcileItems; /** * Remove disabled entries from menu items * * @param items Menu items * @returns Filtered menu items */ function filterDisabledItems(items) { return items.reduce((final, value) => { var _a; const copy = { ...value }; if (!copy.disabled) { if (copy.type === 'submenu') { const { submenu } = copy; if (submenu && !submenu.disabled) { copy.submenu = { ...submenu, items: filterDisabledItems((_a = submenu.items) !== null && _a !== void 0 ? _a : []) }; } } final.push(copy); } return final; }, []); } SettingRegistry.filterDisabledItems = filterDisabledItems; /** * Reconcile default and user shortcuts and return the composite list. * * @param defaults - The list of default shortcuts. * * @param user - The list of user shortcut overrides and additions. * * @returns A loadable list of shortcuts (omitting disabled and overridden). */ function reconcileShortcuts(defaults, user) { const memo = {}; // If a user shortcut collides with another user shortcut warn and filter. user = [ // Reorder so that disabled are first ...user.filter(s => !!s.disabled), ...user.filter(s => !s.disabled) ].filter(shortcut => { const keys = CommandRegistry.normalizeKeys(shortcut).join(RECORD_SEPARATOR); if (!keys) { console.warn('Skipping this shortcut because there are no actionable keys on this platform', shortcut); return false; } if (!(keys in memo)) { memo[keys] = {}; } const { disabled, selector } = shortcut; if (!(selector in memo[keys])) { memo[keys][selector] = { enabledUserShortcut: disabled ? null : shortcut, enabledDefaultShortcut: null, shouldDisableDefaultShortcut: !!disabled }; return !disabled; } if (memo[keys][selector].enabledUserShortcut === null) { if (disabled) { memo[keys][selector].shouldDisableDefaultShortcut = true; return false; } else { memo[keys][selector].enabledUserShortcut = shortcut; return true; } } else { console.warn('Skipping', shortcut, 'shortcut because it collides with another enabled shortcut:', memo[keys][selector].enabledUserShortcut); return false; } }); // If a default shortcut collides with another default, warn and filter, // unless one of the shortcuts is a disabling shortcut (so look through // disabled shortcuts first). If a shortcut has already been added by the // user preferences, filter it out too (this includes shortcuts that are // disabled by user preferences). defaults = [ ...defaults.filter(s => !!s.disabled), ...defaults.filter(s => !s.disabled) ].filter(shortcut => { const keys = CommandRegistry.normalizeKeys(shortcut).join(RECORD_SEPARATOR); if (!keys) { return false; } if (!(keys in memo)) { memo[keys] = {}; } const { disabled, selector } = shortcut; if (!(selector in memo[keys])) { memo[keys][selector] = { enabledUserShortcut: null, // we would have seen it already as we processed user shortcuts earlier enabledDefaultShortcut: disabled ? null : shortcut, shouldDisableDefaultShortcut: !!disabled }; return !disabled; } if (memo[keys][selector].enabledDefaultShortcut === null) { if (disabled) { memo[keys][selector].shouldDisableDefaultShortcut = true; return false; } else { if (memo[keys][selector].shouldDisableDefaultShortcut) { // Default shortcut was disabled - no warning return false; } else { memo[keys][selector].enabledDefaultShortcut = shortcut; return true; } } } else { if (memo[keys][selector].shouldDisableDefaultShortcut) { // Default shortcut was disabled - no warning return false; } else { // Default shortcut conflicts - emit warning console.warn('Skipping', shortcut, 'default shortcut because it collides with another enabled default shortcut:', memo[keys][selector].enabledDefaultShortcut); return false; } } }); // Return all the shortcuts that should be registered return Private.upgradeShortcuts(user .concat(defaults) .filter(shortcut => !shortcut.disabled) // Fix shortcuts comparison in rjsf Form to avoid polluting the user settings .map(shortcut => { return { args: {}, ...shortcut }; })); } SettingRegistry.reconcileShortcuts = reconcileShortcuts; /** * Merge two set of toolbar items. * * @param reference Reference set of toolbar items * @param addition New items to add * @param warn Whether to warn if item is duplicated; default to false * @returns The merged set of items */ function reconcileToolbarItems(reference, addition, warn = false) { if (!reference) { return addition ? JSONExt.deepCopy(addition) : undefined; } if (!addition) { return JSONExt.deepCopy(reference); } const items = JSONExt.deepCopy(reference); // Merge array element depending on the type addition.forEach(item => { // Name must be unique so it's sufficient to only compare it const refIndex = items.findIndex(ref => ref.name === item.name); if (refIndex < 0) { items.push({ ...item }); } else { if (warn && JSONExt.deepEqual(Object.keys(item), Object.keys(items[refIndex]))) { console.warn(`Toolbar item '${item.name}' is duplicated.`); } items[refIndex] = { ...items[refIndex], ...item }; } }); return items; } SettingRegistry.reconcileToolbarItems = reconcileToolbarItems; })(SettingRegistry || (SettingRegistry = {})); /** * A namespace for private module data. */ var Private; (function (Private) { /** * The default indentation level, uses spaces instead of tabs. */ const indent = ' '; /** * Replacement text for schema properties missing a `description` field. */ const nondescript = '[missing schema description]'; /** * Replacement text for schema properties missing a `title` field. */ const untitled = '[missing schema title]'; /** * Returns an annotated (JSON with comments) version of a schema's defaults. */ function annotatedDefaults(schema, plugin) { const { description, properties, title } = schema; const keys = properties ? Object.keys(properties).sort((a, b) => a.localeCompare(b)) : []; const length = Math.max((description || nondescript).length, plugin.length); return [ '{', prefix(`${title || untitled}`), prefix(plugin), prefix(description || nondescript), prefix('*'.repeat(length)), '', join(keys.map(key => defaultDocumentedValue(schema, key))), '}' ].join('\n'); } Private.annotatedDefaults = annotatedDefaults; /** * Returns an annotated (JSON with comments) version of a plugin's * setting data. */ function annotatedPlugin(plugin, data) { const { description, title } = plugin.schema; const keys = Object.keys(data).sort((a, b) => a.localeCompare(b)); const length = Math.max((description || nondescript).length, plugin.id.length); return [ '{', prefix(`${title || untitled}`), prefix(plugin.id), prefix(description || nondescript), prefix('*'.repeat(length)), '', join(keys.map(key => documentedValue(plugin.schema, key, data[key]))), '}' ].join('\n'); } Private.annotatedPlugin = annotatedPlugin; /** * Returns the default value-with-documentation-string for a * specific schema property. */ function defaultDocumentedValue(schema, key) { const props = (schema.properties && schema.properties[key]) || {}; const type = props['type']; const description = props['description'] || nondescript; const title = props['title'] || ''; const reified = reifyDefault(schema, key); const spaces = indent.length; const defaults = reified !== undefined ? prefix(`"${key}": ${JSON.stringify(reified, null, spaces)}`, indent) : prefix(`"${key}": ${type}`); return [prefix(title), prefix(description), defaults] .filter(str => str.length) .join('\n'); } /** * Returns a value-with-documentation-string for a specific schema property. */ function documentedValue(schema, key, value) { const props = schema.properties && schema.properties[key]; const description = (props && props['description']) || nondescript; const title = (props && props['title']) || untitled; const spaces = indent.length; const attribute = prefix(`"${key}": ${JSON.stringify(value, null, spaces)}`, indent); return [prefix(title), prefix(description), attribute].join('\n'); } /** * Returns a joined string with line breaks and commas where appropriate. */ function join(body) { return body.reduce((acc, val, idx) => { const rows = val.split('\n'); const last = rows[rows.length - 1]; const comment = last.trim().indexOf('//') === 0; const comma = comment || idx === body.length - 1 ? '' : ','; const separator = idx === body.length - 1 ? '' : '\n\n'; return acc + val + comma + separator; }, ''); } /** * Returns a documentation string with a comment prefix added on every line. */ function prefix(source, pre = `${indent}// `) { return pre + source.split('\n').join(`\n${pre}`); } /** * Create a fully extrapolated default value for a root key in a schema. * * @todo This function would ideally reuse `getDefaultFormState` from rjsf * with appropriate`defaultFormStateBehavior` setting, as currently * these two implementations duplicate each other. * * Note: absence of a property may mean something else than the default. */ function reifyDefault(schema, root, definitions, required) { var _a, _b, _c, _d, _e, _f, _g; definitions = definitions !== null && definitions !== void 0 ? definitions : schema.definitions; // If the property is at the root level, traverse its schema. required = root ? schema.required instanceof Array && ((_a = schema.required) === null || _a === void 0 ? void 0 : _a.includes(root)) : required; schema = (root ? (_b = schema.properties) === null || _b === void 0 ? void 0 : _b[root] : schema) || {}; if (schema.type === 'object') { // Make a copy of the default value to populate. const result = JSONExt.deepCopy(schema.default); // Iterate through and populate each child property. const props = schema.properties || {}; for (const property in props) { result[property] = reifyDefault(props[property], undefined, definitions, schema.required instanceof Array && ((_c = schema.required) === null || _c === void 0 ? void 0 : _c.includes(property))); } return result; } else if (schema.type === 'array') { const defaultDefined = typeof schema.default !== 'undefined'; const shouldPopulateDefaultArray = defaultDefined || required; if (!shouldPopulateDefaultArray) { return undefined; } // Make a copy of the default value to populate. const result = defaultDefined ? JSONExt.deepCopy(schema.default) : []; // Items defines the properties of each item in the array let props = schema.items || {}; // Use referenced definition if one exists if (props['$ref'] && definitions) { const ref = props['$ref'].replace('#/definitions/', ''); props = (_d = definitions[ref]) !== null && _d !== void 0 ? _d : {}; } // Iterate through the items in the array and fill in defaults for (const item in result) { if (props.type === 'object') { // Use the values that are hard-coded in the default array over the defaults for each field. const reified = (_f = (_e = reifyDefault(props, undefined, definitions)) !== null && _e !== void 0 ? _e : result[item]) !== null && _f !== void 0 ? _f : {}; for (const prop in reified) { if ((_g = result[item]) === null || _g === void 0 ? void 0 : _g[prop]) { reified[prop] = result[item][prop]; } } result[item] = reified; } } return result; } else { return schema.default; } } Private.reifyDefault = reifyDefault; /** * Selectors which were previously warned about. */ const selectorsAlreadyWarnedAbout = new Set(); /** * Upgrade shortcuts to ensure no breaking changes between minor versions. */ function upgradeShortcuts(shortcuts) { const selectorDeprecationWarnings = new Set(); const changes = [ { old: '.jp-Notebook:focus.jp-mod-commandMode', new: '.jp-Notebook.jp-mod-commandMode:not(.jp-mod-readWrite) :focus', versionDeprecated: 'JupyterLab 4.1' }, { old: '.jp-Notebook.jp-mod-commandMode :focus:not(:read-write)', new: '.jp-Notebook.jp-mod-commandMode:not(.jp-mod-readWrite) :focus', versionDeprecated: 'JupyterLab 4.1.1' }, { old: '.jp-Notebook:focus', new: '.jp-Notebook.jp-mod-commandMode:not(.jp-mod-readWrite) :focus', versionDeprecated: 'JupyterLab 4.1' }, { old: '[data-jp-traversable]:focus', new: '.jp-Notebook.jp-mod-commandMode:not(.jp-mod-readWrite) :focus', versionDeprecated: 'JupyterLab 4.1' }, { old: '[data-jp-kernel-user]:focus', new: '[data-jp-kernel-user]:not(.jp-mod-readWrite) :focus:not(:read-write)', versionDeprecated: 'JupyterLab 4.1' }, { old: '[data-jp-kernel-user] :focus:not(:read-write)', new: '[data-jp-kernel-user]:not(.jp-mod-readWrite) :focus:not(:read-write)', versionDeprecated: 'JupyterLab 4.1.1' } ]; const upgraded = shortcuts.map(shortcut => { const oldSelector = shortcut.selector; let newSelector = oldSelector; for (const change of changes) { if (oldSelector.includes(change.old)) { newSelector = oldSelector.replace(change.old, change.new); if (!selectorsAlreadyWarnedAbout.has(oldSelector)) { selectorDeprecationWarnings.add(`"${change.old}" was replaced with "${change.new}" in ${change.versionDeprecated} (present in "${oldSelector}")`); selectorsAlreadyWarnedAbout.add(oldSelector); } } } shortcut.selector = newSelector; return shortcut; }); if (selectorDeprecationWarnings.size > 0) { console.warn('Deprecated shortcut selectors: ' + [...selectorDeprecationWarnings].join('\n') + '\n\nThe selectors will be substituted transparently this time, but need to be updated at source before next major release.'); } return upgraded; } Private.upgradeShortcuts = upgradeShortcuts; })(Private || (Private = {})); //# sourceMappingURL=settingregistry.js.map