@jupyterlab/settingregistry
Version:
Settings registry for Jupyterlab
1,209 lines • 45.6 kB
JavaScript
// 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