loader.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2018-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 A dynamic loader for loading MathJax components based
  19. * on a user configuration, while handling timing of
  20. * dependencies properly
  21. *
  22. * @author dpvc@mathjax.org (Davide Cervone)
  23. */
  24. import {MathJax as MJGlobal, MathJaxObject as MJObject, MathJaxLibrary,
  25. MathJaxConfig as MJConfig, combineWithMathJax, combineDefaults} from './global.js';
  26. import {Package, PackageError, PackageReady, PackageFailed} from './package.js';
  27. export {Package, PackageError, PackageReady, PackageFailed} from './package.js';
  28. export {MathJaxLibrary} from './global.js';
  29. import {FunctionList} from '../util/FunctionList.js';
  30. /*
  31. * The current directory (for webpack), and the browser document (if any)
  32. */
  33. declare var __dirname: string;
  34. declare var document: Document;
  35. /**
  36. * Function used to determine path to a given package.
  37. */
  38. export type PathFilterFunction = (data: {name: string, original: string, addExtension: boolean}) => boolean;
  39. export type PathFilterList = (PathFilterFunction | [PathFilterFunction, number])[];
  40. /**
  41. * Update the configuration structure to include the loader configuration
  42. */
  43. export interface MathJaxConfig extends MJConfig {
  44. loader?: {
  45. paths?: {[name: string]: string}; // The path prefixes for use in locations
  46. source?: {[name: string]: string}; // The URLs for the extensions, e.g., tex: [mathjax]/input/tex.js
  47. dependencies?: {[name: string]: string[]}; // The dependencies for each package
  48. provides?: {[name: string]: string[]}; // The sub-packages provided by each package
  49. load?: string[]; // The packages to load (found in locations or [mathjax]/name])
  50. ready?: PackageReady; // A function to call when MathJax is ready
  51. failed?: PackageFailed; // A function to call when MathJax fails to load
  52. require?: (url: string) => any; // A function for loading URLs
  53. pathFilters?: PathFilterList; // List of path filters (and optional priorities) to add
  54. versionWarnings?: boolean; // True means warn when extension version doesn't match MJ version
  55. [name: string]: any; // Other configuration blocks
  56. };
  57. }
  58. /**
  59. * Update the MathJax object to inclide the loader information
  60. */
  61. export interface MathJaxObject extends MJObject {
  62. _: MathJaxLibrary;
  63. config: MathJaxConfig;
  64. loader: {
  65. ready: (...names: string[]) => Promise<string[]>; // Get a promise for when all the named packages are loaded
  66. load: (...names: string[]) => Promise<string>; // Load the packages and return a promise for when ready
  67. preLoad: (...names: string[]) => void; // Indicate that packages are already loaded by hand
  68. defaultReady: () => void; // The function performed when all packages are loaded
  69. getRoot: () => string; // Find the root URL for the MathJax files
  70. checkVersion: (name: string, version: string) => boolean; // Check the version of an extension
  71. pathFilters: FunctionList; // the filters to use for looking for package paths
  72. };
  73. startup?: any;
  74. }
  75. /**
  76. * Functions used to filter the path to a package
  77. */
  78. export const PathFilters: {[name: string]: PathFilterFunction} = {
  79. /**
  80. * Look up the path in the configuration's source list
  81. */
  82. source: (data) => {
  83. if (CONFIG.source.hasOwnProperty(data.name)) {
  84. data.name = CONFIG.source[data.name];
  85. }
  86. return true;
  87. },
  88. /**
  89. * Add [mathjax] before any relative path, and add .js if needed
  90. */
  91. normalize: (data) => {
  92. const name = data.name;
  93. if (!name.match(/^(?:[a-z]+:\/)?\/|[a-z]:\\|\[/i)) {
  94. data.name = '[mathjax]/' + name.replace(/^\.\//, '');
  95. }
  96. if (data.addExtension && !name.match(/\.[^\/]+$/)) {
  97. data.name += '.js';
  98. }
  99. return true;
  100. },
  101. /**
  102. * Recursively replace path prefixes (e.g., [mathjax], [tex], etc.)
  103. */
  104. prefix: (data) => {
  105. let match;
  106. while ((match = data.name.match(/^\[([^\]]*)\]/))) {
  107. if (!CONFIG.paths.hasOwnProperty(match[1])) break;
  108. data.name = CONFIG.paths[match[1]] + data.name.substr(match[0].length);
  109. }
  110. return true;
  111. }
  112. };
  113. /**
  114. * The implementation of the dynamic loader
  115. */
  116. export namespace Loader {
  117. /**
  118. * The version of MathJax that is running.
  119. */
  120. const VERSION = MJGlobal.version;
  121. /**
  122. * The versions of all the loaded extensions.
  123. */
  124. export const versions: Map<string, string> = new Map();
  125. /**
  126. * Get a promise that is resolved when all the named packages have been loaded.
  127. *
  128. * @param {string[]} names The packages to wait for
  129. * @returns {Promise} A promise that resolves when all the named packages are ready
  130. */
  131. export function ready(...names: string[]): Promise<string[]> {
  132. if (names.length === 0) {
  133. names = Array.from(Package.packages.keys());
  134. }
  135. const promises = [];
  136. for (const name of names) {
  137. const extension = Package.packages.get(name) || new Package(name, true);
  138. promises.push(extension.promise);
  139. }
  140. return Promise.all(promises);
  141. }
  142. /**
  143. * Load the named packages and return a promise that is resolved when they are all loaded
  144. *
  145. * @param {string[]} names The packages to load
  146. * @returns {Promise} A promise that resolves when all the named packages are ready
  147. */
  148. export function load(...names: string[]): Promise<void | string[]> {
  149. if (names.length === 0) {
  150. return Promise.resolve();
  151. }
  152. const promises = [];
  153. for (const name of names) {
  154. let extension = Package.packages.get(name);
  155. if (!extension) {
  156. extension = new Package(name);
  157. extension.provides(CONFIG.provides[name]);
  158. }
  159. extension.checkNoLoad();
  160. promises.push(extension.promise.then(() => {
  161. if (!CONFIG.versionWarnings) return;
  162. if (extension.isLoaded && !versions.has(Package.resolvePath(name))) {
  163. console.warn(`No version information available for component ${name}`);
  164. }
  165. }) as Promise<null>);
  166. }
  167. Package.loadAll();
  168. return Promise.all(promises);
  169. }
  170. /**
  171. * Indicate that the named packages are being loaded by hand (e.g., as part of a larger package).
  172. *
  173. * @param {string[]} names The packages to load
  174. */
  175. export function preLoad(...names: string[]) {
  176. for (const name of names) {
  177. let extension = Package.packages.get(name);
  178. if (!extension) {
  179. extension = new Package(name, true);
  180. extension.provides(CONFIG.provides[name]);
  181. }
  182. extension.loaded();
  183. }
  184. }
  185. /**
  186. * The default function to perform when all the packages are loaded
  187. */
  188. export function defaultReady() {
  189. if (typeof MathJax.startup !== 'undefined') {
  190. MathJax.config.startup.ready();
  191. }
  192. }
  193. /**
  194. * Get the root location for where the MathJax package files are found
  195. *
  196. * @returns {string} The root location (directory for node.js, URL for browser)
  197. */
  198. export function getRoot(): string {
  199. let root = __dirname + '/../../es5';
  200. if (typeof document !== 'undefined') {
  201. const script = document.currentScript || document.getElementById('MathJax-script');
  202. if (script) {
  203. root = (script as HTMLScriptElement).src.replace(/\/[^\/]*$/, '');
  204. }
  205. }
  206. return root;
  207. }
  208. /**
  209. * Check the version of an extension and report an error if not correct
  210. *
  211. * @param {string} name The name of the extension being checked
  212. * @param {string} version The version of the extension to check
  213. * @param {string} type The type of extension (future code may use this to check ranges of versions)
  214. * @return {boolean} True if there was a mismatch, false otherwise
  215. */
  216. export function checkVersion(name: string, version: string, _type?: string): boolean {
  217. versions.set(Package.resolvePath(name), VERSION);
  218. if (CONFIG.versionWarnings && version !== VERSION) {
  219. console.warn(`Component ${name} uses ${version} of MathJax; version in use is ${VERSION}`);
  220. return true;
  221. }
  222. return false;
  223. }
  224. /**
  225. * The filters to use to modify the paths used to obtain the packages
  226. */
  227. export const pathFilters = new FunctionList();
  228. /**
  229. * The default filters to use.
  230. */
  231. pathFilters.add(PathFilters.source, 0);
  232. pathFilters.add(PathFilters.normalize, 10);
  233. pathFilters.add(PathFilters.prefix, 20);
  234. }
  235. /**
  236. * Export the global MathJax object for convenience
  237. */
  238. export const MathJax = MJGlobal as MathJaxObject;
  239. /*
  240. * If the loader hasn't been added to the MathJax variable,
  241. * Add the loader configuration, library, and data objects.
  242. * Add any path filters from the configuration.
  243. */
  244. if (typeof MathJax.loader === 'undefined') {
  245. combineDefaults(MathJax.config, 'loader', {
  246. paths: {
  247. mathjax: Loader.getRoot()
  248. },
  249. source: {},
  250. dependencies: {},
  251. provides: {},
  252. load: [],
  253. ready: Loader.defaultReady.bind(Loader),
  254. failed: (error: PackageError) => console.log(`MathJax(${error.package || '?'}): ${error.message}`),
  255. require: null,
  256. pathFilters: [],
  257. versionWarnings: true
  258. });
  259. combineWithMathJax({
  260. loader: Loader
  261. });
  262. //
  263. // Add any path filters from the configuration
  264. //
  265. for (const filter of MathJax.config.loader.pathFilters) {
  266. if (Array.isArray(filter)) {
  267. Loader.pathFilters.add(filter[0], filter[1]);
  268. } else {
  269. Loader.pathFilters.add(filter);
  270. }
  271. }
  272. }
  273. /**
  274. * Export the loader configuration for convenience
  275. */
  276. export const CONFIG = MathJax.config.loader;