latest.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2019-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. * The data for a CDN
  19. */
  20. type CdnData = {
  21. api: string, // URL for JSON containing version number
  22. key: string, // key for versionb string in JSON data
  23. base?: string // base URL for MathJax on the CDN (version is appended to get actual URL)
  24. };
  25. /**
  26. * A map of server names to CDN data
  27. */
  28. type CdnList = Map<string, CdnData>;
  29. /**
  30. * The data from a script tag for latest.js
  31. */
  32. type ScriptData = {
  33. tag: HTMLScriptElement, // the script DOM element
  34. src: string, // the script's (possibly modified) source attribute
  35. id: string, // the script's (possibly empty) id string
  36. version: string, // the MathJax version where latest.js was loaded
  37. dir: string, // the subdirectory where latest.js was loaded from (e.g., /es5)
  38. file: string, // the file to be loaded by latest.js
  39. cdn: CdnData // the CDN where latest.js was loaded
  40. } | null;
  41. /**
  42. * Add XMLHttpRequest and ActiveXObject (for IE)
  43. */
  44. declare const window: {
  45. XMLHttpRequest: XMLHttpRequest;
  46. ActiveXObject: any;
  47. };
  48. /*=====================================================================*/
  49. /**
  50. * The various CDNs and their data for how to obtain versions
  51. */
  52. const CDN: CdnList = new Map([
  53. ['cdnjs.cloudflare.com', {
  54. api: 'https://api.cdnjs.com/libraries/mathjax?fields=version',
  55. key: 'version',
  56. base: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/'
  57. }],
  58. ['rawcdn.githack.com', {
  59. api: 'https://api.github.com/repos/mathjax/mathjax/releases/latest',
  60. key: 'tag_name',
  61. base: 'https://rawcdn.githack.com/mathjax/MathJax/'
  62. }],
  63. ['gitcdn.xyz', {
  64. api: 'https://api.github.com/repos/mathjax/mathjax/releases/latest',
  65. key: 'tag_name',
  66. base: 'https://gitcdn.xyz/mathjax/MathJax/'
  67. }],
  68. ['cdn.statically.io', {
  69. api: 'https://api.github.com/repos/mathjax/mathjax/releases/latest',
  70. key: 'tag_name',
  71. base: 'https://cdn.statically.io/gh/mathjax/MathJax/'
  72. }],
  73. ['unpkg.com', {
  74. api: 'https://api.github.com/repos/mathjax/mathjax/releases/latest',
  75. key: 'tag_name',
  76. base: 'https://unpkg.com/mathjax@'
  77. }],
  78. ['cdn.jsdelivr.net', {
  79. api: 'https://api.github.com/repos/mathjax/mathjax/releases/latest',
  80. key: 'tag_name',
  81. base: 'https://cdn.jsdelivr.net/npm/mathjax@'
  82. }]
  83. ]);
  84. /**
  85. * The data for getting release versions from GitHub
  86. */
  87. const GITHUB: CdnData = {
  88. api: 'https://api.github.com/repos/mathjax/mathjax/releases',
  89. key: 'tag_name'
  90. };
  91. /**
  92. * The major version number for MathJax (we will load the highest version with this initial number)
  93. */
  94. const MJX_VERSION = 3;
  95. /**
  96. * The name to use for the version in localStorage
  97. */
  98. const MJX_LATEST = 'mjx-latest-version';
  99. /**
  100. * The amount of time a cached version number is valid
  101. */
  102. const SAVE_TIME = 1000 * 60 * 60 * 24 * 7; // one week
  103. /**
  104. * Data for the script that loaded latest.js
  105. */
  106. let script: ScriptData = null;
  107. /*=====================================================================*/
  108. /**
  109. * Produce an error message on the console
  110. *
  111. * @param {string} message The error message to display
  112. */
  113. function Error(message: string) {
  114. if (console && console.error) {
  115. console.error('MathJax(latest.js): ' + message);
  116. }
  117. }
  118. /**
  119. * Create a ScriptData object from the given script tag and CDN
  120. *
  121. * @param {HTMLScriptElement} script The script tag whose data is desired
  122. * @param {CdnData} cdn The CDN data already obtained for the script (or null)
  123. * @return {ScriptData} The data for the given script
  124. */
  125. function scriptData(script: HTMLScriptElement, cdn: CdnData = null): ScriptData {
  126. script.parentNode.removeChild(script);
  127. let src = script.src;
  128. let file = src.replace(/.*?\/latest\.js(\?|$)/, '');
  129. if (file === '') {
  130. file = 'startup.js';
  131. src = src.replace(/\?$/, '') + '?' + file;
  132. }
  133. const version = (src.match(/(\d+\.\d+\.\d+)(\/es\d+)?\/latest.js\?/) || ['', ''])[1];
  134. const dir = (src.match(/(\/es\d+)\/latest.js\?/) || ['', ''])[1] || '';
  135. return {
  136. tag: script,
  137. src: src,
  138. id: script.id,
  139. version: version,
  140. dir: dir,
  141. file: file,
  142. cdn: cdn
  143. };
  144. }
  145. /**
  146. * Check if a script refers to MathJax on one of the CDNs
  147. *
  148. * @param {HTMLScriptElement} script The script tag to check
  149. * @return {ScriptData | null} Non-null if the script is from a MathJax CDN
  150. */
  151. function checkScript(script: HTMLScriptElement): ScriptData | null {
  152. for (const server of CDN.keys()) {
  153. const cdn = CDN.get(server);
  154. const url = cdn.base;
  155. const src = script.src;
  156. if (src && src.substr(0, url.length) === url && src.match(/\/latest\.js(\?|$)/)) {
  157. return scriptData(script, cdn);
  158. }
  159. }
  160. return null;
  161. }
  162. /**
  163. * @return {ScriptData} The data for the script tag that loaded latest.js
  164. */
  165. function getScript(): ScriptData {
  166. if (document.currentScript) {
  167. return scriptData(document.currentScript as HTMLScriptElement);
  168. }
  169. const script = document.getElementById('MathJax-script') as HTMLScriptElement;
  170. if (script && script.nodeName.toLowerCase() === 'script') {
  171. return checkScript(script);
  172. }
  173. const scripts = document.getElementsByTagName('script');
  174. for (const script of Array.from(scripts)) {
  175. const data = checkScript(script);
  176. if (data) {
  177. return data;
  178. }
  179. }
  180. return null;
  181. }
  182. /*=====================================================================*/
  183. /**
  184. * Save the version and date information in localStorage so we don't
  185. * have to contact the CDN for every page that uses MathJax.
  186. *
  187. * @param {string} version The version to save
  188. */
  189. function saveVersion(version: string) {
  190. try {
  191. const data = version + ' ' + Date.now();
  192. localStorage.setItem(MJX_LATEST, data);
  193. } catch (err) {}
  194. }
  195. /**
  196. * Get the version from localStorage, and make sure it is fresh enough to use
  197. *
  198. * @return {string|null} The version string (if one has been saved) or null (if not)
  199. */
  200. function getSavedVersion(): string | null {
  201. try {
  202. const [version, date] = localStorage.getItem(MJX_LATEST).split(/ /);
  203. if (date && Date.now() - parseInt(date) < SAVE_TIME) {
  204. return version;
  205. }
  206. } catch (err) {}
  207. return null;
  208. }
  209. /*=====================================================================*/
  210. /**
  211. * Create a script tag that loads the given URL
  212. *
  213. * @param {string} url The URL of the javascript file to be loaded
  214. * @param {string} id The id to use for the script tag
  215. */
  216. function loadMathJax(url: string, id: string) {
  217. const script = document.createElement('script');
  218. script.type = 'text/javascript';
  219. script.async = true;
  220. script.src = url;
  221. if (id) {
  222. script.id = id;
  223. }
  224. const head = document.head || document.getElementsByTagName('head')[0] || document.body;
  225. if (head) {
  226. head.appendChild(script);
  227. } else {
  228. Error('Can\'t find the document <head> element');
  229. }
  230. }
  231. /**
  232. * When we can't find the current version, use the original URL but remove the "latest.js"
  233. */
  234. function loadDefaultMathJax() {
  235. if (script) {
  236. loadMathJax(script.src.replace(/\/latest\.js\?/, '/'), script.id);
  237. } else {
  238. Error('Can\'t determine the URL for loading MathJax');
  239. }
  240. }
  241. /**
  242. * Load the given version using the base URL and file to load
  243. * (if the versions differ, run latest.js from the new version
  244. * in case there are important changes there)
  245. *
  246. * @param {string} version The version of MathJax to load from
  247. */
  248. function loadVersion(version: string) {
  249. if (script.version && script.version !== version) {
  250. script.file = 'latest.js?' + script.file;
  251. }
  252. loadMathJax(script.cdn.base + version + script.dir + '/' + script.file, script.id);
  253. }
  254. /**
  255. * Check if the given version is acceptable and load it if it is.
  256. *
  257. * @param {string} version The version to check if it is the latest (valid) one
  258. * @return {boolean} True if it is the latest version, false if not
  259. */
  260. function checkVersion(version: string): boolean {
  261. const major = parseInt(version.split(/\./)[0]);
  262. if (major === MJX_VERSION && !version.match(/-(beta|rc)/)) {
  263. saveVersion(version);
  264. loadVersion(version);
  265. return true;
  266. }
  267. return false;
  268. }
  269. /*=====================================================================*/
  270. /**
  271. * Create an XMLHttpRequest object, if possible
  272. *
  273. * @return {XMLHttpRequest} The XMLHttpRequest instance
  274. */
  275. function getXMLHttpRequest(): XMLHttpRequest {
  276. if (window.XMLHttpRequest) {
  277. return new XMLHttpRequest();
  278. }
  279. if (window.ActiveXObject) {
  280. try { return new window.ActiveXObject('Msxml2.XMLHTTP'); } catch (err) {}
  281. try { return new window.ActiveXObject('Microsoft.XMLHTTP'); } catch (err) {}
  282. }
  283. return null;
  284. }
  285. /**
  286. * Request JSON data from a CDN. If it loads OK, call the action() function
  287. * on the data. If not, or if the action returns false, run the failure() function.
  288. *
  289. * @param {CdnData} cdn The CDN whose API will be used
  290. * @param {Function} action The function to perform when the data are received
  291. * @param {Function} failure The function to perform if data can't be obtained,
  292. * or if action() returns false
  293. */
  294. function requestXML(cdn: CdnData, action: (json: JSON | JSON[]) => boolean, failure: () => void) {
  295. const request = getXMLHttpRequest();
  296. if (request) {
  297. // tslint:disable-next-line:jsdoc-require
  298. request.onreadystatechange = function () {
  299. if (request.readyState === 4) {
  300. if (request.status === 200) {
  301. !action(JSON.parse(request.responseText)) && failure();
  302. } else {
  303. Error('Problem acquiring MathJax version: status = ' + request.status);
  304. failure();
  305. }
  306. }
  307. };
  308. request.open('GET', cdn.api, true);
  309. request.send(null);
  310. } else {
  311. Error('Can\'t create XMLHttpRequest object');
  312. failure();
  313. }
  314. }
  315. /**
  316. * Look through the list of versions on GitHub and find the first one that
  317. * has the MJX_VERSION as its major version number, and load that. If none
  318. * is found, run the version from which latest.js was loaded.
  319. */
  320. function loadLatestGitVersion() {
  321. requestXML(GITHUB, (json: JSON[]) => {
  322. if (!(json instanceof Array)) return false;
  323. for (const data of json) {
  324. if (checkVersion((data as any)[GITHUB.key])) {
  325. return true;
  326. }
  327. }
  328. return false;
  329. }, loadDefaultMathJax);
  330. }
  331. /**
  332. * Check the CDN for its latest version, and load that, if it is an
  333. * acceptable version, otherwise, (e.g., the current version has a
  334. * higher major version that MJX_VERSION), find the highest version on
  335. * GitHub with the given major version and use that. If one can't be
  336. * found, use the version where latest.js was loaded.
  337. */
  338. function loadLatestCdnVersion() {
  339. requestXML(script.cdn, function (json) {
  340. if (json instanceof Array) {
  341. json = json[0];
  342. }
  343. if (!checkVersion((json as any)[script.cdn.key])) {
  344. loadLatestGitVersion();
  345. }
  346. return true;
  347. }, loadDefaultMathJax);
  348. }
  349. /*=====================================================================*/
  350. /**
  351. * Find the script that loaded latest.js
  352. * If the script is from a known CDN:
  353. * Retrieve the cached version (if any)
  354. * Load the given version of the file, if the version is cached,
  355. * Otherwise find the latest version and load that.
  356. * Otherwise,
  357. * Load using the version where latest.js was loaded.
  358. */
  359. export function loadLatest() {
  360. script = getScript();
  361. if (script && script.cdn) {
  362. const version = getSavedVersion();
  363. version ?
  364. loadVersion(version) :
  365. loadLatestCdnVersion();
  366. } else {
  367. loadDefaultMathJax();
  368. }
  369. }