1 |
|
2 |
|
3 | import { CommandRegistry } from '@lumino/commands';
|
4 | import { JSONExt } from '@lumino/coreutils';
|
5 | import { DisposableDelegate } from '@lumino/disposable';
|
6 | import { Signal } from '@lumino/signaling';
|
7 | import Ajv from 'ajv';
|
8 | import * as json5 from 'json5';
|
9 | import SCHEMA from './plugin-schema.json';
|
10 |
|
11 |
|
12 |
|
13 | const copy = JSONExt.deepCopy;
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | const DEFAULT_TRANSFORM_TIMEOUT = 1000;
|
20 |
|
21 |
|
22 |
|
23 | const RECORD_SEPARATOR = String.fromCharCode(30);
|
24 |
|
25 |
|
26 |
|
27 | export class DefaultSchemaValidator {
|
28 | |
29 |
|
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 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | validateData(plugin, populate = true) {
|
50 | const validate = this._validator.getSchema(plugin.id);
|
51 | const compose = this._composer.getSchema(plugin.id);
|
52 |
|
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 |
|
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 |
|
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 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 | _addSchema(plugin, schema) {
|
117 | const composer = this._composer;
|
118 | const validator = this._validator;
|
119 | const validate = validator.getSchema('jupyterlab-plugin-schema');
|
120 |
|
121 | if (!validate(schema)) {
|
122 | return validate.errors;
|
123 | }
|
124 |
|
125 | if (!validator.validateSchema(schema)) {
|
126 | return validator.errors;
|
127 | }
|
128 |
|
129 | composer.removeSchema(plugin);
|
130 | validator.removeSchema(plugin);
|
131 |
|
132 | composer.addSchema(schema, plugin);
|
133 | validator.addSchema(schema, plugin);
|
134 | return null;
|
135 | }
|
136 | }
|
137 |
|
138 |
|
139 |
|
140 | export class SettingRegistry {
|
141 | |
142 |
|
143 |
|
144 | constructor(options) {
|
145 | |
146 |
|
147 |
|
148 | this.schema = SCHEMA;
|
149 | |
150 |
|
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 |
|
160 | if (options.plugins) {
|
161 | this._ready = this._preload(options.plugins);
|
162 | }
|
163 | }
|
164 | |
165 |
|
166 |
|
167 | get pluginChanged() {
|
168 | return this._pluginChanged;
|
169 | }
|
170 | |
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | async get(plugin, key) {
|
180 |
|
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 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 | async load(plugin) {
|
201 |
|
202 | await this._ready;
|
203 | const plugins = this.plugins;
|
204 | const registry = this;
|
205 |
|
206 | if (plugin in plugins) {
|
207 | return new Settings({ plugin: plugins[plugin], registry });
|
208 | }
|
209 |
|
210 | return this.reload(plugin);
|
211 | }
|
212 | |
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | async reload(plugin) {
|
221 |
|
222 | await this._ready;
|
223 | const fetched = await this.connector.fetch(plugin);
|
224 | const plugins = this.plugins;
|
225 | const registry = this;
|
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 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 | async remove(plugin, key) {
|
250 |
|
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 |
|
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 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 | async set(plugin, key, value) {
|
276 |
|
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 |
|
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 |
|
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
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 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 | async upload(plugin, raw) {
|
330 |
|
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 |
|
337 | plugins[plugin].raw = raw;
|
338 | return this._save(plugin);
|
339 | }
|
340 | |
341 |
|
342 |
|
343 | async _load(data) {
|
344 | const plugin = data.id;
|
345 |
|
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 |
|
364 |
|
365 | async _preload(plugins) {
|
366 | await Promise.all(plugins.map(async (plugin) => {
|
367 | var _a;
|
368 | try {
|
369 |
|
370 | await this._load(await this._transform('fetch', plugin));
|
371 | }
|
372 | catch (errors) {
|
373 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
456 |
|
457 | async _validate(plugin) {
|
458 |
|
459 | const errors = this.validator.validateData(plugin);
|
460 | if (errors) {
|
461 | throw errors;
|
462 | }
|
463 |
|
464 | this.plugins[plugin.id] = await this._transform('compose', plugin);
|
465 | }
|
466 | }
|
467 |
|
468 |
|
469 |
|
470 | export class Settings {
|
471 | |
472 |
|
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 |
|
483 |
|
484 | get changed() {
|
485 | return this._changed;
|
486 | }
|
487 | |
488 |
|
489 |
|
490 | get composite() {
|
491 | return this.plugin.data.composite;
|
492 | }
|
493 | |
494 |
|
495 |
|
496 | get isDisposed() {
|
497 | return this._isDisposed;
|
498 | }
|
499 | get plugin() {
|
500 | return this.registry.plugins[this.id];
|
501 | }
|
502 | |
503 |
|
504 |
|
505 | get schema() {
|
506 | return this.plugin.schema;
|
507 | }
|
508 | |
509 |
|
510 |
|
511 | get raw() {
|
512 | return this.plugin.raw;
|
513 | }
|
514 | |
515 |
|
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 |
|
538 |
|
539 | get user() {
|
540 | return this.plugin.data.user;
|
541 | }
|
542 | |
543 |
|
544 |
|
545 | get version() {
|
546 | return this.plugin.version;
|
547 | }
|
548 | |
549 |
|
550 |
|
551 | annotatedDefaults() {
|
552 | return Private.annotatedDefaults(this.schema, this.id);
|
553 | }
|
554 | |
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 | default(key) {
|
562 | return Private.reifyDefault(this.schema, key);
|
563 | }
|
564 | |
565 |
|
566 |
|
567 | dispose() {
|
568 | if (this._isDisposed) {
|
569 | return;
|
570 | }
|
571 | this._isDisposed = true;
|
572 | Signal.clearData(this);
|
573 | }
|
574 | |
575 |
|
576 |
|
577 |
|
578 |
|
579 |
|
580 |
|
581 |
|
582 |
|
583 |
|
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 |
|
594 |
|
595 |
|
596 |
|
597 |
|
598 |
|
599 |
|
600 |
|
601 |
|
602 | remove(key) {
|
603 | return this.registry.remove(this.plugin.id, key);
|
604 | }
|
605 | |
606 |
|
607 |
|
608 | save(raw) {
|
609 | return this.registry.upload(this.plugin.id, raw);
|
610 | }
|
611 | |
612 |
|
613 |
|
614 |
|
615 |
|
616 |
|
617 |
|
618 |
|
619 |
|
620 |
|
621 |
|
622 |
|
623 | set(key, value) {
|
624 | return this.registry.set(this.plugin.id, key, value);
|
625 | }
|
626 | |
627 |
|
628 |
|
629 |
|
630 |
|
631 |
|
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 |
|
642 |
|
643 | _onPluginChanged(sender, plugin) {
|
644 | if (plugin === this.plugin.id) {
|
645 | this._changed.emit(undefined);
|
646 | }
|
647 | }
|
648 | }
|
649 |
|
650 |
|
651 |
|
652 | (function (SettingRegistry) {
|
653 | |
654 |
|
655 |
|
656 |
|
657 |
|
658 |
|
659 |
|
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 |
|
685 |
|
686 |
|
687 |
|
688 |
|
689 |
|
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 |
|
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 |
|
750 |
|
751 |
|
752 |
|
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 |
|
773 |
|
774 |
|
775 |
|
776 |
|
777 |
|
778 |
|
779 |
|
780 | function reconcileShortcuts(defaults, user) {
|
781 | const memo = {};
|
782 |
|
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;
|
795 | return true;
|
796 | }
|
797 | console.warn('Skipping this shortcut because it collides with another shortcut.', shortcut);
|
798 | return false;
|
799 | });
|
800 |
|
801 |
|
802 |
|
803 |
|
804 |
|
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 |
|
819 | memo[keys][selector] = !disabled;
|
820 | return true;
|
821 | }
|
822 |
|
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 |
|
829 | return (user
|
830 | .concat(defaults)
|
831 | .filter(shortcut => !shortcut.disabled)
|
832 |
|
833 | .map(shortcut => {
|
834 | return Object.assign({ args: {} }, shortcut);
|
835 | }));
|
836 | }
|
837 | SettingRegistry.reconcileShortcuts = reconcileShortcuts;
|
838 | |
839 |
|
840 |
|
841 |
|
842 |
|
843 |
|
844 |
|
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 |
|
855 | addition.forEach(item => {
|
856 |
|
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 |
|
875 |
|
876 | var Private;
|
877 | (function (Private) {
|
878 | |
879 |
|
880 |
|
881 | const indent = ' ';
|
882 | |
883 |
|
884 |
|
885 | const nondescript = '[missing schema description]';
|
886 | |
887 |
|
888 |
|
889 | const untitled = '[missing schema title]';
|
890 | |
891 |
|
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 |
|
913 |
|
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 |
|
933 |
|
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 |
|
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 |
|
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 |
|
975 |
|
976 | function prefix(source, pre = `${indent}// `) {
|
977 | return pre + source.split('\n').join(`\n${pre}`);
|
978 | }
|
979 | |
980 |
|
981 |
|
982 | function reifyDefault(schema, root) {
|
983 | var _a, _b, _c;
|
984 | const definitions = schema.definitions;
|
985 |
|
986 | schema = (root ? (_a = schema.properties) === null || _a === void 0 ? void 0 : _a[root] : schema) || {};
|
987 | if (schema.type === 'object') {
|
988 |
|
989 | const result = JSONExt.deepCopy(schema.default);
|
990 |
|
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 |
|
999 | const result = JSONExt.deepCopy(schema.default);
|
1000 |
|
1001 | let props = schema.items || {};
|
1002 |
|
1003 | if (props['$ref'] && definitions) {
|
1004 | const ref = props['$ref'].replace('#/definitions/', '');
|
1005 | props = (_b = definitions[ref]) !== null && _b !== void 0 ? _b : {};
|
1006 | }
|
1007 |
|
1008 | for (const item in result) {
|
1009 |
|
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 |
|
\ | No newline at end of file |