364 lines
12 KiB
TypeScript
364 lines
12 KiB
TypeScript
/*************************************************************
|
|
*
|
|
* Copyright (c) 2017-2022 The MathJax Consortium
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Implements functions for handling option lists
|
|
*
|
|
* @author dpvc@mathjax.org (Davide Cervone)
|
|
*/
|
|
|
|
|
|
/*****************************************************************/
|
|
/* tslint:disable-next-line:jsdoc-require */
|
|
const OBJECT = {}.constructor;
|
|
|
|
/**
|
|
* Check if an object is an object literal (as opposed to an instance of a class)
|
|
*/
|
|
export function isObject(obj: any) {
|
|
return typeof obj === 'object' && obj !== null &&
|
|
(obj.constructor === OBJECT || obj.constructor === Expandable);
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Generic list of options
|
|
*/
|
|
export type OptionList = {[name: string]: any};
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Used to append an array to an array in default options
|
|
* E.g., an option of the form
|
|
*
|
|
* {
|
|
* name: {[APPEND]: [1, 2, 3]}
|
|
* }
|
|
*
|
|
* where 'name' is an array in the default options would end up with name having its
|
|
* original value with 1, 2, and 3 appended.
|
|
*/
|
|
export const APPEND = '[+]';
|
|
|
|
/**
|
|
* Used to remove elements from an array in default options
|
|
* E.g., an option of the form
|
|
*
|
|
* {
|
|
* name: {[REMOVE]: [2]}
|
|
* }
|
|
*
|
|
* where 'name' is an array in the default options would end up with name having its
|
|
* original value but with any entry of 2 removed So if the original value was [1, 2, 3, 2],
|
|
* then the final value will be [1, 3] instead.
|
|
*/
|
|
export const REMOVE = '[-]';
|
|
|
|
|
|
/**
|
|
* Provides options for the option utlities.
|
|
*/
|
|
export const OPTIONS = {
|
|
invalidOption: 'warn' as ('fatal' | 'warn'),
|
|
/**
|
|
* Function to report messages for invalid options
|
|
*
|
|
* @param {string} message The message for the invalid parameter.
|
|
* @param {string} key The invalid key itself.
|
|
*/
|
|
optionError: (message: string, _key: string) => {
|
|
if (OPTIONS.invalidOption === 'fatal') {
|
|
throw new Error(message);
|
|
}
|
|
console.warn('MathJax: ' + message);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* A Class to use for options that should not produce warnings if an undefined key is used
|
|
*/
|
|
export class Expandable {}
|
|
|
|
/**
|
|
* Produces an instance of Expandable with the given values (to be used in defining options
|
|
* that can use keys that don't have default values). E.g., default options of the form:
|
|
*
|
|
* OPTIONS = {
|
|
* types: expandable({
|
|
* a: 1,
|
|
* b: 2
|
|
* })
|
|
* }
|
|
*
|
|
* would allow user options of
|
|
*
|
|
* {
|
|
* types: {
|
|
* c: 3
|
|
* }
|
|
* }
|
|
*
|
|
* without reporting an error.
|
|
*/
|
|
export function expandable(def: OptionList) {
|
|
return Object.assign(Object.create(Expandable.prototype), def);
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Make sure an option is an Array
|
|
*/
|
|
export function makeArray(x: any): any[] {
|
|
return Array.isArray(x) ? x : [x];
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Get all keys and symbols from an object
|
|
*
|
|
* @param {Optionlist} def The object whose keys are to be returned
|
|
* @return {(string | symbol)[]} The list of keys for the object
|
|
*/
|
|
export function keys(def: OptionList): (string | symbol)[] {
|
|
if (!def) {
|
|
return [];
|
|
}
|
|
return (Object.keys(def) as (string | symbol)[]).concat(Object.getOwnPropertySymbols(def));
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Make a deep copy of an object
|
|
*
|
|
* @param {OptionList} def The object to be copied
|
|
* @return {OptionList} The copy of the object
|
|
*/
|
|
export function copy(def: OptionList): OptionList {
|
|
let props: OptionList = {};
|
|
for (const key of keys(def)) {
|
|
let prop = Object.getOwnPropertyDescriptor(def, key);
|
|
let value = prop.value;
|
|
if (Array.isArray(value)) {
|
|
prop.value = insert([], value, false);
|
|
} else if (isObject(value)) {
|
|
prop.value = copy(value);
|
|
}
|
|
if (prop.enumerable) {
|
|
props[key as string] = prop;
|
|
}
|
|
}
|
|
return Object.defineProperties(def.constructor === Expandable ? expandable({}) : {}, props);
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Insert one object into another (with optional warnings about
|
|
* keys that aren't in the original)
|
|
*
|
|
* @param {OptionList} dst The option list to merge into
|
|
* @param {OptionList} src The options to be merged
|
|
* @param {boolean} warn True if a warning should be issued for a src option that isn't already in dst
|
|
* @return {OptionList} The modified destination option list (dst)
|
|
*/
|
|
export function insert(dst: OptionList, src: OptionList, warn: boolean = true): OptionList {
|
|
for (let key of keys(src) as string[]) {
|
|
//
|
|
// Check if the key is valid (i.e., is in the defaults or in an expandable block)
|
|
//
|
|
if (warn && dst[key] === undefined && dst.constructor !== Expandable) {
|
|
if (typeof key === 'symbol') {
|
|
key = (key as symbol).toString();
|
|
}
|
|
OPTIONS.optionError(`Invalid option "${key}" (no default value).`, key);
|
|
continue;
|
|
}
|
|
//
|
|
// Shorthands for the source and destination values
|
|
//
|
|
let sval = src[key], dval = dst[key];
|
|
//
|
|
// If the source is an object literal and the destination exists and is either an
|
|
// object or a function (so can have properties added to it)...
|
|
//
|
|
if (isObject(sval) && dval !== null &&
|
|
(typeof dval === 'object' || typeof dval === 'function')) {
|
|
const ids = keys(sval);
|
|
//
|
|
// Check for APPEND or REMOVE objects:
|
|
//
|
|
if (
|
|
//
|
|
// If the destination value is an array...
|
|
//
|
|
Array.isArray(dval) &&
|
|
(
|
|
//
|
|
// If there is only one key and it is APPEND or REMOVE and the keys value is an array...
|
|
//
|
|
(ids.length === 1 && (ids[0] === APPEND || ids[0] === REMOVE) && Array.isArray(sval[ids[0]])) ||
|
|
//
|
|
// Or if there are two keys and they are APPEND and REMOVE and both keys' values
|
|
// are arrays...
|
|
//
|
|
(ids.length === 2 && ids.sort().join(',') === APPEND + ',' + REMOVE &&
|
|
Array.isArray(sval[APPEND]) && Array.isArray(sval[REMOVE]))
|
|
)
|
|
) {
|
|
//
|
|
// Then remove any values to be removed
|
|
//
|
|
if (sval[REMOVE]) {
|
|
dval = dst[key] = dval.filter(x => sval[REMOVE].indexOf(x) < 0);
|
|
}
|
|
//
|
|
// And append any values to be added (make a copy so as not to modify the original)
|
|
//
|
|
if (sval[APPEND]) {
|
|
dst[key] = [...dval, ...sval[APPEND]];
|
|
}
|
|
} else {
|
|
//
|
|
// Otherwise insert the values of the source object into the destination object
|
|
//
|
|
insert(dval, sval, warn);
|
|
}
|
|
} else if (Array.isArray(sval)) {
|
|
//
|
|
// If the source is an array, replace the destination with an empty array
|
|
// and copy the source values into it.
|
|
//
|
|
dst[key] = [];
|
|
insert(dst[key], sval, false);
|
|
} else if (isObject(sval)) {
|
|
//
|
|
// If the source is an object literal, set the destination to a copy of it
|
|
//
|
|
dst[key] = copy(sval);
|
|
} else {
|
|
//
|
|
// Otherwise set the destination to the source value
|
|
//
|
|
dst[key] = sval;
|
|
}
|
|
}
|
|
return dst;
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Merge options without warnings (so we can add new default values into an
|
|
* existing default list)
|
|
*
|
|
* @param {OptionList} options The option list to be merged into
|
|
* @param {OptionList[]} defs The option lists to merge into the first one
|
|
* @return {OptionList} The modified options list
|
|
*/
|
|
export function defaultOptions(options: OptionList, ...defs: OptionList[]): OptionList {
|
|
defs.forEach(def => insert(options, def, false));
|
|
return options;
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Merge options with warnings about undefined ones (so we can merge
|
|
* user options into the default list)
|
|
*
|
|
* @param {OptionList} options The option list to be merged into
|
|
* @param {OptionList[]} defs The option lists to merge into the first one
|
|
* @return {OptionList} The modified options list
|
|
*/
|
|
export function userOptions(options: OptionList, ...defs: OptionList[]): OptionList {
|
|
defs.forEach(def => insert(options, def, true));
|
|
return options;
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Select a subset of options by key name
|
|
*
|
|
* @param {OptionList} options The option list from which option values will be taken
|
|
* @param {string[]} keys The names of the options to extract
|
|
* @return {OptionList} The option list consisting of only the ones whose keys were given
|
|
*/
|
|
export function selectOptions(options: OptionList, ...keys: string[]): OptionList {
|
|
let subset: OptionList = {};
|
|
for (const key of keys) {
|
|
if (options.hasOwnProperty(key)) {
|
|
subset[key] = options[key];
|
|
}
|
|
}
|
|
return subset;
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Select a subset of options by keys from an object
|
|
*
|
|
* @param {OptionList} options The option list from which the option values will be taken
|
|
* @param {OptionList} object The option list whose keys will be used to select the options
|
|
* @return {OptionList} The option list consisting of the option values from the first
|
|
* list whose keys are those from the second list.
|
|
*/
|
|
export function selectOptionsFromKeys(options: OptionList, object: OptionList): OptionList {
|
|
return selectOptions(options, ...Object.keys(object));
|
|
}
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Separate options into sets: the ones having the same keys
|
|
* as the second object, the third object, etc, and the ones that don't.
|
|
* (Used to separate an option list into the options needed for several
|
|
* subobjects.)
|
|
*
|
|
* @param {OptionList} options The option list to be split into parts
|
|
* @param {OptionList[]} objects The list of option lists whose keys are used to break up
|
|
* the original options into separate pieces.
|
|
* @return {OptionList[]} The option lists taken from the original based on the
|
|
* keys of the other objects. The first one in the list
|
|
* consists of the values not appearing in any of the others
|
|
* (i.e., whose keys were not in any of the others).
|
|
*/
|
|
export function separateOptions(options: OptionList, ...objects: OptionList[]): OptionList[] {
|
|
let results: OptionList[] = [];
|
|
for (const object of objects) {
|
|
let exists: OptionList = {}, missing: OptionList = {};
|
|
for (const key of Object.keys(options || {})) {
|
|
(object[key] === undefined ? missing : exists)[key] = options[key];
|
|
}
|
|
results.push(exists);
|
|
options = missing;
|
|
}
|
|
results.unshift(options);
|
|
return results;
|
|
}
|
|
|
|
|
|
/*****************************************************************/
|
|
/**
|
|
* Look up a value from object literal, being sure it is an
|
|
* actual property (not inherited), with a default if not found.
|
|
*
|
|
* @param {string} name The name of the key to look up.
|
|
* @param {OptionList} lookup The list of options to check.
|
|
* @param {any} def The default value if the key isn't found.
|
|
*/
|
|
export function lookup(name: string, lookup: OptionList, def: any = null) {
|
|
return (lookup.hasOwnProperty(name) ? lookup[name] : def);
|
|
}
|
|
|