Skip to content
80 changes: 50 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ This library works well as a bridge between [MathQuill](http://mathquill.com/) a
## TeX Features

* Common operators available in TeX math mode: `+`, `-`, `*`, `^`, `/`, `\cdot`, `||` (absolute value), `\times` (cross product)
* Comparison operators: `=`, `\ne`/`\neq`, `<`, `>`, `\le`/`\leq`, `\ge`/`\geq`
* Assignment operator: `:=`
* Basic functions: `\sqrt`, `\frac`, `\sin`, `\cos`, `\tan`, `\csc`, `\sec`, `\cot`, `\arcsin`, `\arccos`, `\arctan`, `\log`, `\ln`, `\det`
* Custom functions implemented with MathJS: `eigenvectors`, `eigenvalues`, `cross`, `proj`, `comp`, `norm`, `inv`
* Since these are custom functions, they should be formatted as `\operatorname{function}` in TeX.
* Constants: `\pi`, `e`
* Functions with custom bases: `\sqrt[n]`, `\log_n`
* Custom functions implemented with MathJS: `eigenvectors`, `eigenvalues`, `cross`, `proj`, `comp`, `norm`, `inv`, ...
* Since these are custom functions, they should be formatted as `\operatorname{function}` in TeX.
* Constants: `\pi`, `e`, `\i`, `{True}`, `{False}`, `{?}` (undefined), `\infty`
* Environments: `matrix`
* Variables
* `^T` is interpreted as the transpose operation
* Variables (including greek symbols): `x`, `y`, `a`, `\alpha`, `\theta`, ...
* `^T` is interpreted as the transpose operation
* Non-latin symbols are converted to their English spellings (`\alpha` in TeX becomes the MathJS symbol `alpha`)
* Variable subscripts: `a_b`, `c_{max}`

## Browser Support

Expand All @@ -23,7 +28,7 @@ Any browser with ES6 support.

Install with NPM:

```
```bash
npm install tex-math-parser
```

Expand All @@ -38,6 +43,7 @@ or link to it from a CDN:
Given the following TeX source string:

![Example TeX](docs/imgs/example_tex.png)

```latex
\begin{bmatrix}1&3\\2&4\end{bmatrix}\begin{bmatrix}-5\\-6\end{bmatrix}+\left|\sqrt{7}-\sqrt{8}\right|^{\frac{9}{10}}\begin{bmatrix}\cos\left(\frac{\pi}{6}\right)\\\sin\left(\frac{\pi}{6}\right)\end{bmatrix}
```
Expand All @@ -59,12 +65,14 @@ console.log(texAnswer);
// \begin{bmatrix}-22.812481734548864\\-33.89173627896382\\\end{bmatrix}
```

Parse the string and get a [a MathJS expression tree](https://mathjs.org/docs/expressions/expression_trees.html):
Parse the string and get [a MathJS expression tree](https://mathjs.org/docs/expressions/expression_trees.html):

```javascript
const mathJSTree = parseTex(escapedTex);
```

### Variables

If the TeX string contains variables, the value of the variables must be supplied when evaluating.

![Example TeX with variables](docs/imgs/example_tex_variables.png)
Expand All @@ -79,15 +87,15 @@ console.log(answer); // 1

`evaluateTex(texStr: string, scope?: Object)`

Evaluate a TeX string, replacing any variable occurences with their values in `scope`. The answer is returned as a TeX string.
Evaluate a TeX string, replacing any variable occurrences with their values in `scope`. The answer is returned as a TeX string.

`parseTex(texStr: string)`

Convert a TeX string into [a MathJS expression tree](https://mathjs.org/docs/expressions/expression_trees.html). The function returns the root node of the tree.

## Contributing

Please feel free to make a PR and add any features, add unit tests, or refactor any of the code. Both `tokenizeTex` and the `Parser` are quite messy and could really use a clean-up (maybe someday I'll get around to it...).
Please feel free to make a PR and add any features, add unit tests, or refactor any of the code. Both `tokenizeTex` and the `Parser` are quite messy and could really use a clean-up (maybe someday I'll get around to it...).

Run `pnpm test` to run some unit tests and make sure they're passing!

Expand All @@ -99,36 +107,48 @@ TODO: include better documentation on how to do this

`parseTex` first lexes the TeX string into tokens, which are then passed to the parser to create the expression tree. A context-free grammar for the simplified version of TeX math used by the parser is as follows:

```
expr = term ((PLUS | MINUS) term)*
```text
comp => expr ((EQUALS | NOTEQUALS | LESS | LESSEQUAL | GREATER | GREATEREQUAL) expr)*
| VARIABLE EQUALS EQUALS comp

term = factor ((CDOT factor | primary )* // primary and factor must both not be NUMBERs
expr => term ((PLUS | MINUS) term)*

factor = MINUS? power
term => factor ((STAR factor | primary))* // primary and factor must both not be numbers

power = primary (CARET primary)*
factor => MINUS? power

primary = grouping
| environnment
| frac
| function
| NUMBER
| VARIABLE
power => primary (CARET primary)*

grouping = LEFT LPAREN expr RIGHT RPAREN
| LPAREN expr RPAREN
| LBRACE expr RBRACE
| LEFT BAR expr RIGHT BAR
| BAR expr BAR
primary => grouping
| environnment
| frac
| sqrt
| log
| function
| NUMBER
| VARIABLE

environnment = matrix
grouping => LEFT LPAREN comp RIGHT RPAREN
| LPAREN comp RPAREN
| LBRACE comp RBRACE
| LEFT BAR comp RIGHT BAR
| BAR comp BAR

frac = FRAC LBRACE expr RBRACE LBRACE expr RBRACE
environnment => matrix

function = (SQRT | SIN | COS | TAN ...) grouping
frac => FRAC LBRACE comp RBRACE LBRACE comp RBRACE

matrix = BEGIN LBRACE MATRIX RBRACE ((expr)(AMP | DBLBACKSLASH))* END LBRACE MATRIX RBRACE
matrix => BEGIN LBRACE MATRIX RBRACE ((comp)(AMP | DBLBACKSLASH))* END LBRACE MATRIX RBRACE

sqrt => SQRT (LBRACKET comp RBRACKET)? argument

log => LOG (UNDERSCORE (primary))? argument

function => (SIN | COS | TAN | ...) argument
| OPNAME LBRACE customfunc RBRACE argument

argument => grouping
| primary
```

As the grammar is not left-recursive, the parser was implemented as a recursive descent parser with each production being represented by a separate function. This keeps the parser easily extensible.

125 changes: 123 additions & 2 deletions src/Token.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
export const enum TokenType {
Number,
Variable,
Symbol,
Colon,
Equals,
Notequals,
Less,
Greater,
Lessequal,
Greaterequal,
Plus,
Minus,
Star,
Times,
Slash,
Caret,
Underscore,
Comma,
Lbrace,
Rbrace,
Lparen,
Rparen,
Lbracket,
Rbracket,
Bar,
Amp,
Dblbackslash,
Expand All @@ -32,7 +42,11 @@ export const enum TokenType {
Tanh,
Log,
Ln,
Pi,
Mathrm,
True,
False,
Mathbf,
Undefined,
E,
Begin,
End,
Expand All @@ -54,21 +68,36 @@ export const enum TokenType {
}

export const lexemeToType: { [key: string]: TokenType } = {
':': TokenType.Colon,
'=': TokenType.Equals,
'\\ne': TokenType.Notequals,
'\\neq': TokenType.Notequals,
'<': TokenType.Less,
'>': TokenType.Greater,
'\\le': TokenType.Lessequal,
'\\leq': TokenType.Lessequal,
'\\ge': TokenType.Greaterequal,
'\\geq': TokenType.Greaterequal,
'+': TokenType.Plus,
'-': TokenType.Minus,
'*': TokenType.Star,
'\\cdot': TokenType.Star,
'\\times': TokenType.Times,
'^': TokenType.Caret,
_: TokenType.Underscore,
'/': TokenType.Slash,
',': TokenType.Comma,
'{': TokenType.Lbrace,
'}': TokenType.Rbrace,
'(': TokenType.Lparen,
')': TokenType.Rparen,
'[': TokenType.Lbracket,
']': TokenType.Rbracket,
'|': TokenType.Bar,
'&': TokenType.Amp,
True: TokenType.True,
False: TokenType.False,
'?': TokenType.Undefined,
bmatrix: TokenType.Matrix,
'\\\\': TokenType.Dblbackslash,
'\\sqrt': TokenType.Sqrt,
Expand All @@ -87,8 +116,9 @@ export const lexemeToType: { [key: string]: TokenType } = {
'\\tanh': TokenType.Tanh,
'\\log': TokenType.Log,
'\\ln': TokenType.Ln,
'\\pi': TokenType.Pi,
e: TokenType.E,
'\\mathrm': TokenType.Mathrm,
'\\mathbf': TokenType.Mathbf,
'\\begin': TokenType.Begin,
'\\end': TokenType.End,
'\\left': TokenType.Left,
Expand All @@ -105,12 +135,93 @@ export const lexemeToType: { [key: string]: TokenType } = {
inv: TokenType.Inv,
};

export const lexemeToSymbol: { [key: string]: string } = {
// Greek letters
'\\Alpha': 'Alpha',
'\\alpha': 'alpha',
'\\Beta': 'Beta',
'\\beta': 'beta',
'\\Gamma': 'Gamma',
'\\gamma': 'gamma',
'\\Delta': 'Delta',
'\\delta': 'delta',
'\\Epsilon': 'Epsilon',
'\\epsilon': 'epsilon',
'\\varepsilon': 'varepsilon',
'\\Zeta': 'Zeta',
'\\zeta': 'zeta',
'\\Eta': 'Eta',
'\\eta': 'eta',
'\\Theta': 'Theta',
'\\theta': 'theta',
'\\vartheta': 'vartheta',
'\\Iota': 'Iota',
'\\iota': 'iota',
'\\Kappa': 'Kappa',
'\\kappa': 'kappa',
'\\varkappa': 'varkappa',
'\\Lambda': 'Lambda',
'\\lambda': 'lambda',
'\\Mu': 'Mu',
'\\mu': 'mu',
'\\Nu': 'Nu',
'\\nu': 'nu',
'\\Xi': 'Xi',
'\\xi': 'xi',
'\\Omicron': 'Omicron',
'\\omicron': 'omicron',
'\\Pi': 'Pi',
'\\pi': 'pi',
'\\varpi': 'varpi',
'\\Rho': 'Rho',
'\\rho': 'rho',
'\\varrho': 'varrho',
'\\Sigma': 'Sigma',
'\\sigma': 'sigma',
'\\varsigma': 'varsigma',
'\\Tau': 'Tau',
'\\tau': 'tau',
'\\Upsilon': 'Upsilon',
'\\upsilon': 'upsilon',
'\\Phi': 'Phi',
'\\phi': 'phi',
'\\varphi': 'varphi',
'\\Chi': 'Chi',
'\\chi': 'chi',
'\\Psi': 'Psi',
'\\psi': 'psi',
'\\Omega': 'Omega',
'\\omega': 'omega',
// Comparisons
'\\ne': '!=',
'\\neq': '!=',
'\\le': '<=',
'\\leq': '<=',
'\\ge': '>=',
'\\geq': '>=',
// Operators
'\\frac': '/',
'\\cdot': '*',
// Other
'\\i': 'i',
'\\infty': 'Infinity',
'\\lim': 'lim',
};

// TODO: Make conversions consistent with those in mathjs src/utils/latex.js

/**
* A mapping from a token type to the operation it represents.
* The operation is the name of a function in the mathjs namespace,
* or of a function to be defined in scope (i.e. in the argument to math.evaluate())
*/
export const typeToOperation: { [key in TokenType]?: string } = {
[TokenType.Equals]: 'equal',
[TokenType.Notequals]: 'unequal',
[TokenType.Less]: 'smaller',
[TokenType.Greater]: 'larger',
[TokenType.Lessequal]: 'smallerEq',
[TokenType.Greaterequal]: 'largerEq',
[TokenType.Plus]: 'add',
[TokenType.Minus]: 'subtract',
[TokenType.Star]: 'multiply',
Expand Down Expand Up @@ -144,6 +255,16 @@ export const typeToOperation: { [key in TokenType]?: string } = {
[TokenType.Inv]: 'inv',
};

/**
* A mapping from a token type to the operation it represents for multiple variables.
* The operation is the name of a function in the mathjs namespace,
* or of a function to be defined in scope (i.e. in the argument to math.evaluate())
*/
export const typeToMultivarOperation: { [key in TokenType]?: string } = {
[TokenType.Sqrt]: 'nthRoot',
[TokenType.Log]: 'log',
};

interface Token {
lexeme: string;
type: TokenType;
Expand Down
18 changes: 13 additions & 5 deletions src/customMath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,19 @@ const math = create(all, {
const mathImport = {
lastFn: '',
lastArgs: [],
eigenvalues: (matrix: any) => math.eigs(matrix).values,
eigenvectors: (matrix: any) => math.eigs(matrix).eigenvectors,
comp: (a: any, b: any) => math.divide(math.dot(a, b), math.norm(a)), // component of b along a
proj: (a: any, b: any) => math.multiply(math.divide(a, math.norm(a)),
math.divide(math.dot(a, b), math.norm(a))), // projection of b along a
eigenvalues: (matrix: math.MathCollection) => math.eigs(matrix).values,
eigenvectors: (matrix: math.MathCollection) => math.eigs(matrix).eigenvectors,
comp: (
a: math.MathCollection,
b: math.MathCollection,
) => math.divide(math.dot(a, b), math.norm(a)), // component of b along a
proj: (
a: math.MathCollection,
b: math.MathCollection,
) => math.multiply(
math.divide(a, math.norm(a)),
math.divide(math.dot(a, b), math.norm(a)),
), // projection of b along a
};

math.import(mathImport, {
Expand Down
Loading