Options.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2017-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Implements functions for handling option lists
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. /*****************************************************************/
  23. /* tslint:disable-next-line:jsdoc-require */
  24. const OBJECT = {}.constructor;
  25. /**
  26. * Check if an object is an object literal (as opposed to an instance of a class)
  27. */
  28. export function isObject(obj: any) {
  29. return typeof obj === 'object' && obj !== null &&
  30. (obj.constructor === OBJECT || obj.constructor === Expandable);
  31. }
  32. /*****************************************************************/
  33. /**
  34. * Generic list of options
  35. */
  36. export type OptionList = {[name: string]: any};
  37. /*****************************************************************/
  38. /**
  39. * Used to append an array to an array in default options
  40. * E.g., an option of the form
  41. *
  42. * {
  43. * name: {[APPEND]: [1, 2, 3]}
  44. * }
  45. *
  46. * where 'name' is an array in the default options would end up with name having its
  47. * original value with 1, 2, and 3 appended.
  48. */
  49. export const APPEND = '[+]';
  50. /**
  51. * Used to remove elements from an array in default options
  52. * E.g., an option of the form
  53. *
  54. * {
  55. * name: {[REMOVE]: [2]}
  56. * }
  57. *
  58. * where 'name' is an array in the default options would end up with name having its
  59. * original value but with any entry of 2 removed So if the original value was [1, 2, 3, 2],
  60. * then the final value will be [1, 3] instead.
  61. */
  62. export const REMOVE = '[-]';
  63. /**
  64. * Provides options for the option utlities.
  65. */
  66. export const OPTIONS = {
  67. invalidOption: 'warn' as ('fatal' | 'warn'),
  68. /**
  69. * Function to report messages for invalid options
  70. *
  71. * @param {string} message The message for the invalid parameter.
  72. * @param {string} key The invalid key itself.
  73. */
  74. optionError: (message: string, _key: string) => {
  75. if (OPTIONS.invalidOption === 'fatal') {
  76. throw new Error(message);
  77. }
  78. console.warn('MathJax: ' + message);
  79. }
  80. };
  81. /**
  82. * A Class to use for options that should not produce warnings if an undefined key is used
  83. */
  84. export class Expandable {}
  85. /**
  86. * Produces an instance of Expandable with the given values (to be used in defining options
  87. * that can use keys that don't have default values). E.g., default options of the form:
  88. *
  89. * OPTIONS = {
  90. * types: expandable({
  91. * a: 1,
  92. * b: 2
  93. * })
  94. * }
  95. *
  96. * would allow user options of
  97. *
  98. * {
  99. * types: {
  100. * c: 3
  101. * }
  102. * }
  103. *
  104. * without reporting an error.
  105. */
  106. export function expandable(def: OptionList) {
  107. return Object.assign(Object.create(Expandable.prototype), def);
  108. }
  109. /*****************************************************************/
  110. /**
  111. * Make sure an option is an Array
  112. */
  113. export function makeArray(x: any): any[] {
  114. return Array.isArray(x) ? x : [x];
  115. }
  116. /*****************************************************************/
  117. /**
  118. * Get all keys and symbols from an object
  119. *
  120. * @param {Optionlist} def The object whose keys are to be returned
  121. * @return {(string | symbol)[]} The list of keys for the object
  122. */
  123. export function keys(def: OptionList): (string | symbol)[] {
  124. if (!def) {
  125. return [];
  126. }
  127. return (Object.keys(def) as (string | symbol)[]).concat(Object.getOwnPropertySymbols(def));
  128. }
  129. /*****************************************************************/
  130. /**
  131. * Make a deep copy of an object
  132. *
  133. * @param {OptionList} def The object to be copied
  134. * @return {OptionList} The copy of the object
  135. */
  136. export function copy(def: OptionList): OptionList {
  137. let props: OptionList = {};
  138. for (const key of keys(def)) {
  139. let prop = Object.getOwnPropertyDescriptor(def, key);
  140. let value = prop.value;
  141. if (Array.isArray(value)) {
  142. prop.value = insert([], value, false);
  143. } else if (isObject(value)) {
  144. prop.value = copy(value);
  145. }
  146. if (prop.enumerable) {
  147. props[key as string] = prop;
  148. }
  149. }
  150. return Object.defineProperties(def.constructor === Expandable ? expandable({}) : {}, props);
  151. }
  152. /*****************************************************************/
  153. /**
  154. * Insert one object into another (with optional warnings about
  155. * keys that aren't in the original)
  156. *
  157. * @param {OptionList} dst The option list to merge into
  158. * @param {OptionList} src The options to be merged
  159. * @param {boolean} warn True if a warning should be issued for a src option that isn't already in dst
  160. * @return {OptionList} The modified destination option list (dst)
  161. */
  162. export function insert(dst: OptionList, src: OptionList, warn: boolean = true): OptionList {
  163. for (let key of keys(src) as string[]) {
  164. //
  165. // Check if the key is valid (i.e., is in the defaults or in an expandable block)
  166. //
  167. if (warn && dst[key] === undefined && dst.constructor !== Expandable) {
  168. if (typeof key === 'symbol') {
  169. key = (key as symbol).toString();
  170. }
  171. OPTIONS.optionError(`Invalid option "${key}" (no default value).`, key);
  172. continue;
  173. }
  174. //
  175. // Shorthands for the source and destination values
  176. //
  177. let sval = src[key], dval = dst[key];
  178. //
  179. // If the source is an object literal and the destination exists and is either an
  180. // object or a function (so can have properties added to it)...
  181. //
  182. if (isObject(sval) && dval !== null &&
  183. (typeof dval === 'object' || typeof dval === 'function')) {
  184. const ids = keys(sval);
  185. //
  186. // Check for APPEND or REMOVE objects:
  187. //
  188. if (
  189. //
  190. // If the destination value is an array...
  191. //
  192. Array.isArray(dval) &&
  193. (
  194. //
  195. // If there is only one key and it is APPEND or REMOVE and the keys value is an array...
  196. //
  197. (ids.length === 1 && (ids[0] === APPEND || ids[0] === REMOVE) && Array.isArray(sval[ids[0]])) ||
  198. //
  199. // Or if there are two keys and they are APPEND and REMOVE and both keys' values
  200. // are arrays...
  201. //
  202. (ids.length === 2 && ids.sort().join(',') === APPEND + ',' + REMOVE &&
  203. Array.isArray(sval[APPEND]) && Array.isArray(sval[REMOVE]))
  204. )
  205. ) {
  206. //
  207. // Then remove any values to be removed
  208. //
  209. if (sval[REMOVE]) {
  210. dval = dst[key] = dval.filter(x => sval[REMOVE].indexOf(x) < 0);
  211. }
  212. //
  213. // And append any values to be added (make a copy so as not to modify the original)
  214. //
  215. if (sval[APPEND]) {
  216. dst[key] = [...dval, ...sval[APPEND]];
  217. }
  218. } else {
  219. //
  220. // Otherwise insert the values of the source object into the destination object
  221. //
  222. insert(dval, sval, warn);
  223. }
  224. } else if (Array.isArray(sval)) {
  225. //
  226. // If the source is an array, replace the destination with an empty array
  227. // and copy the source values into it.
  228. //
  229. dst[key] = [];
  230. insert(dst[key], sval, false);
  231. } else if (isObject(sval)) {
  232. //
  233. // If the source is an object literal, set the destination to a copy of it
  234. //
  235. dst[key] = copy(sval);
  236. } else {
  237. //
  238. // Otherwise set the destination to the source value
  239. //
  240. dst[key] = sval;
  241. }
  242. }
  243. return dst;
  244. }
  245. /*****************************************************************/
  246. /**
  247. * Merge options without warnings (so we can add new default values into an
  248. * existing default list)
  249. *
  250. * @param {OptionList} options The option list to be merged into
  251. * @param {OptionList[]} defs The option lists to merge into the first one
  252. * @return {OptionList} The modified options list
  253. */
  254. export function defaultOptions(options: OptionList, ...defs: OptionList[]): OptionList {
  255. defs.forEach(def => insert(options, def, false));
  256. return options;
  257. }
  258. /*****************************************************************/
  259. /**
  260. * Merge options with warnings about undefined ones (so we can merge
  261. * user options into the default list)
  262. *
  263. * @param {OptionList} options The option list to be merged into
  264. * @param {OptionList[]} defs The option lists to merge into the first one
  265. * @return {OptionList} The modified options list
  266. */
  267. export function userOptions(options: OptionList, ...defs: OptionList[]): OptionList {
  268. defs.forEach(def => insert(options, def, true));
  269. return options;
  270. }
  271. /*****************************************************************/
  272. /**
  273. * Select a subset of options by key name
  274. *
  275. * @param {OptionList} options The option list from which option values will be taken
  276. * @param {string[]} keys The names of the options to extract
  277. * @return {OptionList} The option list consisting of only the ones whose keys were given
  278. */
  279. export function selectOptions(options: OptionList, ...keys: string[]): OptionList {
  280. let subset: OptionList = {};
  281. for (const key of keys) {
  282. if (options.hasOwnProperty(key)) {
  283. subset[key] = options[key];
  284. }
  285. }
  286. return subset;
  287. }
  288. /*****************************************************************/
  289. /**
  290. * Select a subset of options by keys from an object
  291. *
  292. * @param {OptionList} options The option list from which the option values will be taken
  293. * @param {OptionList} object The option list whose keys will be used to select the options
  294. * @return {OptionList} The option list consisting of the option values from the first
  295. * list whose keys are those from the second list.
  296. */
  297. export function selectOptionsFromKeys(options: OptionList, object: OptionList): OptionList {
  298. return selectOptions(options, ...Object.keys(object));
  299. }
  300. /*****************************************************************/
  301. /**
  302. * Separate options into sets: the ones having the same keys
  303. * as the second object, the third object, etc, and the ones that don't.
  304. * (Used to separate an option list into the options needed for several
  305. * subobjects.)
  306. *
  307. * @param {OptionList} options The option list to be split into parts
  308. * @param {OptionList[]} objects The list of option lists whose keys are used to break up
  309. * the original options into separate pieces.
  310. * @return {OptionList[]} The option lists taken from the original based on the
  311. * keys of the other objects. The first one in the list
  312. * consists of the values not appearing in any of the others
  313. * (i.e., whose keys were not in any of the others).
  314. */
  315. export function separateOptions(options: OptionList, ...objects: OptionList[]): OptionList[] {
  316. let results: OptionList[] = [];
  317. for (const object of objects) {
  318. let exists: OptionList = {}, missing: OptionList = {};
  319. for (const key of Object.keys(options || {})) {
  320. (object[key] === undefined ? missing : exists)[key] = options[key];
  321. }
  322. results.push(exists);
  323. options = missing;
  324. }
  325. results.unshift(options);
  326. return results;
  327. }
  328. /*****************************************************************/
  329. /**
  330. * Look up a value from object literal, being sure it is an
  331. * actual property (not inherited), with a default if not found.
  332. *
  333. * @param {string} name The name of the key to look up.
  334. * @param {OptionList} lookup The list of options to check.
  335. * @param {any} def The default value if the key isn't found.
  336. */
  337. export function lookup(name: string, lookup: OptionList, def: any = null) {
  338. return (lookup.hasOwnProperty(name) ? lookup[name] : def);
  339. }