UNPKG

37.8 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { CommandRegistry } from '@lumino/commands';
4import { JSONExt } from '@lumino/coreutils';
5import { DisposableDelegate } from '@lumino/disposable';
6import { Signal } from '@lumino/signaling';
7import Ajv from 'ajv';
8import * as json5 from 'json5';
9import SCHEMA from './plugin-schema.json';
10/**
11 * An alias for the JSON deep copy function.
12 */
13const copy = JSONExt.deepCopy;
14/**
15 * The default number of milliseconds before a `load()` call to the registry
16 * will wait before timing out if it requires a transformation that has not been
17 * registered.
18 */
19const DEFAULT_TRANSFORM_TIMEOUT = 1000;
20/**
21 * The ASCII record separator character.
22 */
23const RECORD_SEPARATOR = String.fromCharCode(30);
24/**
25 * The default implementation of a schema validator.
26 */
27export class DefaultSchemaValidator {
28 /**
29 * Instantiate a schema validator.
30 */
31 constructor() {
32 this._composer = new Ajv({ useDefaults: true });
33 this._validator = new Ajv();
34 this._composer.addSchema(SCHEMA, 'jupyterlab-plugin-schema');
35 this._validator.addSchema(SCHEMA, 'jupyterlab-plugin-schema');
36 }
37 /**
38 * Validate a plugin's schema and user data; populate the `composite` data.
39 *
40 * @param plugin - The plugin being validated. Its `composite` data will be
41 * populated by reference.
42 *
43 * @param populate - Whether plugin data should be populated, defaults to
44 * `true`.
45 *
46 * @return A list of errors if either the schema or data fail to validate or
47 * `null` if there are no errors.
48 */
49 validateData(plugin, populate = true) {
50 const validate = this._validator.getSchema(plugin.id);
51 const compose = this._composer.getSchema(plugin.id);
52 // If the schemas do not exist, add them to the validator and continue.
53 if (!validate || !compose) {
54 if (plugin.schema.type !== 'object') {
55 const keyword = 'schema';
56 const message = `Setting registry schemas' root-level type must be ` +
57 `'object', rejecting type: ${plugin.schema.type}`;
58 return [{ dataPath: 'type', keyword, schemaPath: '', message }];
59 }
60 const errors = this._addSchema(plugin.id, plugin.schema);
61 return errors || this.validateData(plugin);
62 }
63 // Parse the raw commented JSON into a user map.
64 let user;
65 try {
66 user = json5.parse(plugin.raw);
67 }
68 catch (error) {
69 if (error instanceof SyntaxError) {
70 return [
71 {
72 dataPath: '',
73 keyword: 'syntax',
74 schemaPath: '',
75 message: error.message
76 }
77 ];
78 }
79 const { column, description } = error;
80 const line = error.lineNumber;
81 return [
82 {
83 dataPath: '',
84 keyword: 'parse',
85 schemaPath: '',
86 message: `${description} (line ${line} column ${column})`
87 }
88 ];
89 }
90 if (!validate(user)) {
91 return validate.errors;
92 }
93 // Copy the user data before merging defaults into composite map.
94 const composite = copy(user);
95 if (!compose(composite)) {
96 return compose.errors;
97 }
98 if (populate) {
99 plugin.data = { composite, user };
100 }
101 return null;
102 }
103 /**
104 * Add a schema to the validator.
105 *
106 * @param plugin - The plugin ID.
107 *
108 * @param schema - The schema being added.
109 *
110 * @return A list of errors if the schema fails to validate or `null` if there
111 * are no errors.
112 *
113 * #### Notes
114 * It is safe to call this function multiple times with the same plugin name.
115 */
116 _addSchema(plugin, schema) {
117 const composer = this._composer;
118 const validator = this._validator;
119 const validate = validator.getSchema('jupyterlab-plugin-schema');
120 // Validate against the main schema.
121 if (!validate(schema)) {
122 return validate.errors;
123 }
124 // Validate against the JSON schema meta-schema.
125 if (!validator.validateSchema(schema)) {
126 return validator.errors;
127 }
128 // Remove if schema already exists.
129 composer.removeSchema(plugin);
130 validator.removeSchema(plugin);
131 // Add schema to the validator and composer.
132 composer.addSchema(schema, plugin);
133 validator.addSchema(schema, plugin);
134 return null;
135 }
136}
137/**
138 * The default concrete implementation of a setting registry.
139 */
140export class SettingRegistry {
141 /**
142 * Create a new setting registry.
143 */
144 constructor(options) {
145 /**
146 * The schema of the setting registry.
147 */
148 this.schema = SCHEMA;
149 /**
150 * The collection of setting registry plugins.
151 */
152 this.plugins = Object.create(null);
153 this._pluginChanged = new Signal(this);
154 this._ready = Promise.resolve();
155 this._transformers = Object.create(null);
156 this.connector = options.connector;
157 this.validator = options.validator || new DefaultSchemaValidator();
158 this._timeout = options.timeout || DEFAULT_TRANSFORM_TIMEOUT;
159 // Preload with any available data at instantiation-time.
160 if (options.plugins) {
161 this._ready = this._preload(options.plugins);
162 }
163 }
164 /**
165 * A signal that emits the name of a plugin when its settings change.
166 */
167 get pluginChanged() {
168 return this._pluginChanged;
169 }
170 /**
171 * Get an individual setting.
172 *
173 * @param plugin - The name of the plugin whose settings are being retrieved.
174 *
175 * @param key - The name of the setting being retrieved.
176 *
177 * @returns A promise that resolves when the setting is retrieved.
178 */
179 async get(plugin, key) {
180 // Wait for data preload before allowing normal operation.
181 await this._ready;
182 const plugins = this.plugins;
183 if (plugin in plugins) {
184 const { composite, user } = plugins[plugin].data;
185 return {
186 composite: composite[key] !== undefined ? copy(composite[key]) : undefined,
187 user: user[key] !== undefined ? copy(user[key]) : undefined
188 };
189 }
190 return this.load(plugin).then(() => this.get(plugin, key));
191 }
192 /**
193 * Load a plugin's settings into the setting registry.
194 *
195 * @param plugin - The name of the plugin whose settings are being loaded.
196 *
197 * @returns A promise that resolves with a plugin settings object or rejects
198 * if the plugin is not found.
199 */
200 async load(plugin) {
201 // Wait for data preload before allowing normal operation.
202 await this._ready;
203 const plugins = this.plugins;
204 const registry = this; // eslint-disable-line
205 // If the plugin exists, resolve.
206 if (plugin in plugins) {
207 return new Settings({ plugin: plugins[plugin], registry });
208 }
209 // If the plugin needs to be loaded from the data connector, fetch.
210 return this.reload(plugin);
211 }
212 /**
213 * Reload a plugin's settings into the registry even if they already exist.
214 *
215 * @param plugin - The name of the plugin whose settings are being reloaded.
216 *
217 * @returns A promise that resolves with a plugin settings object or rejects
218 * with a list of `ISchemaValidator.IError` objects if it fails.
219 */
220 async reload(plugin) {
221 // Wait for data preload before allowing normal operation.
222 await this._ready;
223 const fetched = await this.connector.fetch(plugin);
224 const plugins = this.plugins; // eslint-disable-line
225 const registry = this; // eslint-disable-line
226 if (fetched === undefined) {
227 throw [
228 {
229 dataPath: '',
230 keyword: 'id',
231 message: `Could not fetch settings for ${plugin}.`,
232 schemaPath: ''
233 }
234 ];
235 }
236 await this._load(await this._transform('fetch', fetched));
237 this._pluginChanged.emit(plugin);
238 return new Settings({ plugin: plugins[plugin], registry });
239 }
240 /**
241 * Remove a single setting in the registry.
242 *
243 * @param plugin - The name of the plugin whose setting is being removed.
244 *
245 * @param key - The name of the setting being removed.
246 *
247 * @returns A promise that resolves when the setting is removed.
248 */
249 async remove(plugin, key) {
250 // Wait for data preload before allowing normal operation.
251 await this._ready;
252 const plugins = this.plugins;
253 if (!(plugin in plugins)) {
254 return;
255 }
256 const raw = json5.parse(plugins[plugin].raw);
257 // Delete both the value and any associated comment.
258 delete raw[key];
259 delete raw[`// ${key}`];
260 plugins[plugin].raw = Private.annotatedPlugin(plugins[plugin], raw);
261 return this._save(plugin);
262 }
263 /**
264 * Set a single setting in the registry.
265 *
266 * @param plugin - The name of the plugin whose setting is being set.
267 *
268 * @param key - The name of the setting being set.
269 *
270 * @param value - The value of the setting being set.
271 *
272 * @returns A promise that resolves when the setting has been saved.
273 *
274 */
275 async set(plugin, key, value) {
276 // Wait for data preload before allowing normal operation.
277 await this._ready;
278 const plugins = this.plugins;
279 if (!(plugin in plugins)) {
280 return this.load(plugin).then(() => this.set(plugin, key, value));
281 }
282 // Parse the raw JSON string removing all comments and return an object.
283 const raw = json5.parse(plugins[plugin].raw);
284 plugins[plugin].raw = Private.annotatedPlugin(plugins[plugin], Object.assign(Object.assign({}, raw), { [key]: value }));
285 return this._save(plugin);
286 }
287 /**
288 * Register a plugin transform function to act on a specific plugin.
289 *
290 * @param plugin - The name of the plugin whose settings are transformed.
291 *
292 * @param transforms - The transform functions applied to the plugin.
293 *
294 * @returns A disposable that removes the transforms from the registry.
295 *
296 * #### Notes
297 * - `compose` transformations: The registry automatically overwrites a
298 * plugin's default values with user overrides, but a plugin may instead wish
299 * to merge values. This behavior can be accomplished in a `compose`
300 * transformation.
301 * - `fetch` transformations: The registry uses the plugin data that is
302 * fetched from its connector. If a plugin wants to override, e.g. to update
303 * its schema with dynamic defaults, a `fetch` transformation can be applied.
304 */
305 transform(plugin, transforms) {
306 const transformers = this._transformers;
307 if (plugin in transformers) {
308 const error = new Error(`${plugin} already has a transformer.`);
309 error.name = 'TransformError';
310 throw error;
311 }
312 transformers[plugin] = {
313 fetch: transforms.fetch || (plugin => plugin),
314 compose: transforms.compose || (plugin => plugin)
315 };
316 return new DisposableDelegate(() => {
317 delete transformers[plugin];
318 });
319 }
320 /**
321 * Upload a plugin's settings.
322 *
323 * @param plugin - The name of the plugin whose settings are being set.
324 *
325 * @param raw - The raw plugin settings being uploaded.
326 *
327 * @returns A promise that resolves when the settings have been saved.
328 */
329 async upload(plugin, raw) {
330 // Wait for data preload before allowing normal operation.
331 await this._ready;
332 const plugins = this.plugins;
333 if (!(plugin in plugins)) {
334 return this.load(plugin).then(() => this.upload(plugin, raw));
335 }
336 // Set the local copy.
337 plugins[plugin].raw = raw;
338 return this._save(plugin);
339 }
340 /**
341 * Load a plugin into the registry.
342 */
343 async _load(data) {
344 const plugin = data.id;
345 // Validate and preload the item.
346 try {
347 await this._validate(data);
348 }
349 catch (errors) {
350 const output = [`Validating ${plugin} failed:`];
351 errors.forEach((error, index) => {
352 const { dataPath, schemaPath, keyword, message } = error;
353 if (dataPath || schemaPath) {
354 output.push(`${index} - schema @ ${schemaPath}, data @ ${dataPath}`);
355 }
356 output.push(`{${keyword}} ${message}`);
357 });
358 console.warn(output.join('\n'));
359 throw errors;
360 }
361 }
362 /**
363 * Preload a list of plugins and fail gracefully.
364 */
365 async _preload(plugins) {
366 await Promise.all(plugins.map(async (plugin) => {
367 var _a;
368 try {
369 // Apply a transformation to the plugin if necessary.
370 await this._load(await this._transform('fetch', plugin));
371 }
372 catch (errors) {
373 /* Ignore preload timeout errors silently. */
374 if (((_a = errors[0]) === null || _a === void 0 ? void 0 : _a.keyword) !== 'timeout') {
375 console.warn('Ignored setting registry preload errors.', errors);
376 }
377 }
378 }));
379 }
380 /**
381 * Save a plugin in the registry.
382 */
383 async _save(plugin) {
384 const plugins = this.plugins;
385 if (!(plugin in plugins)) {
386 throw new Error(`${plugin} does not exist in setting registry.`);
387 }
388 try {
389 await this._validate(plugins[plugin]);
390 }
391 catch (errors) {
392 console.warn(`${plugin} validation errors:`, errors);
393 throw new Error(`${plugin} failed to validate; check console.`);
394 }
395 await this.connector.save(plugin, plugins[plugin].raw);
396 // Fetch and reload the data to guarantee server and client are in sync.
397 const fetched = await this.connector.fetch(plugin);
398 if (fetched === undefined) {
399 throw [
400 {
401 dataPath: '',
402 keyword: 'id',
403 message: `Could not fetch settings for ${plugin}.`,
404 schemaPath: ''
405 }
406 ];
407 }
408 await this._load(await this._transform('fetch', fetched));
409 this._pluginChanged.emit(plugin);
410 }
411 /**
412 * Transform the plugin if necessary.
413 */
414 async _transform(phase, plugin, started = new Date().getTime()) {
415 const elapsed = new Date().getTime() - started;
416 const id = plugin.id;
417 const transformers = this._transformers;
418 const timeout = this._timeout;
419 if (!plugin.schema['jupyter.lab.transform']) {
420 return plugin;
421 }
422 if (id in transformers) {
423 const transformed = transformers[id][phase].call(null, plugin);
424 if (transformed.id !== id) {
425 throw [
426 {
427 dataPath: '',
428 keyword: 'id',
429 message: 'Plugin transformations cannot change plugin IDs.',
430 schemaPath: ''
431 }
432 ];
433 }
434 return transformed;
435 }
436 // If the timeout has not been exceeded, stall and try again in 250ms.
437 if (elapsed < timeout) {
438 await new Promise(resolve => {
439 setTimeout(() => {
440 resolve();
441 }, 250);
442 });
443 return this._transform(phase, plugin, started);
444 }
445 throw [
446 {
447 dataPath: '',
448 keyword: 'timeout',
449 message: `Transforming ${plugin.id} timed out.`,
450 schemaPath: ''
451 }
452 ];
453 }
454 /**
455 * Validate and preload a plugin, compose the `composite` data.
456 */
457 async _validate(plugin) {
458 // Validate the user data and create the composite data.
459 const errors = this.validator.validateData(plugin);
460 if (errors) {
461 throw errors;
462 }
463 // Apply a transformation if necessary and set the local copy.
464 this.plugins[plugin.id] = await this._transform('compose', plugin);
465 }
466}
467/**
468 * A manager for a specific plugin's settings.
469 */
470export class Settings {
471 /**
472 * Instantiate a new plugin settings manager.
473 */
474 constructor(options) {
475 this._changed = new Signal(this);
476 this._isDisposed = false;
477 this.id = options.plugin.id;
478 this.registry = options.registry;
479 this.registry.pluginChanged.connect(this._onPluginChanged, this);
480 }
481 /**
482 * A signal that emits when the plugin's settings have changed.
483 */
484 get changed() {
485 return this._changed;
486 }
487 /**
488 * The composite of user settings and extension defaults.
489 */
490 get composite() {
491 return this.plugin.data.composite;
492 }
493 /**
494 * Test whether the plugin settings manager disposed.
495 */
496 get isDisposed() {
497 return this._isDisposed;
498 }
499 get plugin() {
500 return this.registry.plugins[this.id];
501 }
502 /**
503 * The plugin's schema.
504 */
505 get schema() {
506 return this.plugin.schema;
507 }
508 /**
509 * The plugin settings raw text value.
510 */
511 get raw() {
512 return this.plugin.raw;
513 }
514 /**
515 * Checks if any fields are different from the default value.
516 */
517 isDefault(user) {
518 for (const key in this.schema.properties) {
519 const value = user[key];
520 const defaultValue = this.default(key);
521 if (value === undefined ||
522 defaultValue === undefined ||
523 JSONExt.deepEqual(value, JSONExt.emptyObject) ||
524 JSONExt.deepEqual(value, JSONExt.emptyArray)) {
525 continue;
526 }
527 if (!JSONExt.deepEqual(value, defaultValue)) {
528 return false;
529 }
530 }
531 return true;
532 }
533 get isModified() {
534 return !this.isDefault(this.user);
535 }
536 /**
537 * The user settings.
538 */
539 get user() {
540 return this.plugin.data.user;
541 }
542 /**
543 * The published version of the NPM package containing these settings.
544 */
545 get version() {
546 return this.plugin.version;
547 }
548 /**
549 * Return the defaults in a commented JSON format.
550 */
551 annotatedDefaults() {
552 return Private.annotatedDefaults(this.schema, this.id);
553 }
554 /**
555 * Calculate the default value of a setting by iterating through the schema.
556 *
557 * @param key - The name of the setting whose default value is calculated.
558 *
559 * @returns A calculated default JSON value for a specific setting.
560 */
561 default(key) {
562 return Private.reifyDefault(this.schema, key);
563 }
564 /**
565 * Dispose of the plugin settings resources.
566 */
567 dispose() {
568 if (this._isDisposed) {
569 return;
570 }
571 this._isDisposed = true;
572 Signal.clearData(this);
573 }
574 /**
575 * Get an individual setting.
576 *
577 * @param key - The name of the setting being retrieved.
578 *
579 * @returns The setting value.
580 *
581 * #### Notes
582 * This method returns synchronously because it uses a cached copy of the
583 * plugin settings that is synchronized with the registry.
584 */
585 get(key) {
586 const { composite, user } = this;
587 return {
588 composite: composite[key] !== undefined ? copy(composite[key]) : undefined,
589 user: user[key] !== undefined ? copy(user[key]) : undefined
590 };
591 }
592 /**
593 * Remove a single setting.
594 *
595 * @param key - The name of the setting being removed.
596 *
597 * @returns A promise that resolves when the setting is removed.
598 *
599 * #### Notes
600 * This function is asynchronous because it writes to the setting registry.
601 */
602 remove(key) {
603 return this.registry.remove(this.plugin.id, key);
604 }
605 /**
606 * Save all of the plugin's user settings at once.
607 */
608 save(raw) {
609 return this.registry.upload(this.plugin.id, raw);
610 }
611 /**
612 * Set a single setting.
613 *
614 * @param key - The name of the setting being set.
615 *
616 * @param value - The value of the setting.
617 *
618 * @returns A promise that resolves when the setting has been saved.
619 *
620 * #### Notes
621 * This function is asynchronous because it writes to the setting registry.
622 */
623 set(key, value) {
624 return this.registry.set(this.plugin.id, key, value);
625 }
626 /**
627 * Validates raw settings with comments.
628 *
629 * @param raw - The JSON with comments string being validated.
630 *
631 * @returns A list of errors or `null` if valid.
632 */
633 validate(raw) {
634 const data = { composite: {}, user: {} };
635 const { id, schema } = this.plugin;
636 const validator = this.registry.validator;
637 const version = this.version;
638 return validator.validateData({ data, id, raw, schema, version }, false);
639 }
640 /**
641 * Handle plugin changes in the setting registry.
642 */
643 _onPluginChanged(sender, plugin) {
644 if (plugin === this.plugin.id) {
645 this._changed.emit(undefined);
646 }
647 }
648}
649/**
650 * A namespace for `SettingRegistry` statics.
651 */
652(function (SettingRegistry) {
653 /**
654 * Reconcile the menus.
655 *
656 * @param reference The reference list of menus.
657 * @param addition The list of menus to add.
658 * @param warn Warn if the command items are duplicated within the same menu.
659 * @returns The reconciled list of menus.
660 */
661 function reconcileMenus(reference, addition, warn = false, addNewItems = true) {
662 if (!reference) {
663 return addition && addNewItems ? JSONExt.deepCopy(addition) : [];
664 }
665 if (!addition) {
666 return JSONExt.deepCopy(reference);
667 }
668 const merged = JSONExt.deepCopy(reference);
669 addition.forEach(menu => {
670 const refIndex = merged.findIndex(ref => ref.id === menu.id);
671 if (refIndex >= 0) {
672 merged[refIndex] = Object.assign(Object.assign(Object.assign({}, merged[refIndex]), menu), { items: reconcileItems(merged[refIndex].items, menu.items, warn, addNewItems) });
673 }
674 else {
675 if (addNewItems) {
676 merged.push(menu);
677 }
678 }
679 });
680 return merged;
681 }
682 SettingRegistry.reconcileMenus = reconcileMenus;
683 /**
684 * Merge two set of menu items.
685 *
686 * @param reference Reference set of menu items
687 * @param addition New items to add
688 * @param warn Whether to warn if item is duplicated; default to false
689 * @returns The merged set of items
690 */
691 function reconcileItems(reference, addition, warn = false, addNewItems = true) {
692 if (!reference) {
693 return addition ? JSONExt.deepCopy(addition) : undefined;
694 }
695 if (!addition) {
696 return JSONExt.deepCopy(reference);
697 }
698 const items = JSONExt.deepCopy(reference);
699 // Merge array element depending on the type
700 addition.forEach(item => {
701 var _a;
702 switch ((_a = item.type) !== null && _a !== void 0 ? _a : 'command') {
703 case 'separator':
704 if (addNewItems) {
705 items.push(Object.assign({}, item));
706 }
707 break;
708 case 'submenu':
709 if (item.submenu) {
710 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); });
711 if (refIndex < 0) {
712 if (addNewItems) {
713 items.push(JSONExt.deepCopy(item));
714 }
715 }
716 else {
717 items[refIndex] = Object.assign(Object.assign(Object.assign({}, items[refIndex]), item), { submenu: reconcileMenus(items[refIndex].submenu
718 ? [items[refIndex].submenu]
719 : null, [item.submenu], warn, addNewItems)[0] });
720 }
721 }
722 break;
723 case 'command':
724 if (item.command) {
725 const refIndex = items.findIndex(ref => {
726 var _a, _b;
727 return ref.command === item.command &&
728 ref.selector === item.selector &&
729 JSONExt.deepEqual((_a = ref.args) !== null && _a !== void 0 ? _a : {}, (_b = item.args) !== null && _b !== void 0 ? _b : {});
730 });
731 if (refIndex < 0) {
732 if (addNewItems) {
733 items.push(Object.assign({}, item));
734 }
735 }
736 else {
737 if (warn) {
738 console.warn(`Menu entry for command '${item.command}' is duplicated.`);
739 }
740 items[refIndex] = Object.assign(Object.assign({}, items[refIndex]), item);
741 }
742 }
743 }
744 });
745 return items;
746 }
747 SettingRegistry.reconcileItems = reconcileItems;
748 /**
749 * Remove disabled entries from menu items
750 *
751 * @param items Menu items
752 * @returns Filtered menu items
753 */
754 function filterDisabledItems(items) {
755 return items.reduce((final, value) => {
756 var _a;
757 const copy = Object.assign({}, value);
758 if (!copy.disabled) {
759 if (copy.type === 'submenu') {
760 const { submenu } = copy;
761 if (submenu && !submenu.disabled) {
762 copy.submenu = Object.assign(Object.assign({}, submenu), { items: filterDisabledItems((_a = submenu.items) !== null && _a !== void 0 ? _a : []) });
763 }
764 }
765 final.push(copy);
766 }
767 return final;
768 }, []);
769 }
770 SettingRegistry.filterDisabledItems = filterDisabledItems;
771 /**
772 * Reconcile default and user shortcuts and return the composite list.
773 *
774 * @param defaults - The list of default shortcuts.
775 *
776 * @param user - The list of user shortcut overrides and additions.
777 *
778 * @returns A loadable list of shortcuts (omitting disabled and overridden).
779 */
780 function reconcileShortcuts(defaults, user) {
781 const memo = {};
782 // If a user shortcut collides with another user shortcut warn and filter.
783 user = user.filter(shortcut => {
784 const keys = CommandRegistry.normalizeKeys(shortcut).join(RECORD_SEPARATOR);
785 if (!keys) {
786 console.warn('Skipping this shortcut because there are no actionable keys on this platform', shortcut);
787 return false;
788 }
789 if (!(keys in memo)) {
790 memo[keys] = {};
791 }
792 const { selector } = shortcut;
793 if (!(selector in memo[keys])) {
794 memo[keys][selector] = false; // Do not warn if a default shortcut conflicts.
795 return true;
796 }
797 console.warn('Skipping this shortcut because it collides with another shortcut.', shortcut);
798 return false;
799 });
800 // If a default shortcut collides with another default, warn and filter,
801 // unless one of the shortcuts is a disabling shortcut (so look through
802 // disabled shortcuts first). If a shortcut has already been added by the
803 // user preferences, filter it out too (this includes shortcuts that are
804 // disabled by user preferences).
805 defaults = [
806 ...defaults.filter(s => !!s.disabled),
807 ...defaults.filter(s => !s.disabled)
808 ].filter(shortcut => {
809 const keys = CommandRegistry.normalizeKeys(shortcut).join(RECORD_SEPARATOR);
810 if (!keys) {
811 return false;
812 }
813 if (!(keys in memo)) {
814 memo[keys] = {};
815 }
816 const { disabled, selector } = shortcut;
817 if (!(selector in memo[keys])) {
818 // Warn of future conflicts if the default shortcut is not disabled.
819 memo[keys][selector] = !disabled;
820 return true;
821 }
822 // We have a conflict now. Warn the user if we need to do so.
823 if (memo[keys][selector]) {
824 console.warn('Skipping this default shortcut because it collides with another default shortcut.', shortcut);
825 }
826 return false;
827 });
828 // Return all the shortcuts that should be registered
829 return (user
830 .concat(defaults)
831 .filter(shortcut => !shortcut.disabled)
832 // Fix shortcuts comparison in rjsf Form to avoid polluting the user settings
833 .map(shortcut => {
834 return Object.assign({ args: {} }, shortcut);
835 }));
836 }
837 SettingRegistry.reconcileShortcuts = reconcileShortcuts;
838 /**
839 * Merge two set of toolbar items.
840 *
841 * @param reference Reference set of toolbar items
842 * @param addition New items to add
843 * @param warn Whether to warn if item is duplicated; default to false
844 * @returns The merged set of items
845 */
846 function reconcileToolbarItems(reference, addition, warn = false) {
847 if (!reference) {
848 return addition ? JSONExt.deepCopy(addition) : undefined;
849 }
850 if (!addition) {
851 return JSONExt.deepCopy(reference);
852 }
853 const items = JSONExt.deepCopy(reference);
854 // Merge array element depending on the type
855 addition.forEach(item => {
856 // Name must be unique so it's sufficient to only compare it
857 const refIndex = items.findIndex(ref => ref.name === item.name);
858 if (refIndex < 0) {
859 items.push(Object.assign({}, item));
860 }
861 else {
862 if (warn &&
863 JSONExt.deepEqual(Object.keys(item), Object.keys(items[refIndex]))) {
864 console.warn(`Toolbar item '${item.name}' is duplicated.`);
865 }
866 items[refIndex] = Object.assign(Object.assign({}, items[refIndex]), item);
867 }
868 });
869 return items;
870 }
871 SettingRegistry.reconcileToolbarItems = reconcileToolbarItems;
872})(SettingRegistry || (SettingRegistry = {}));
873/**
874 * A namespace for private module data.
875 */
876var Private;
877(function (Private) {
878 /**
879 * The default indentation level, uses spaces instead of tabs.
880 */
881 const indent = ' ';
882 /**
883 * Replacement text for schema properties missing a `description` field.
884 */
885 const nondescript = '[missing schema description]';
886 /**
887 * Replacement text for schema properties missing a `title` field.
888 */
889 const untitled = '[missing schema title]';
890 /**
891 * Returns an annotated (JSON with comments) version of a schema's defaults.
892 */
893 function annotatedDefaults(schema, plugin) {
894 const { description, properties, title } = schema;
895 const keys = properties
896 ? Object.keys(properties).sort((a, b) => a.localeCompare(b))
897 : [];
898 const length = Math.max((description || nondescript).length, plugin.length);
899 return [
900 '{',
901 prefix(`${title || untitled}`),
902 prefix(plugin),
903 prefix(description || nondescript),
904 prefix('*'.repeat(length)),
905 '',
906 join(keys.map(key => defaultDocumentedValue(schema, key))),
907 '}'
908 ].join('\n');
909 }
910 Private.annotatedDefaults = annotatedDefaults;
911 /**
912 * Returns an annotated (JSON with comments) version of a plugin's
913 * setting data.
914 */
915 function annotatedPlugin(plugin, data) {
916 const { description, title } = plugin.schema;
917 const keys = Object.keys(data).sort((a, b) => a.localeCompare(b));
918 const length = Math.max((description || nondescript).length, plugin.id.length);
919 return [
920 '{',
921 prefix(`${title || untitled}`),
922 prefix(plugin.id),
923 prefix(description || nondescript),
924 prefix('*'.repeat(length)),
925 '',
926 join(keys.map(key => documentedValue(plugin.schema, key, data[key]))),
927 '}'
928 ].join('\n');
929 }
930 Private.annotatedPlugin = annotatedPlugin;
931 /**
932 * Returns the default value-with-documentation-string for a
933 * specific schema property.
934 */
935 function defaultDocumentedValue(schema, key) {
936 const props = (schema.properties && schema.properties[key]) || {};
937 const type = props['type'];
938 const description = props['description'] || nondescript;
939 const title = props['title'] || '';
940 const reified = reifyDefault(schema, key);
941 const spaces = indent.length;
942 const defaults = reified !== undefined
943 ? prefix(`"${key}": ${JSON.stringify(reified, null, spaces)}`, indent)
944 : prefix(`"${key}": ${type}`);
945 return [prefix(title), prefix(description), defaults]
946 .filter(str => str.length)
947 .join('\n');
948 }
949 /**
950 * Returns a value-with-documentation-string for a specific schema property.
951 */
952 function documentedValue(schema, key, value) {
953 const props = schema.properties && schema.properties[key];
954 const description = (props && props['description']) || nondescript;
955 const title = (props && props['title']) || untitled;
956 const spaces = indent.length;
957 const attribute = prefix(`"${key}": ${JSON.stringify(value, null, spaces)}`, indent);
958 return [prefix(title), prefix(description), attribute].join('\n');
959 }
960 /**
961 * Returns a joined string with line breaks and commas where appropriate.
962 */
963 function join(body) {
964 return body.reduce((acc, val, idx) => {
965 const rows = val.split('\n');
966 const last = rows[rows.length - 1];
967 const comment = last.trim().indexOf('//') === 0;
968 const comma = comment || idx === body.length - 1 ? '' : ',';
969 const separator = idx === body.length - 1 ? '' : '\n\n';
970 return acc + val + comma + separator;
971 }, '');
972 }
973 /**
974 * Returns a documentation string with a comment prefix added on every line.
975 */
976 function prefix(source, pre = `${indent}// `) {
977 return pre + source.split('\n').join(`\n${pre}`);
978 }
979 /**
980 * Create a fully extrapolated default value for a root key in a schema.
981 */
982 function reifyDefault(schema, root) {
983 var _a, _b, _c;
984 const definitions = schema.definitions;
985 // If the property is at the root level, traverse its schema.
986 schema = (root ? (_a = schema.properties) === null || _a === void 0 ? void 0 : _a[root] : schema) || {};
987 if (schema.type === 'object') {
988 // Make a copy of the default value to populate.
989 const result = JSONExt.deepCopy(schema.default);
990 // Iterate through and populate each child property.
991 const props = schema.properties || {};
992 for (const property in props) {
993 result[property] = reifyDefault(props[property]);
994 }
995 return result;
996 }
997 else if (schema.type === 'array') {
998 // Make a copy of the default value to populate.
999 const result = JSONExt.deepCopy(schema.default);
1000 // Items defines the properties of each item in the array
1001 let props = schema.items || {};
1002 // Use referenced definition if one exists
1003 if (props['$ref'] && definitions) {
1004 const ref = props['$ref'].replace('#/definitions/', '');
1005 props = (_b = definitions[ref]) !== null && _b !== void 0 ? _b : {};
1006 }
1007 // Iterate through the items in the array and fill in defaults
1008 for (const item in result) {
1009 // Use the values that are hard-coded in the default array over the defaults for each field.
1010 const reified = reifyDefault(props) || {};
1011 for (const prop in reified) {
1012 if ((_c = result[item]) === null || _c === void 0 ? void 0 : _c[prop]) {
1013 reified[prop] = result[item][prop];
1014 }
1015 }
1016 result[item] = reified;
1017 }
1018 return result;
1019 }
1020 else {
1021 return schema.default;
1022 }
1023 }
1024 Private.reifyDefault = reifyDefault;
1025})(Private || (Private = {}));
1026//# sourceMappingURL=settingregistry.js.map
\No newline at end of file