/**
* This module loads scripts from the MathLive library (from
* https://cortexjs.io/mathlive/), and then creates from it several different
* tools useful in the Lurch application.
*
* First, it creates the {@link MathItem} class, which can be added to
* {@link Dialog} instances as an input component containing an equation editor.
* (Note that I say "equation editor" here because that is common parlance, but
* of course one can use it for many types of mathematical expressions, not just
* equations.)
*
* Second, it creates a function for converting among various math notation
* formats. See {@link module:MathLive.getConverter getConverter()} for
* details.
*
* @module MathLive
*/
import { loadScript } from './utilities.js'
import asciiMathToLatex from '../parsers/asciimath-to-latex.js'
// Internal use only.
// Ensures the MathLive scripts are loaded, so you can do whatever you want with
// the stuff they install in the global (window) object thereafter.
const loadMathFieldClass = () =>
loadScript( 'https://unpkg.com/mathlive@0.100.0' ).then( () =>
loadScript( 'https://unpkg.com/@cortex-js/compute-engine@0.24.1' ) )
/**
* We store here the URL to the MathLive CSS stylesheet, so that we can define
* it in only one location and others can reference it from here.
*
* @type {string}
*/
export const stylesheet = 'https://unpkg.com/mathlive@0.100.0/dist/mathlive-static.css'
/**
* An item that can be used in a {@link Dialog} and shows up as an equation
* editor powered by a MathLive math editing component.
*/
export class MathItem {
/**
* Construct a MathLive editing component.
*
* @param {string} name - the name of the control in the dialog, used for
* querying its value when the dialog closes, or providing an initial
* value when the dialog opens
* @param {string} label - the label to show next to the math ediitor in the
* user interface
*/
constructor ( name, label ) {
this.name = name
this.label = label
this.setupInitiated = false
this.mathLiveEditor = null
this.mathValue = null
this.focusWhenShown = false
}
/**
* Whether to focus this item once it has loaded. Defaults to `false`, so
* as to not interfere with the usual focus mechanics of the {@link Dialog}
* class. However, because this component is a unique one, the normal focus
* mechanics will not work for it, so you should call this function to
* override them if you want this item to receive focus once it has been
* installed into the dialog.
*
* @param {boolean} value - whether to focus this item once it appears
*/
setFocusWhenShown ( value ) { this.focusWhenShown = value }
// internal use only; creates the JSON to represent this object to TinyMCE
// (actually, it creates an HTML element that will LATER be populated
// programmatically with a MathLive editor)
json () {
return [
{
type : 'htmlpanel',
html : `
<label class='tox-label'>${this.label}</label>
<div id='math-${this.name}'>Loading editor...</div>
`
}
]
}
// Internal use only.
// Stores the current value of the MathLive editor, as a LaTeX string,
// because once the editor is closed, you can't get this value back. So we
// need to store it somewhere for retrieval even after the editor closes.
saveValue () {
if ( this.mathValue != this.mathLiveEditor?.value ) {
this.mathValue = this.mathLiveEditor?.value
this.dialog.onChange( this.dialog, this )
}
}
// Internal use only; how to fetch the value stored by saveValue().
savedValue () { return this.mathValue }
/**
* Set the current contents of the editor to the expression represented in
* the given LaTeX content.
*
* @param {string} value - LaTeX content to be put into the MathLive editor
*/
setValue ( value ) {
this.mathLiveEditor.value = value
this.saveValue()
}
// Called whenever the dialog is shown (or, if this item is inside a tab,
// whenever that tab is shown). This is what initializes the MathLive
// editor into the DIV created by the `json()` method, and installs an
// event handler that calls saveValue() whenever an edit takes place.
onShow () {
if ( this.setupInitiated ) return
this.setupInitiated = true
loadMathFieldClass().then( () => {
document.body.style.setProperty( '--keyboard-zindex', '2000' )
this.mathLiveEditor = new MathfieldElement()
this.mathLiveEditor.value = this.dialog.json.initialData[this.name] || ''
this.mathLiveEditor.style.width = '100%'
this.mathLiveEditor.style.border = 'solid 1px #cccccc'
const insertHere = document.getElementById( `math-${this.name}` )
while ( insertHere.firstChild )
insertHere.removeChild( insertHere.firstChild )
insertHere.appendChild( this.mathLiveEditor )
delete this.mathLiveEditor.shadowRoot.querySelector(
'.ML__virtual-keyboard-toggle' ).dataset['ml__tooltip']
this.saveValue()
this.mathLiveEditor.addEventListener( 'input',
() => this.saveValue() )
if ( this.focusWhenShown )
this.mathLiveEditor.focus()
} )
}
/**
* Get the saved value of the equation editor, as a LaTeX string.
*
* @param {string} key - the key whose value should be looked up
* @returns {string?} the value associated with the given key, if the key
* is the one that this item is in charge of editing, or undefined
* otherwise
*/
get ( key ) { if ( key == this.name ) return this.savedValue() }
}
// Internal use only.
// MathJSON is a format invented by the author of MathLive, and documented here:
// https://cortexjs.io/math-json/
//
// This function converts the given MathJSON structure to putdown notation.
// It is not exported by this module. Instead, to be able to use it, you should
// asynchronously construct a converter object using getConverter(), below.
const mathJSONToPutdown = json => {
// MathJSON numbers come in 3 formats:
// 1. plain numbers
if ( !isNaN( json ) ) return `${json}`
// 2. object literals with a "num" field
if ( json.num ) return `${json.num}`
// 3. a string starting with +, -, or a digit 0-9
if ( ( typeof( json ) == 'string' ) && '+-0123456789'.includes( json[0] ) )
return json
// MathJSON strings come in 2 formats:
// 1. a string with a leading and trailing apostrophe
if ( ( typeof( json ) == 'string ' )
&& ( json[0] == '\'' ) && ( json[json.length - 1] == '\'' ) )
return JSON.parse( json )
// 2. object literals with a "str" field
if ( json.str ) return json.str
// MathJSON symbols come in 2 formats:
// 1. a string that doesn't match the format for strings given above
if ( typeof( json ) == 'string' ) return json
// 2. an object literal with a "sym" field
if ( json.sym ) return json.sym
// MathJSON function applications come in 3 formats:
// 1. a JavaScript array
if ( json instanceof Array )
return '(' + json.map( mathJSONToPutdown ).join( ' ' ) + ')'
// 2. object literals with a "fn" field
if ( json.fn )
return '(' + json.fn.map( mathJSONToPutdown ).join( ' ' ) + ')'
// MathJSON also supports dictionaries, but putdown does not.
// So every other kind of MathJSON object just gets called "unsupported":
return `(unsupported_MathJSON ${JSON.stringify( json )})`
}
/**
* An array of names of all the input formats known by the converter defined in
* {@link module:MathLive.getConverter getConverter()}.
*/
export const inputFormats = [ 'latex', 'mathjson', 'asciimath' ]
/**
* An array of names of all the output formats known by the converter defined in
* {@link module:MathLive.getConverter getConverter()}.
*/
export const outputFormats = [ 'html', 'putdown', ...inputFormats ]
// Internal use only
// LaTeX commands not parseable by MathLive, but that we need to have parsed.
// This string is used later to convert all such commands in to plain \text{...}
// blocks, so that they go through the MathLive parser, and we can manipulate
// them after the JSON/putdown/LC form has been created.
const unsupportedLatexCommands = `forall exists`
/**
* A converter is a function with the following signature:
* `convert( data, 'input format', 'output format' )`.
* For example, to convert some $\LaTeX$ code to putdown, you might do the
* following.
*
* ```js
* getConverter().then( convert => {
* const putdown = convert( someLaTeXString, 'latex', 'putdown' )
* console.log( putdown )
* } )
* ```
*
* Thre are five formats that this function knows about. Two are output-only
* formats: `'html'` and `'putdown'`. You cannot convert from these formats
* into any other format. The other three are storage formats: `'latex'`,
* `'mathjson'`, and `'asciimath'`. The formats are case-insensitive, so you
* can write `'LaTeX'` or `'MathJSON'` instead if you like. All of the three
* storage formats can be converted into one another, and into any of the
* output formats. So the only constraint is that the output format cannot be
* `'html'` or `'putdown'`.
*
* The reason that this function is asynchronous is because some of those
* conversion functions can be run only once MathLive has been loaded, and so
* this function ensures that has happened before returning to you a converter
* instance. That way, the instance you receive is guaranteed to work
* immediately, and all of its methods can be synchronous. Note that many of
* the conversion functions are built into MathLive, and I'm simply making them
* available here and connecting them in all the transitive ways the user might
* need them.
*
* @returns {Promise} a promise that resolves to a function that behaves as above
* @function
*/
export const getConverter = () => loadMathFieldClass().then( () => {
// Define the function here that we will return, so that we have a name to
// us inside of it for recursive calls.
const convert = ( data, inputFormat, outputFormat ) => {
// Ensure that .json doesn't convert 1/2 to "Half"
MathfieldElement.computeEngine.jsonSerializationOptions.exclude = [ 'Half' ]
// (Tried to put that code outside the function but it was too soon; not sure why.)
inputFormat = inputFormat.toLowerCase()
outputFormat = outputFormat.toLowerCase()
if ( !inputFormats.includes( inputFormat ) )
throw new Error( `Invalid input format: ${inputFormat}` )
if ( !outputFormats.includes( outputFormat ) )
throw new Error( `Invalid output format: ${outputFormat}` )
// handle trivial case
if ( inputFormat == outputFormat )
return data
// handle all other cases
switch ( `${inputFormat} ${outputFormat}` ) {
case 'latex putdown':
return convert( convert( data, 'latex', 'mathjson' ),
'mathjson', 'putdown' )
case 'latex html':
return MathLive.convertLatexToMarkup( data )
case 'latex mathjson':
unsupportedLatexCommands.trim().split( /\s+/ )
.forEach( latexCommand =>
data = data.replace(
RegExp( '\\\\'+latexCommand+'\\b', 'g' ),
`\\text{${latexCommand}}` ) )
return MathfieldElement.computeEngine.parse(
data, { canonical: false } ).json
case 'latex asciimath':
return MathLive.convertLatexToAsciiMath( data )
case 'mathjson latex':
return MathLive.serializeMathJsonToLatex( data )
case 'mathjson html':
return convert( convert( data, 'mathjson', 'latex' ),
'latex', 'putdown' )
case 'mathjson putdown':
return mathJSONToPutdown( data )
case 'mathjson asciimath':
return convert( convert( data, 'mathjson', 'latex' ),
'latex', 'asciimath' )
case 'asciimath latex':
return asciiMathToLatex( data )
// Handle unnecessary brackets around symbols:
// (These can cause spacing problems in some cases.)
.replace( /[{](\w)[}]/g, ' $1 ' )
// MathLive-specific preferences for special sets:
.replace( /\\mathbb\s+Q/g, '\\Q' )
.replace( /\\mathbb\s+R/g, '\\R' )
.replace( /\\mathbb\s+C/g, '\\C' )
.replace( /\\mathbb\s+Z/g, '\\Z' )
.replace( /\\mathbb\s+N/g, '\\N' )
case 'asciimath html':
return convert( convert( data, 'asciimath', 'latex' ),
'latex', 'html' )
case 'asciimath putdown':
return convert( convert( data, 'asciimath', 'mathjson' ),
'mathjson', 'putdown' )
case 'asciimath mathjson':
return convert( convert( data, 'asciimath', 'latex' ),
'latex', 'mathjson' )
default: throw new Error(
`Unsupported conversion: ${inputFormat} -> ${outputFormat}` )
}
}
// Return the function we just built.
return convert
} )
source