From bda4883825dabc26786ef1c1645be3f63f9bac7e Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Mon, 14 Jun 2021 09:40:00 -0400 Subject: [PATCH 1/7] Update v3-lab to include mml and textmacros packages, tex input options, and chtml/svg output options. Make all checkbox areas collapsible. Better handling or keep. Fix a number of interface bugs. --- README.md | 11 +- lib/v3-lab.js | 1492 ++++++++++++++++++++++++++++++------------------- v3-lab.html | 98 +++- 3 files changed, 1010 insertions(+), 591 deletions(-) diff --git a/README.md b/README.md index 83bed74..8598a4f 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,11 @@ For the remainder we assume that this symlink has been set. ## Getting the Lab to work -You need to install the MathJax context menu first: +You need to install the MathJax context menu and the mhchem parser first: ``` shell -nmp install mj-context-menu -``` - -Create a symbolic link for the context menu. MathJax expects it to be _in parallel_ to its code. - -``` shell -ln -s node_modules/mj-context-menu +npm install mj-context-menu +npm install mhchemparser ``` Then run the lab by loading `v3-lab.html` in your webbrowser via a local diff --git a/lib/v3-lab.js b/lib/v3-lab.js index 9336f90..598c3d7 100644 --- a/lib/v3-lab.js +++ b/lib/v3-lab.js @@ -27,10 +27,10 @@ import '../mathjax3/js/util/asyncLoad/system.js'; import {TeX} from '../mathjax3/js/input/tex.js'; import {ConfigurationHandler} from '../mathjax3/js/input/tex/Configuration.js'; import {STATE} from '../mathjax3/js/core/MathItem.js'; +import {browserAdaptor} from '../mathjax3/js/adaptors/browserAdaptor.js'; import {source} from '../mathjax3/components/src/source.js'; - /*****************************************************************/ /** @@ -42,44 +42,44 @@ import {source} from '../mathjax3/components/src/source.js'; */ function V3DocumentMixin(documentClass) { - return class extends documentClass { - - /** - * Call the superclass' enrich() method depending on the checkbox settings. - * - * @override - */ - enrich() { - if (Lab.enrich) { - super.enrich(true); - } - return this; - } + return class extends documentClass { - /** - * Call the superclass' complexity() method depending on the checkbox settings. - * - * @override - */ - complexity() { - if (Lab.collapse || Lab.compute) { - this.complexityVisitor.options.makeCollapsible = Lab.collapse; - super.complexity(true); - } - return this; - } + /** + * Call the superclass' enrich() method depending on the checkbox settings. + * + * @override + */ + enrich() { + if (Lab.menu.enrich) { + super.enrich(true); + } + return this; + } - /** - * Rerender the math on the page using the Lab's Typeset() function - * - * @override - */ - rerender() { - Lab.Typeset(); - return this; - } + /** + * Call the superclass' complexity() method depending on the checkbox settings. + * + * @override + */ + complexity() { + if (Lab.menu.collapse || Lab.menu.compute) { + this.complexityVisitor.options.makeCollapsible = Lab.menu.collapse; + super.complexity(true); + } + return this; + } - }; + /** + * Rerender the math on the page using the Lab's Typeset() function + * + * @override + */ + rerender() { + Lab.Typeset(); + return this; + } + + }; } /*****************************************************************/ @@ -88,35 +88,39 @@ function V3DocumentMixin(documentClass) { * The configuration object for loader/startup */ window.MathJax = { - loader: { - load: [ - 'input/tex-full', - 'input/mml', - 'output/chtml', - 'ui/menu' - ], - paths: { - mathjax: './mathjax3/es5' - }, - source: source, - failed: (err) => console.log(err), - require: (url) => System.import(url) + loader: { + load: [ + 'input/tex-full', + 'input/mml', + '[mml]/mml3', + 'output/chtml', + 'ui/menu' + ], + paths: { + mathjax: './mathjax3/es5' }, - options: { - compileError(doc, math, err) {console.log(err); return doc.compileError(math, err)}, - typesetError(doc, math, err) {console.log(err); return doc.typesetError(math, err)} - }, - startup: { - input: ['tex', 'mml'], - output: 'chtml', - ready: () => Lab.Startup() - }, - tex: { - packages: ['base', 'autoload'] - }, - mml: { - forceReparse: true - } + source: source, + failed: (err) => console.log(err), + require: (url) => System.import(url) + }, + options: { + compileError(doc, math, err) {console.log(err); return doc.compileError(math, err)}, + typesetError(doc, math, err) {console.log(err); return doc.typesetError(math, err)} + }, + startup: { + input: ['tex', 'mml'], + output: 'chtml', + ready: () => Lab.Startup(), + invalidOption: 'fatal' + }, + tex: { + packages: ['base', 'autoload'] + }, + mml: { + forceReparse: true + }, + svg: {}, + chtml: {} }; /*****************************************************************/ @@ -125,549 +129,901 @@ window.MathJax = { * The object that manages the lab */ const Lab = window.Lab = { - ready: false, // true when everything is laoded and ready to go - input: document.getElementById('input'), // the input textarea - output: document.getElementById('output'), // where MathJax output will be displayed - output2: document.getElementById('output2'), // where second copy of MathJax output will be displayed - mathml: document.getElementById('mathml'), // where MathML output will be displayed - display: true, // true when TeX input is in display mode - showMML: false, // true when MathML output is to be shown - showSecondOutput: false, // true when second output is to be shown - enrich: false, // true when semantic enrichment is to be performed - compute: false, // true when complexity should be computed - collapse: false, // true when mactions should be inserted for complex math - explore: false, // true when the explorer is enabled - packages: {}, // the list of element ids for the TeX package checkboxes - format: 'TeX', // the input format - renderer: 'CHTML', // the output format - doc: null, // the current MathDocument - mathItem: null, // a MathItem for the current display - jax: {}, // an array of input jax objects - tex: '', // the saved input so we can switch back from MathML, if unchanged - - /*************************************************************/ - - - MathItem(input, output) { - output.innerHTML = ''; - const text = output.appendChild(document.createTextNode('')); - const math = this.mathItem = new this.doc.options.MathItem(input, this.jax[this.format], this.display); - math.setMetrics(...this.metrics); - math.start = {node: text, n: 0, delim: ''}; - math.end = {node: text, n: 0, delim: ''}; - return math; - }, + input: document.getElementById('input'), // the input textarea + output1: document.getElementById('output1'), // where MathJax output will be displayed + output2: document.getElementById('output2'), // where second copy of MathJax output will be displayed + mathml: document.getElementById('mathml'), // where MathML output will be displayed + adaptor: new browserAdaptor(), // for creating new nodes more easily - readInput() { - let input = this.input.value; - if (this.format !== 'MathML') { - return input; - } - input = input.trim(); - if (!input.match(/^$/)) { - input += ''; - } + doc: null, // the current MathDocument + mathItem: null, // a MathItem for the current display + jax: {}, // an array of input jax objects + prevTex: '', // the saved input so we can switch back from MathML, if unchanged + ready: false, // true when everything is loaded and ready to go + + render: { + display: true, // true when TeX input is in display mode + format: 'TeX', // the input format + jax: 'CHTML', // the output format + MML: false, // true when MathML output is to be shown + secondOutput: false // true when second output is to be shown + }, + texinput: { // tex input settings + tags: 'none', + tagSide: 'right', + tagIndent: '0.8em' + }, + output: { // output jax settings + mathmlSpacing: false, + mtextInheritFont: false, + merrorInheritFont: false, + mtextFont: '', + merrorFont: 'serif', + displayAlign: 'center', + displayIndent: '0', + fontCache: 'local', + adaptiveCSS: true + }, + menu: { + enrich: false, // true when semantic enrichment is to be performed + compute: false, // true when complexity should be computed + collapse: false, // true when mactions should be inserted for complex math + explore: false, // true when the explorer is enabled + semantics: false // true when data-semantics attributes should be removed + }, + packages: { + mml: {}, // the list of element ids for the MML package checkboxes + tex: {}, // the list of element ids for the TeX package checkboxes + text: {} // the list of element ids for the TextMacros package checkboxes + }, + details: { // which details are open + render: true, + menu: true, + texinput: false, + output: false, + mml: false, + tex: true, + text: false + }, + + keepOptions: [ + ['render', { + format: ['TeX', 'MathML'], + jax: ['CHTML', 'SVG'] + }], + 'menu', + ['texinput', { + tags: ['none', 'ams', 'all'], + tagSide: ['left', 'right'], + tagIndent: ['0em', '0.8em', '1em', '2em', '-1em'] + }, 'setTexInput'], + ['output', { + mtextFont: ['', 'serif', 'Arial', 'Times', 'Courier'], + merrorFont: ['', 'serif', 'Arial', 'Times', 'Courier'], + displayAlign: ['left', 'center', 'right'], + displayIndent: ['-5em', '-2em', '-1em', '0', '1em', '2em', '5em'], + fontCache: ['none', 'local', 'global'] + }, 'setOutput'], + 'packages', + 'details' + ], + + /*************************************************************/ + + + /** + * Add tag if it is missing from MathML input + */ + readInput() { + let input = this.input.value; + if (this.render.format !== 'MathML') { return input; - }, + } + input = input.trim(); + if (!input.match(/^$/)) { + input += ''; + } + return input; + }, - /** - * Perform the typesetting of the math in the proper format - */ - Typeset() { - this.doc = MathJax.startup.document; - if (!this.ready || this.doc.menu.loading) return; - - // - // Clear the old math and make a blank text node to use as the start/end node for the MathItem below - // - this.tex = ''; - - // - // Create a new MathItem from the input using the proper input jax and display mode, - // set its metrics, and link it to the text element created above, then - // add it to the document's math list. - // - this.mathItem = this.MathItem(this.readInput(), this.output); - this.doc.clear(); - this.doc.math.push(this.mathItem); - - if (this.showSecondOutput) { - let math2 = this.MathItem(this.input.value, this.output2); - this.doc.math.push(math2); - } else { - this.output2.innerHTML = ''; - } + /** + * Create a MathItem to typeset + * + * @param {string} input The math input string to typeset + * @param {HTMLElement} output The DOM element in which to display the output + */ + MathItem(input, output) { + output.innerHTML = ''; + const text = output.appendChild(document.createTextNode('')); + const math = new this.doc.options.MathItem(input, this.jax[this.render.format], this.render.display); + this.mathItem = math; + math.setMetrics(...this.metrics); + math.start = {node: text, n: 0, delim: ''}; + math.end = {node: text, n: 0, delim: ''}; + return math; + }, - // - // Reset the TeX numbering/labels, and clear the menu store - // - MathJax.texReset(); - this.doc.menu.clear(); - - // - // Typeset the math, and output the MathML - // - MathJax.typesetPromise().then(() => { - this.doc = MathJax.startup.document; - this.outputMML(Array.from(this.doc.math)[0]); - }).catch(err => { - console.log('Error: ' + (err.message || err)); - if (err.stack) console.log(err.stack); - }); - }, + /** + * Perform the typesetting of the math in the proper format + */ + Typeset() { + this.doc = MathJax.startup.document; + if (!this.ready || this.doc.menu.loading) return; - /** - * Serialize the internal MathML, shortening data-semantic-* attributes for easier viewing, - * and output the results. - */ - outputMML(math) { - this.mathml.innerHTML = ''; - if (this.showMML && math.root) { - const mml = this.doc.menu.toMML(math); - this.mathml.appendChild(document.createTextNode(mml.replace(/data-semantic/g, 'DS'))); - } - }, + // + // Clear the old math and make a blank text node to use as the start/end node for the MathItem below + // + this.prevTex = ''; - /*************************************************************/ + // + // Create a new MathItem from the input using the proper input jax and display mode, + // set its metrics, and link it to the text element created above, then + // add it to the document's math list. + // + const input = this.readInput(); + this.mathItem = this.MathItem(input, this.output1); + this.doc.clear(); + this.doc.math.push(this.mathItem); - /** - * Record the current state in the URL and reload the page - */ - Keep() { - window.location.search = [ - '?', - this.renderer.charAt(0), - this.format.charAt(0), - (this.display ? 1 : 0), - (this.showMML ? 1 : 0), - (this.showSecondOutput ? 1 : 0), - (this.enrich ? 1 : 0), - (this.compute ? 1 : 0), - (this.collapse ? 1 : 0), - (this.explore ? 1 : 0), - this.getPackageFlags(), - encodeURIComponent(this.input.value) - ].join(''); - }, + // + // Produce a second copy, if requested + // + if (this.render.secondOutput) { + let math2 = this.MathItem(input, this.output2); + this.doc.math.push(math2); + } else { + this.output2.innerHTML = ''; + } - /** - * @returns {string} A string of 1's and 0's indicating which packages are checked - */ - getPackageFlags() { - const keys = Object.keys(this.packages); - return keys.map(key => document.getElementById(this.packages[key]).checked ? 1 : 0).join(''); - }, + // + // Reset the TeX numbering/labels, and clear the menu store + // + Object.values(this.jax).map(jax => jax.reset()); + this.doc.menu.clear(); - /** - * @returns {string[]} An array of the packages that are checked - */ - getPackages() { - let result = []; - for (let key in this.packages) { - if (document.getElementById(this.packages[key]).checked) { - result.push(key); - } - } - return result; - }, + // + // Typeset the math, and output the MathML + // + return MathJax.typesetPromise().then(() => { + this.doc = MathJax.startup.document; + this.outputMML(Array.from(this.doc.math)[0]); + }).catch(err => { + console.log('Error: ' + (err.message || err)); + if (err.stack) console.log(err.stack); + }); + }, - /** - * Create the checkbox elements for all the packages that are available - */ - Packages() { - let div = document.getElementById('package'); - for (let key of Array.from(ConfigurationHandler.keys()).sort()) { - let checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.name = key; - checkbox.value = key; - checkbox.id = 'package-' + key; - checkbox.onchange = () => this.newPackages(); - if (MathJax.config.tex.packages.indexOf(key) >= 0) { - checkbox.checked = true; - } - let label = document.createElement('label'); - label.htmlFor = 'package-' + key; - label.appendChild(document.createTextNode(key[0].toUpperCase() + key.slice(1))); - checkbox.appendChild(label); - let span = div.appendChild(document.createElement('span')); - span = span.appendChild(document.createElement('span')); - span.appendChild(checkbox); - span.appendChild(label); - this.packages[key] = 'package-' + key; - } - }, + /** + * Serialize the internal MathML, shortening data-semantic-* attributes for easier viewing, + * and output the results. + */ + outputMML(math) { + this.mathml.innerHTML = ''; + if (this.render.MML && math.root) { + const mml = this.doc.menu.toMML(math); + const text = (this.menu.semantics ? + mml.replace(/ data-semantic-.*?".*?"/g, '') : + mml.replace(/data-semantic/g, 'DS')); + this.mathml.appendChild(document.createTextNode(text)); + } + }, - /** - * Create a new TeX input jax for the given set of packages, - * and set its adaptor and mmlFactory, then use that jax - * for the input jax of the current document, and retypeset - * using the new set of packages. - */ - newPackages() { - MathJax.config.tex.packages = this.getPackages(); - this.jax.TeX = new TeX(MathJax.config.tex); - this.jax.TeX.setAdaptor(this.doc.adaptor); - this.jax.TeX.setMmlFactory(this.doc.mmlFactory); - if (this.format === 'TeX') { - this.doc.inputJax = [this.jax.TeX]; - this.Typeset(); - } - }, + /*************************************************************/ - /** - * Disable/enable all the package checkboxes - * - * @param {boolean} disabled True to disable, false to enable - */ - disablePackages(disabled) { - for (const input of document.querySelectorAll('#package input')) { - input.disabled = disabled; + /** + * Record the current state in the URL and reload the page + */ + Keep() { + window.location.search = [ + '?', + this.writeOptions(this.keepOptions), + '/', + encodeURIComponent(this.input.value) + ].join(''); + }, + + /** + * Read the data stored in the URL and set the internal state and input elements + * to correspond to those values. + */ + Load() { + const data = decodeURIComponent(window.location.search.substr(1)); + this.input.value = this.readOptions(this.keepOptions, data); + }, + + /** + * Produce the string of data for saving the settings + * + * @param {(string|[string,object])[]} data The description of the data to save + * @return {string} The data string for the encoded options + */ + writeOptions(data) { + const groups = []; + for (const item of data) { + const params = []; + const [name, values] = (Array.isArray(item) ? item : [item, {}]); + for (const [key, option] of Object.entries(this[name])) { + switch(typeof option) { + case 'boolean': + params.push(option ? 'T' : 'F'); + break; + case 'string': + params.push((values[key] || []).indexOf(option)); + break; + case 'object': + for (const input of Object.values(option)) { + params.push(input.checked ? '1' : '0'); + } + params.push('/'); + break; } - }, - - /*************************************************************/ + } + groups.push(params.join('')); + } + return groups.join('/'); + }, - /** - * Sets the input format to the given type - * - * @param {value: string} The input format (Display TeX, Inline TeX, or MathML) - */ - setFormat(value) { - const format = value.split(/ /); - const changed = (this.format !== format[0]); - if (changed) { - // - // Attach the proper input jax to the current document - // - this.format = format[0]; - const jax = this.jax[this.format]; - this.doc.inputJax = [jax]; - jax.setAdaptor(this.doc.adaptor); - jax.setMmlFactory(this.doc.mmlFactory); + /** + * Read a data string and set the options based on that + * + * @param {(string|[string,object])[]} data The description of the data to save + * @param {string} params The data string containing the encoded options + */ + readOptions(data, params) { + let i = 0; + for (const item of data) { + const [name, values] = (Array.isArray(item) ? item : [item, {}]); + const options = this[name]; + for (const key of Object.keys(options)) { + let c = params.charAt(i++); + if (c === '/') { + i--; + break; } - let tex = ''; - if (format[0] === 'TeX') { - // - // Switch to TeX input by setting the display flag, - // enabling the package checkboxes and setting the - // input textarea to the original TeX (or blank) - // - this.display = format[1] === 'D'; - this.disablePackages(false); - if (changed) { - this.input.value = this.tex; - } - } else { - // - // Switch to MathML input by disabling the package checkboxes, - // saving the current TeX code for later, and using the - // serlialized internal MathML as the new input - // - this.disablePackages(true); - tex = this.input.value; - const mml = this.doc.menu.toMML(this.mathItem); - this.input.value = mml.replace(/ data-semantic-\S+="[^"]*"/g, ''); + switch(typeof options[key]) { + case 'boolean': + options[key] = (c === 'T'); + break; + case 'string': + options[key] = (values[key][parseInt(c)] || ''); + break; + case 'object': + for (const input of Object.values(options[key])) { + input.checked = (c === '1'); + c = params.charAt(i++); + if (c === '/') i--; + } + while (params.charAt(i++) !== '/' && i < params.length) {} + break; } - this.Typeset(); - this.tex = tex; - }, + } + while (params.charAt(i++) !== '/' && i < params.length) {} + } + return params.slice(i); + }, - /** - * Set the output renderer to the given one - * - * @param {string} value The renderer to select (CHTML or SVG) - */ - setRenderer(value) { - this.renderer = value; - this.setVariable('renderer', value); - }, + /*************************************************************/ - /** - * Sets whether or not to show the internal MathML - * - * @param {boolean} checked Whether to show the MathML or not (true = show) - */ - setMathML(checked) { - this.showMML = checked; - this.Typeset(); - }, + /** + * @param {string} type The package type (mml, tex, text) + * @returns {string} A string of 1's and 0's indicating which packages are checked + */ + getPackageFlags(type) { + const packages = this.packages[type]; + return Object.values(packages).map(input => input.checked ? 1 : 0).join(''); + }, - /** - * Sets whether or not to show the second copy of the MathJax output. This - * is useful for testing with tools that keep an internal state, like menu, - * explorer etc. to verify that there are no interferences with multiple - * math items in the page. - * - * @param {boolean} checked Whether or not to show the second outputMathML. - */ - setSecondOutput(checked) { - this.showSecondOutput = checked; - this.Typeset(); - }, + /** + * @param {string} type The package type (mml, tex, text) + * @returns {string[]} An array of the packages that are checked + */ + getPackages(type) { + const packages = this.packages[type]; + let result = []; + for (let key of Object.keys(packages)) { + if (packages[key].checked) { + result.push(key); + } + } + return result; + }, - /** - * Sets whether or not to enrich the MathML - * - * @param {boolean} checked Whether to enrich or not (true = enrich) - */ - setEnrich(checked) { - this.enrich = checked; - if (checked) { - this.loadA11y('complexity'); - this.Typeset(); - } else { - this.setVariable('collapsible', false, true); - this.setVariable('explorer', false, true); - } - }, + /** + * Create the checkbox elements for all the packages that are available + * + * @param {string} type The package type (mml, tex, text) + */ + createPackageCheckboxes(type) { + let div = document.getElementById(`${type}-package`); + if (type === 'mml') { + this.createCheckbox(type, 'mml3', false, div); + return; + } + for (let key of Array.from(ConfigurationHandler.keys()).sort()) { + if (type === 'tex' && ConfigurationHandler.get(key).parser !== 'tex') continue; + const config = (type === 'tex' ? MathJax.config.tex : + MathJax.config.tex.textmacros || {packages: ['text-base']}); + this.createCheckbox(type, key, config.packages.indexOf(key) >= 0, div); + } + }, - /** - * Sets whether or not to compute complexity values in the enriched MathML - * - * @param {boolean} checked Whether to compute complexity or not (true = compute) - */ - setCompute(checked) { - this.compute = checked; - if (checked) { - this.enrich = true; - document.getElementById('enrich').checked = true; - this.loadA11y('complexity'); - this.Typeset(); - } else { - this.collapse = false; - document.getElementById('collapse').checked = false; - this.menuVariable('collapsible').setValue(false); // don't clear other a11y checkboxes - } - }, + /** + * Create a package checkbox + * + * @param {string} type The package type (mml, tex, text) + * @param {string} key The package name + * @param {boolean} checked Whether it should be checked or not + * @param {HTMLElement} div The
where the checkbox should go + * @return {HTMLInputElement} The newly created checkbox + */ + createCheckbox(type, key, checked, div) { + let checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.name = key; + checkbox.value = key; + checkbox.id = type + '-' + key; + checkbox.onchange = () => this.newPackages(); + checkbox.checked = checked; + let label = document.createElement('label'); + label.htmlFor = checkbox.id; + label.appendChild(document.createTextNode(key)); + checkbox.appendChild(label); + let span = div.appendChild(document.createElement('span')); + span = span.appendChild(document.createElement('span')); + span.appendChild(checkbox); + span.appendChild(label); + this.packages[type][key] = checkbox; + }, - /** - * Sets whether or not to add maction elements for complex math - * - * @param {boolean} checked Whether to add mactions or not (true = add) - */ - setCollapse(checked) { - this.collapse = checked; - if (checked) { - this.setVariable('collapsible', true); - } else { - this.menuVariable('collapsible').setValue(false); // don't clear other a11y checkboxes - } - }, + /** + * Create a new TeX input jax for the given set of packages, + * and set its adaptor and mmlFactory, then use that jax + * for the input jax of the current document, and retypeset + * using the new set of packages. + */ + newPackages() { + if (this.render.format === 'TeX') { + MathJax.config.tex.packages = this.getPackages('tex'); + if (MathJax.config.tex.packages.indexOf('textmacros') >= 0) { + MathJax.config.tex.textmacros = {packages: this.getPackages('text')}; + this.disablePackages('text', false); + } else { + delete MathJax.config.tex.textmacros; + this.disablePackages('text', true); + } + this.newTexInput(); + } + return this.Typeset(); + }, - /** - * Sets whether or not to enable the Explorer module - * - * @param {boolean} checked Whether to add the Explorer or not (true = add) - */ - setExplorer(checked) { - this.explore = checked; - this.setVariable('explorer', checked); - }, + /** + * Get a new TeX input jax + */ + newTexInput() { + const jax = this.jax.TeX = new TeX(MathJax.config.tex); + jax.setAdaptor(this.doc.adaptor); + jax.setMmlFactory(this.doc.mmlFactory); + this.doc.inputJax[0] = jax; + }, - /** - * Loads an a11y module (complexity or explorer) if it hasn't already been loaded - */ - loadA11y(component) { - if (!MathJax._.a11y || !MathJax._.a11y[component]) { - this.doc.menu.loadA11y(component); - } - }, + /** + * Get a new output jax + */ + newOutput() { + const jax = (this.render.jax === 'CHTML' ? + new MathJax._.output.chtml_ts.CHTML(MathJax.config.chtml) : + new MathJax._.output.svg_ts.SVG(MathJax.config.SVG)); + jax.setAdaptor(this.doc.adaptor); + this.doc.menu.jax[this.render.jax] = jax; + this.doc.outputJax = jax; + }, - /*************************************************************/ + /** + * Disable/enable all the package checkboxes + * + * @param {string} type The package type (mml, tex, text) + * @param {boolean} disabled True to disable, false to enable + */ + disablePackages(type, disabled) { + for (const input of Object.values(this.packages[type])) { + input.disabled = disabled; + } + }, + + /*************************************************************/ - /** - * Ask the output renderer to determine the measure the (font and container) metrics - * for the output area and save them to be used for the MathItems during typesetting. - */ - initMetrics() { - let {em, ex, containerWidth, lineWidth, scale} = MathJax.getMetricsFor(this.output); - this.metrics = [em, ex, containerWidth, lineWidth, scale]; - }, + /** + * Sets the input format to the given type + * + * @param {value: string} The input format (Display TeX, Inline TeX, or MathML) + */ + setFormat(value) { + const format = value.split(/ /); + const changed = (this.render.format !== format[0]); + if (changed) { + // + // Attach the proper input jax to the current document + // + this.render.format = format[0]; + const jax = this.jax[this.render.format]; + this.doc.inputJax = [jax]; + jax.setAdaptor(this.doc.adaptor); + jax.setMmlFactory(this.doc.mmlFactory); + } + let tex = ''; + if (format[0] === 'TeX') { + // + // Switch to TeX input by setting the display flag, + // enabling the package checkboxes and setting the + // input textarea to the original TeX (or blank) + // + this.render.display = format[1] === 'D'; + this.disablePackages('mml', true); + this.disablePackages('tex', false); + this.disablePackages('text', MathJax.config.tex.packages.indexOf('textmacros') < 0); + if (changed) { + this.input.value = this.prevTex; + } + } else { + // + // Switch to MathML input by disabling the package checkboxes, + // saving the current TeX code for later, and using the + // serlialized internal MathML as the new input + // + this.disablePackages('mml', false); + this.disablePackages('tex', true); + this.disablePackages('text', true); + tex = this.input.value; + const mml = this.doc.menu.toMML(this.mathItem); + this.input.value = mml.replace(/ data-semantic-\S+="[^"]*"/g, ''); + } + this.Typeset(); + this.prevTex = tex; + }, - /** - * Add callbacks to the menu items that need to be synchronized with checkboxes - * and set their values to correspond to the current state - */ - initMenu() { - this.setVariable('renderer', this.renderer, true); - this.setVariable('collapsible', this.collapse, true); - this.setVariable('explorer', this.explore, true); - - this.menuVariable('explorer').registerCallback(() => { - this.explore = this.doc.menu.settings.explorer; - document.getElementById('explore').checked = this.explore; - if (this.explore) { - this.enrich = true; - document.getElementById('enrich').checked = true; - } - this.Typeset(); - }); - this.menuVariable('collapsible').registerCallback(() => { - const checked = this.doc.menu.settings.collapsible; - document.getElementById('collapse').checked = checked; - this.compute = this.collapse = checked; - document.getElementById('compute').checked = checked; - if (checked || !this.doc.menu.settings.explorer) { - this.enrich = checked; - document.getElementById('enrich').checked = checked; - } - this.Typeset(); - }); - this.menuVariable('renderer').registerCallback(() => { - this.renderer = this.doc.menu.settings.renderer; - document.getElementById('renderer').value = this.renderer - }); - this.menuVariable('semantics').registerCallback(() => this.outputMML(this.mathItem)); - this.menuVariable('texHints').registerCallback(() => this.outputMML(this.mathItem)); - }, + /** + * Set the output renderer to the given one + * + * @param {string} value The renderer to select (CHTML or SVG) + */ + setRenderer(value) { + this.render.jax = value; + document.getElementById('fontCache').disabled = (value === 'CHTML'); + document.getElementById('adaptiveCSS').disabled = (value === 'SVG'); + this.setVariable('renderer', value); + }, - /** - * Get a named variable object from the menu's variable pool - * - * @param {string} name The name of the variable to get - */ - menuVariable(name) { - return this.doc.menu.menu.pool.lookup(name); - }, + /** + * Set a TeX input option and retypeset + * + * @param {HTMLElement} node The input element beinbg updated + */ + setTexInput(node) { + const key = node.id; + const value = node.value; + this.texinput[key] = value; + MathJax.config.tex[key] = value; + this.jax.TeX.parseOptions.options[key] = value; + if (key === 'tags') { + this.newTexInput(); + } + this.Typeset(); + }, - /** - * Set a menu variable and call its callbacks - * - * @param {string} name The name of the variable to set - * @param {string | boolean} value The new value for the variable - * @param {boolean} force Whether to force the change (to run actions and callbacks) - * even if the value is already equal to the new value - */ - setVariable(name, value, force) { - if (value !== this.doc.menu.settings[name] || force) { - const variable = this.menuVariable(name); - variable.setValue(value); - const item = variable.items[0]; - if (item) { - item.executeCallbacks_(); - } - } - }, + /** + * Set an output option + * + * @param {HTMLElement} node The input element beinbg updated + */ + setOutput(node) { + const key = node.id; + const value = (typeof this.output[key] === 'boolean' ? node.checked : node.value); + this.output[key] = value; + if (key !== 'fontCache') { + MathJax.config.chtml[key] = value; + } + if (key !== 'adaptiveCSS') { + MathJax.config.svg[key] = value; + } + this.doc.outputJax.options[key] = value; + if ((key === 'fontCache' && this.render.jax === 'SVG') || + (key === 'adaptiveCSS' && this.render.jax === 'CHTML')) { + this.newOutput(); + } + this.Typeset(); + }, - /** - * Augment a handler's document using the mixin for complexity() and explorable() - * - * @param {Handler} handler The handler to be augmented - */ - menuHandler(handler) { - handler.documentClass = V3DocumentMixin(handler.documentClass); - return handler; - }, + /** + * Set the details flag + * + * @param {HTMLDetailsElement} node The details tag being opened or closed + */ + setDetails(node) { + const type = node.id.replace(/-.*/, ''); + this.details[type] = node.open; + }, - /*************************************************************/ + /** + * Sets whether or not to show the internal MathML + * + * @param {boolean} checked Whether to show the MathML or not (true = show) + */ + setMathML(checked) { + this.render.MML = checked; + this.Typeset(); + }, - /** - * Check a keypress in the input textarea to see if it should force - * typesetting (e.g., SHIFT-RETURN does this) - * - * @param {HTMLTextarea} textarea The textarea node - * @param {KeyEvent} event The key event to check - */ - checkKey(textarea, event) { - if (!event) event = window.event; - var code = event.which || event.keyCode; - if ((event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) && - (code === 13 || code === 10)) { - if (event.preventDefault) event.preventDefault(); - event.returnValue = false; - this.Typeset(); - } - }, + /** + * Sets whether or not to show the second copy of the MathJax output. This + * is useful for testing with tools that keep an internal state, like menu, + * explorer etc. to verify that there are no interferences with multiple + * math items in the page. + * + * @param {boolean} checked Whether or not to show the second outputMathML. + */ + setSecondOutput(checked) { + this.render.secondOutput = checked; + this.output2.style.display = (checked ? '' : 'none'); + this.Typeset(); + }, - /*************************************************************/ + /** + * Sets whether or not to enrich the MathML + * + * @param {boolean} checked Whether to enrich or not (true = enrich) + */ + setEnrich(checked) { + this.menu.enrich = checked; + if (checked) { + this.loadA11y('complexity'); + this.Typeset(); + } else { + this.setVariable('collapsible', false, true); + this.setVariable('explorer', false, true); + } + }, - /** - * Initialize the lab once all the components have been loaded - */ - Startup() { - // - // Get the package list, and read any parameters from the URL - // - this.Packages(); - if (window.location.search !== '') this.Load(); - // - // Extend the handler created by the startup module to include the menu handler - // - const startup = MathJax.startup; - startup.extendHandler(handler => this.menuHandler(handler), 20); - // - // Transfer the checkbox information into the menu initialization - // - MathJax.config.options.menuOptions = { - settings: { - renderer: this.renderer, - collapsible: this.collapse, - explorer: this.explore - } - }; - // - // Run the startup module's initialization - // - startup.getComponents(); - startup.makeMethods(); - // - // Get the input jax and document that were created by the startup module, - // and add our hooks into the menu for synchronizing the checkboxes - // - this.jax = { - TeX: startup.input[0], - MathML: startup.input[1] - }; - this.doc = startup.document; - this.initMenu(); - // - // Initialize the rest of the lab - // - this.initMetrics(); - this.ready = true; - this.newPackages(); + /** + * Sets whether or not to compute complexity values in the enriched MathML + * + * @param {boolean} checked Whether to compute complexity or not (true = compute) + */ + setCompute(checked) { + this.menu.compute = checked; + if (checked) { + this.menu.enrich = true; + document.getElementById('enrich').checked = true; + this.loadA11y('complexity'); + this.Typeset(); + } else { + this.menu.collapse = false; + document.getElementById('collapse').checked = false; + this.menuVariable('collapsible').setValue(false); // don't clear other a11y checkboxes + } + }, + + /** + * Sets whether or not to add maction elements for complex math + * + * @param {boolean} checked Whether to add mactions or not (true = add) + */ + setCollapse(checked) { + this.menu.collapse = checked; + if (checked) { + this.setVariable('collapsible', true); + } else { + this.menuVariable('collapsible').setValue(false); // don't clear other a11y checkboxes + } + }, + + /** + * Sets whether or not to enable the Explorer module + * + * @param {boolean} checked Whether to add the Explorer or not (true = add) + */ + setExplorer(checked) { + this.menu.explore = checked; + this.setVariable('explorer', checked); + }, + + setSemantics(checked) { + this.menu.semantics = checked; + this.Typeset(); + }, + + /** + * Loads an a11y module (complexity or explorer) if it hasn't already been loaded + */ + loadA11y(component) { + if (!MathJax._.a11y || !MathJax._.a11y[component]) { + this.doc.menu.loadA11y(component); + } + }, + + /*************************************************************/ + + /** + * Ask the output renderer to determine the measure the (font and container) metrics + * for the output area and save them to be used for the MathItems during typesetting. + */ + initMetrics() { + let {em, ex, containerWidth, lineWidth, scale} = MathJax.getMetricsFor(this.output1); + this.metrics = [em, ex, containerWidth, lineWidth, scale]; + }, + + /** + * Add callbacks to the menu items that need to be synchronized with checkboxes + * and set their values to correspond to the current state + */ + initMenu() { + this.setVariable('renderer', this.render.jax, true); + this.setVariable('collapsible', this.menu.collapse, true); + this.setVariable('explorer', this.menu.explore, true); + + this.menuVariable('explorer').registerCallback(() => { + this.menu.explore = this.doc.menu.settings.explorer; + document.getElementById('explore').checked = this.menu.explore; + if (this.menu.explore) { + this.menu.enrich = true; + document.getElementById('enrich').checked = true; + } + this.Typeset(); + }); + this.menuVariable('collapsible').registerCallback(() => { + const checked = this.doc.menu.settings.collapsible; + document.getElementById('collapse').checked = checked; + this.menu.compute = this.menu.collapse = checked; + document.getElementById('compute').checked = checked; + if (checked || !this.doc.menu.settings.explorer) { + this.menu.enrich = checked; + document.getElementById('enrich').checked = checked; + } + this.Typeset(); + }); + this.menuVariable('renderer').registerCallback(() => { + this.render.jax = this.doc.menu.settings.renderer; + document.getElementById('renderer').value = this.render.jax + }); + this.menuVariable('semantics').registerCallback(() => this.outputMML(this.mathItem)); + this.menuVariable('texHints').registerCallback(() => this.outputMML(this.mathItem)); + }, + + /** + * Get a named variable object from the menu's variable pool + * + * @param {string} name The name of the variable to get + */ + menuVariable(name) { + return this.doc.menu.menu.pool.lookup(name); + }, + + /** + * Set a menu variable and call its callbacks + * + * @param {string} name The name of the variable to set + * @param {string | boolean} value The new value for the variable + * @param {boolean} force Whether to force the change (to run actions and callbacks) + * even if the value is already equal to the new value + */ + setVariable(name, value, force) { + if (value !== this.doc.menu.settings[name] || force) { + const variable = this.menuVariable(name); + variable.setValue(value); + const item = variable.items[0]; + if (item) { + item.executeCallbacks_(); + } + } + }, + + /** + * Augment a handler's document using the mixin for complexity() and explorable() + * + * @param {Handler} handler The handler to be augmented + */ + menuHandler(handler) { + handler.documentClass = V3DocumentMixin(handler.documentClass); + return handler; + }, + + /*************************************************************/ + + /** + * Check a keypress in the input textarea to see if it should force + * typesetting (e.g., SHIFT-RETURN does this) + * + * @param {HTMLTextarea} textarea The textarea node + * @param {KeyEvent} event The key event to check + */ + checkKey(textarea, event) { + if (!event) event = window.event; + var code = event.which || event.keyCode; + if ((event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) && + (code === 13 || code === 10)) { + if (event.preventDefault) event.preventDefault(); + event.returnValue = false; + this.Typeset(); + } + }, + + /*************************************************************/ + + /** + * Initialize the lab once all the components have been loaded + */ + Startup() { + // + // Create the form elements, and read any parameters from the URL, + // then set the form elements to their correct values + // + this.createFormElements(); + if (window.location.search !== '') this.Load(); + if (this.render.format === 'MathML') { + Lab.disablePackages('tex', true); + Lab.disablePackages('text', true); + } else { + Lab.disablePackages('mml', true); + Lab.disablePackages('text', MathJax.config.tex.packages.indexOf('textmacros') < 0); + } + this.initFormElements(); + // + // Extend the handler created by the startup module to include the menu handler + // + const startup = MathJax.startup; + startup.extendHandler(handler => this.menuHandler(handler), 20); + // + // Transfer the checkbox information into the menu initialization + // + this.initOptions(); + // + // Run the startup module's initialization + // + startup.getComponents(); + startup.makeMethods(); + // + // Get the input jax and document that were created by the startup module, + // and add our hooks into the menu for synchronizing the checkboxes + // + this.jax = { + TeX: startup.input[0], + MathML: startup.input[1] + }; + this.jax.MathML.preFilters.items = []; + this.jax.MathML.preFilters.add(() => document.getElementById('mml-mml3').checked); + this.doc = startup.document; + this.initMenu(); + // + // Load any needed extensions + // + if (this.menu.explore) { + this.loadA11y('explorer') + } else if (this.menu.enrich || this.complexity) { + this.loadA11y('complexity'); + } + + // + // Initialize the rest of the lab + // + this.initMetrics(); + this.ready = true; + return MathJax.typesetPromise().then(() => { + return this.newPackages().then(() => { document.getElementById('keep').disabled = false; document.getElementById('typeset').disabled = false; - }, + }); + }).catch(error => console.warn(error)); + }, - /** - * Read the data stored in the URL and set the internal state and input elements - * to correspond to those values. - */ - Load() { - const data = decodeURIComponent(window.location.search.substr(1)); - const n = Lab.getPackageFlags().length; - this.input.value = data.substr(n + 9).trim(); - this.explore = data.charAt(8) === '1'; - this.collapse = data.charAt(7) === '1'; - this.compute = data.charAt(6) === '1'; - this.enrich = data.charAt(5) === '1'; - this.showSecondOutput = data.charAt(4) === '1'; - this.showMML = data.charAt(3) === '1'; - this.display = data.charAt(2) === '1'; - this.format = {T: 'TeX', M: 'MathML'}[data.charAt(1)]; - this.renderer = {C: 'CHTML', S: 'SVG'}[data.charAt(0)]; - const format = this.format + (this.format === 'TeX' ? ' ' + (this.display ? 'D' : 'I') : ''); - document.getElementById('format').value = format; - document.getElementById('renderer').value = this.renderer; - document.getElementById('showMML').checked = this.showMML; - document.getElementById('showSecondOutput').checked = this.showSecondOutput; - document.getElementById('enrich').checked = this.enrich; - document.getElementById('compute').checked = this.compute; - document.getElementById('collapse').checked = this.collapse; - document.getElementById('explore').checked = this.explore; - const flags = data.substr(9,n); - let i = 0; - for (const key in Lab.packages) { - document.getElementById(Lab.packages[key]).checked = (flags.charAt(i++) === '1'); + /** + * Creates the needed form elements for the keep options + */ + createFormElements() { + for (const data of this.keepOptions) { + const [name, values, change] = (Array.isArray(data) ? data : [data, {}, false]); + if (name === 'packages') { + Object.keys(this.packages).map(type => this.createPackageCheckboxes(type)); + } else if (change) { + this.createOptionElements(name, change, values); + } + } + }, + + /** + * Creates the input elements for an option collection + * + * @param {string} name The name of the option collection + * @param {string} change The method to call when an option changes + * @param {object} values The object containing the option data + */ + createOptionElements(name, change, values) { + const adaptor = this.adaptor; + const parent = document.getElementById(`${name}-details`); + const onchange = `Lab.${change}(this)`; + let div; + for (const [key, value] of Object.entries(this[name])) { + if (!div) { + div = parent.appendChild(adaptor.node('div')); + } + switch(typeof value) { + case 'boolean': + div.appendChild(adaptor.node('input', {type: 'checkbox', id: key, onchange: onchange})); + div.appendChild(adaptor.node('label', {for: key}, [adaptor.text(' ' + key)])); + div.appendChild(adaptor.node('br')); + break; + case 'string': + div = parent.appendChild(adaptor.node('div')); + div.appendChild(adaptor.node('label', {for: key}, [adaptor.text(key + ':')])); + div.appendChild(adaptor.text(' ')); + const select = div.appendChild(adaptor.node('select', {id: key, onchange: onchange})); + for (const option of values[key]) { + select.appendChild(adaptor.node('option', {value: option}, [adaptor.text(option || '(none)')])); } - if (this.format === 'MathML') Lab.disablePackages(true); + div = null; + break; + } + } + }, + + /** + * Set the initial values of the form elements. + */ + initFormElements() { + this.setFormOptions(this.menu); + this.setFormOptions(this.texinput); + this.setFormOptions(this.output); + this.setFormOptions(this.render); + const format = this.render.format + (this.render.format === 'TeX' ? ' ' + (this.render.display ? 'D' : 'I') : ''); + document.getElementById('format').value = format; + for (const [type, open] of Object.entries(this.details)) { + document.getElementById(`${type}-details`).open = open; + } + this.output2.style.display = (this.render.secondOutput ? '' : 'none'); + document.getElementById('fontCache').disabled = (this.render.jax === 'CHTML'); + document.getElementById('adaptiveCSS').disabled = (this.render.jax === 'SVG'); + }, + + /** + * @param {object} options The {[name: string]: string | boolean} set of options and their values + */ + setFormOptions(options) { + for (const [id, value] of Object.entries(options)) { + const input = document.getElementById(id); + if (!input) continue; + if (typeof value === 'boolean') { + input.checked = value; + } else { + input.value = value; + } + } + }, + + /** + * Set the MathJax configuration to be the initivial values given in the form + */ + initOptions() { + MathJax.config.options.menuOptions = { + settings: { + renderer: this.render.jax, + collapsible: this.menu.collapse, + explorer: this.menu.explore + } + }; + for (const [key, value] of Object.entries(this.texinput)) { + MathJax.config.tex[key] = value; + } + for (const [key, value] of Object.entries(this.output)) { + if (key !== 'fontCache') { + MathJax.config.chtml[key] = value; + } + if (key !== 'adaptiveCSS') { + MathJax.config.svg[key] = value; + } } + } - /*************************************************************/ + /*************************************************************/ }; diff --git a/v3-lab.html b/v3-lab.html index d42b5a2..ddebbe0 100644 --- a/v3-lab.html +++ b/v3-lab.html @@ -12,7 +12,8 @@ @@ -39,8 +40,8 @@ vertical-align: top; } #layout > div > div:last-child { - width: 18em; - max-width: 18em; + width: 18.5em; + max-width: 18.5em; } #main { @@ -74,28 +75,44 @@ padding-left: 1em; font-size: 80%; } -#controls > div { +#controls > div, +#controls details > div { margin-bottom: .5em; white-space: nowrap; } +summary + * { + margin-top: .5em; +} #controls > hr { border: none; border-top: 2px dotted; margin: .7em 1em .7em 0; } -#package { - margin-top: .5em; +#mml-package, +#tex-package, +#text-package { white-space: normal ! important; } -#package > span > span { +#mml-package > span > span, +#tex-package > span > span, +#text-package > span > span { display: inline-block; width: 9em; white-space: nowrap; } -#package label { +#mml-package label, +#tex-package label, +#text-package label { padding-left: 5px; } +#output-details label:first-child, +#texinput-details label:first-child { + display: inline-block; + min-width: 7em; + text-align: right; +} + input[disabled] + label { color: #AAAAAA; } @@ -114,12 +131,17 @@

MathJax v3 Interactive Lab

-
-
+
+ +

 
+
+ +
+Rendering Options:
- -
-
- +
+
+
+
+ +
+ +
+
+TeX Input Options: +
+ +
+
+Output Options: +
+ +
+
+MathML Extensions: +
+
+
-
+
+TeX Packages: +
+
+ +
+
+TextMacros Packages: +
+
+
@@ -156,10 +217,17 @@

MathJax v3 Interactive Lab

__dirname = String(location.href).replace(/v3-lab\.html.*/, 'mathjax3/components/src'); + + + From 303452dedc101edc973c97e6444522ebdfcf3a40 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Mon, 14 Jun 2021 09:48:19 -0400 Subject: [PATCH 2/7] Fix comment spacing, and remove unneeded function --- lib/v3-lab.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/v3-lab.js b/lib/v3-lab.js index 598c3d7..b11e1bc 100644 --- a/lib/v3-lab.js +++ b/lib/v3-lab.js @@ -138,7 +138,7 @@ const Lab = window.Lab = { doc: null, // the current MathDocument mathItem: null, // a MathItem for the current display jax: {}, // an array of input jax objects - prevTex: '', // the saved input so we can switch back from MathML, if unchanged + prevTex: '', // the saved input so we can switch back from MathML, if unchanged ready: false, // true when everything is loaded and ready to go render: { @@ -148,7 +148,7 @@ const Lab = window.Lab = { MML: false, // true when MathML output is to be shown secondOutput: false // true when second output is to be shown }, - texinput: { // tex input settings + texinput: { // tex input settings tags: 'none', tagSide: 'right', tagIndent: '0.8em' @@ -407,15 +407,6 @@ const Lab = window.Lab = { /*************************************************************/ - /** - * @param {string} type The package type (mml, tex, text) - * @returns {string} A string of 1's and 0's indicating which packages are checked - */ - getPackageFlags(type) { - const packages = this.packages[type]; - return Object.values(packages).map(input => input.checked ? 1 : 0).join(''); - }, - /** * @param {string} type The package type (mml, tex, text) * @returns {string[]} An array of the packages that are checked From d265485e82cfa115e47b48669396de0ac0401647 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Mon, 14 Jun 2021 10:01:23 -0400 Subject: [PATCH 3/7] List packages alphabetically in columns --- v3-lab.html | 1 + 1 file changed, 1 insertion(+) diff --git a/v3-lab.html b/v3-lab.html index ddebbe0..335a0a0 100644 --- a/v3-lab.html +++ b/v3-lab.html @@ -91,6 +91,7 @@ #mml-package, #tex-package, #text-package { + column-count: 2; white-space: normal ! important; } #mml-package > span > span, From 6cc0cdb9d868658e691cda1a4eac745cf512e5a5 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Mon, 14 Jun 2021 11:12:45 -0400 Subject: [PATCH 4/7] Don't need fix for mml3 only loading once. --- lib/v3-lab.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/v3-lab.js b/lib/v3-lab.js index b11e1bc..2dd8d20 100644 --- a/lib/v3-lab.js +++ b/lib/v3-lab.js @@ -882,8 +882,7 @@ const Lab = window.Lab = { TeX: startup.input[0], MathML: startup.input[1] }; - this.jax.MathML.preFilters.items = []; - this.jax.MathML.preFilters.add(() => document.getElementById('mml-mml3').checked); + this.jax.MathML.preFilters.add(() => document.getElementById('mml-mml3').checked, 0); this.doc = startup.document; this.initMenu(); // From e8e04ea1f09e433369a5e84c9145bb720000ffac Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 19 Jan 2022 23:04:05 +0100 Subject: [PATCH 5/7] js extension configuration. --- v3-lab.html | 1 + 1 file changed, 1 insertion(+) diff --git a/v3-lab.html b/v3-lab.html index 335a0a0..1aa5b54 100644 --- a/v3-lab.html +++ b/v3-lab.html @@ -10,6 +10,7 @@