123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- /*************************************************************
- *
- * 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);
- }
|