├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── package.json ├── rollup.config.js ├── src ├── components │ ├── Equation.stories.tsx │ ├── Equation.tsx │ ├── EquationContext │ │ ├── index.tsx │ │ ├── isComparison.ts │ │ ├── isEqualPlaceholder.ts │ │ └── stories.tsx │ ├── EquationEvaluate.stories.tsx │ ├── EquationEvaluate.tsx │ ├── EquationEvaluatePreparsed.stories.tsx │ ├── EquationEvaluatePreparsed.tsx │ ├── EquationOptions.tsx │ ├── EquationPreparsed.stories.tsx │ ├── EquationPreparsed.tsx │ ├── StoryRefLogger.tsx │ ├── context.ts │ └── useEquationOptions.tsx ├── errorHandler.tsx ├── generic.stories.tsx ├── index.tsx ├── rendering │ ├── StoryEquationWrapper.tsx │ ├── Wrapper.tsx │ ├── block │ │ ├── index.tsx │ │ └── stories.tsx │ ├── comparison.stories.tsx │ ├── fraction │ │ ├── index.tsx │ │ └── stories.tsx │ ├── func │ │ ├── index.tsx │ │ └── stories.tsx │ ├── index.tsx │ ├── matrix │ │ ├── index.tsx │ │ └── stories.tsx │ ├── operator.stories.tsx │ ├── parens │ │ └── index.tsx │ ├── power │ │ ├── index.tsx │ │ └── stories.tsx │ ├── special │ │ ├── abs │ │ │ ├── index.tsx │ │ │ └── stories.tsx │ │ ├── root │ │ │ ├── index.tsx │ │ │ └── stories.tsx │ │ ├── sqrt │ │ │ ├── index.tsx │ │ │ ├── root-symbol.tsx │ │ │ └── stories.tsx │ │ └── sum │ │ │ ├── index.tsx │ │ │ └── stories.tsx │ └── variable │ │ ├── index.tsx │ │ └── stories.tsx ├── types │ ├── EquationRenderError.ts │ ├── ErrorHandler.tsx │ ├── RenderOptions.ts │ ├── Rendering.ts │ └── RenderingPart.ts └── utils │ ├── joinClasses.ts │ ├── throwUnknownType.ts │ └── unionArrays.tsx ├── storybook ├── main.js ├── preview.js └── styles.css ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | 5 | parserOptions: { 6 | ecmaVersion: 6, 7 | sourceType: 'module', 8 | ecmaFeatures: { 9 | jsx: true, 10 | }, 11 | }, 12 | 13 | extends: [ 14 | 'eslint:recommended', 15 | 'plugin:react/recommended', 16 | 'plugin:@typescript-eslint/recommended', 17 | ], 18 | plugins: [ 19 | 'react', 20 | '@typescript-eslint', 21 | ], 22 | env: { 23 | browser: true, 24 | }, 25 | rules: { 26 | 'quotes': ['error', 'single', { 'allowTemplateLiterals': true }], 27 | 'semi': ['error', 'never'], 28 | 'comma-dangle': ['error', { 29 | 'arrays': 'always-multiline', 30 | 'objects': 'always-multiline', 31 | 'imports': 'always-multiline', 32 | 'exports': 'always-multiline', 33 | 'functions': 'always-multiline', 34 | }], 35 | 36 | '@typescript-eslint/member-delimiter-style': ['error', { 37 | 'multiline': { 38 | 'delimiter': 'comma', 39 | 'requireLast': true, 40 | }, 41 | 'singleline': { 42 | 'delimiter': 'comma', 43 | 'requireLast': false, 44 | }, 45 | 'overrides': { 46 | 'interface': { 47 | 'multiline': { 48 | 'delimiter': 'none', 49 | 'requireLast': false, 50 | }, 51 | 'singleline': { 52 | 'delimiter': 'semi', 53 | 'requireLast': false, 54 | }, 55 | }, 56 | }, 57 | }], 58 | // Allow using rest destructuring to omit variables without complaint 59 | '@typescript-eslint/no-unused-vars': ['error', { 'ignoreRestSiblings': true }], 60 | 61 | 'no-undef': 'off', 62 | 63 | // does not work with ts-parser because of comments 64 | 'no-empty': 'off', 65 | 66 | '@typescript-eslint/indent': ['error', 4, { SwitchCase: 1 }], 67 | '@typescript-eslint/no-use-before-define': 'off', 68 | '@typescript-eslint/explicit-function-return-type': 'off', 69 | '@typescript-eslint/no-explicit-any': 'off', 70 | '@typescript-eslint/no-misused-new': 'off', 71 | '@typescript-eslint/prefer-interface': 'off', 72 | '@typescript-eslint/generic-type-naming': 'off', 73 | '@typescript-eslint/explicit-member-accessibility': 'off', 74 | // Is triggered by , which is necessary for generics in .tsx files 75 | '@typescript-eslint/no-unnecessary-type-constraint': 'off', 76 | // This just causes noise, the problems highlighted aren't actually problems 77 | '@typescript-eslint/ban-types': 'off', 78 | }, 79 | } 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | docs 4 | dist 5 | deploy-key 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `react-equation` – Parse, evaluate and render equation from plain text 2 | 3 | A react render-layer for [`equation-parser`](https://github.com/kgram/equation-parser) and [`equation-resolver`](https://github.com/kgram/equation-resolver). 4 | 5 | 6 | ## Quick-start 7 | Install the package. 8 | 9 | ``` 10 | npm install -S react-equation equation-resolver 11 | ``` 12 | or 13 | ``` 14 | yarn add react-equation equation-resolver 15 | ``` 16 | 17 | Start rendering equations 18 | 19 | ```jsx 20 | import React from 'react' 21 | import ReactDOM from 'react-dom' 22 | import { Equation, EquationEvaluate, EquationContext, EquationOptions, defaultErrorHandler } from 'react-equation' 23 | import { defaultVariables, defaultFunctions } from 'equation-resolver' 24 | 25 | ReactDOM.render(( 26 | <> 27 | 32 | 35 | ( 37 |

38 | Here, we define that {equation('a = 5')}, and can then evaluate that {equation('7 / a = ')} 39 |

40 | )} 41 | /> 42 |
43 | 44 | ), document.getElementById("root")); 45 | ``` 46 | 47 | Some more example can be seen in this sandbox: https://codesandbox.io/s/react-equation-example-t0oe8 48 | 49 | A simple playground built around `EquationContext` can be found here: https://codesandbox.io/s/react-equation-playground-w5q2en 50 | 51 | ## Introduction 52 | 53 | This package is intended to render text-equations For help with equation structure, see [`equation-parser`](https://github.com/kgram/equation-parser#general-format). 54 | 55 | ### Equations in context 56 | The most straight-forward way to render equations is to use the [`EquationContext`](#equationcontext) component. This component is made to render a series of interconnected equations and functions, evaluated in order, and will generally try to figure out what you want based on the form of the equation. 57 | 58 | The `equation`-function provided to the `render`-prop allows you to render simple expressions, but also supports more complex patterns through a (hopefully) intuitive syntax: 59 | 60 | * Evaluate an expression: By ending the equation with equals and optionally a placeholder (`_`) and unit, the evaluated result is shown. 61 | * `5 * 2 =` 62 | * `22/7=_` 63 | * `0.2m * 0.7m = _ cm^2` 64 | * Assign a variable: By following a variable name with equals, the value will be available as a variable in all subsequent calls to `equation`. This can be combined with the evaluate-rules above to show the value of the variable. 65 | * `a = 5` 66 | * `b = 2a =` 67 | * `c = (100% + 2%)^3 = _ %` 68 | * Assign a function: Assigning a function-call where every argument is a variable-name will make it available as a function for subsequent calls to `equation`. The function can use variables and functions as defined when it itself is defined. 69 | 70 | If you need variables or functions defined without them being shown, you can simply call `equation` without using the return value. 71 | 72 | ### Direct rendered components 73 | If you don't need context, or need more control over the components or bundling, you can use components directly. A component will only include the parser and evaluator when necessary, enabling efficient tree-shaking in simple scenarios. 74 | 75 | ### Variables and functions 76 | It is necessary to manually include variables and functions. This is to allow changing the names for localization purposes, and omitting unnecessary code for bundle optimization. 77 | 78 | The functions included can be found in [`equation-resolver`](https://github.com/kgram/equation-resolver#defaultfunctions). There is special rendering for `sqrt`, `root`, `abs` and `sum`. 79 | 80 | The variables are only listed [in the raw source](https://github.com/kgram/equation-resolver/blob/master/src/defaultVariables.ts), since there's quite a few of them. They should hopefully cover anything one could want to define, but if something is missing or wrong, please create an issue. 81 | 82 | ## Components 83 | All the included components are split up in the interest of allowing tree-shaking to reduce the bundle-size by avoiding either the parser, the resolver or both, depending on needs. 84 | 85 | All the components can (when applicable) have props `variables`, `functions`, `simplifyableUnits` (see [`equation-resolver`](https://github.com/kgram/equation-resolver)), `errorHandler` (see section on error handling), `className` and `style`. These props can also be passed along through the `EquationOptions` context-provider. 86 | 87 | ### `Equation` 88 | Parse the string provided as `value` and render it. No evaluation is done. 89 | 90 | Exposes as ref: 91 | 92 | ```ts 93 | type RefValue = { 94 | /** Equation is valid */ 95 | valid: boolean, 96 | /** Parsed equation */ 97 | equation: EquationNode | EquationParserError, 98 | } 99 | ``` 100 | 101 | Example: 102 | 103 | ```jsx 104 | 107 | // Renders a prettified 2 + a 108 | ``` 109 | 110 | ### `EquationEvaluate` 111 | Parse the string provided as `value`, evaluate it and render the formatted equation. 112 | 113 | Exposes as ref: 114 | 115 | ```ts 116 | type RefValue = { 117 | /** Equation and result valid */ 118 | valid: boolean, 119 | /** Parsed equation */ 120 | equation: EquationNode | EquationParserError, 121 | /** Parsed equation for the display unit */ 122 | unitEquation: EquationNode | EquationParserError | null, 123 | /** Evaluated result of the equation */ 124 | result: ResultNode | ResultResolveError, 125 | /** Evaluated result of the unit passed */ 126 | unitResult: ResultNode | ResultResolveError | null, 127 | /** Equation combined with result expressed as unit */ 128 | resultEquation: EquationNode | EquationParserError | EquationResolveError | EquationRenderError, 129 | } 130 | ``` 131 | 132 | Example: 133 | 134 | ```jsx 135 | 139 | // Renders a prettified 2 + a = 7 140 | ``` 141 | 142 | ### `EquationPreparsed` 143 | Render a pre-parsed equation provided as `value`. No evaluation is done. This is mostly useful for building functionality on top of this library. 144 | 145 | Example: 146 | 147 | ```jsx 148 | 151 | // Renders a prettified 2 + a 152 | ``` 153 | 154 | ### `EquationEvaluatePreparsed` 155 | Evaluate a pre-parsed equation provided as `value` and render the formatted equation. This is mostly useful for building functionality on top of this library. 156 | 157 | Exposes as ref: 158 | 159 | ```ts 160 | type RefValue = { 161 | /** Equation can be evaluated */ 162 | valid: boolean, 163 | /** Evaluated result of the equation */ 164 | result: ResultNode | ResultResolveError, 165 | /** Evaluated result of the unit passed */ 166 | unitResult: ResultNode | ResultResolveError | null, 167 | /** Equation combined with result expressed as unit */ 168 | resultEquation: EquationNode | EquationParserError | EquationResolveError, 169 | } 170 | ``` 171 | 172 | Example: 173 | 174 | ```jsx 175 | 179 | // Renders a prettified 2 + a = 7 180 | ``` 181 | 182 | ### `EquationContext 183 | Render multiple, interconnected equations, variables and functions. Variables 184 | and functions can be defined by simply assigning them (`x=2`, `f(x)=x^2`), and 185 | expressions are evalutaed by ending them on an equals-sign (`2*3=`). Force 186 | conversion to a specific unit by ending on equals underscore-placeholder and the 187 | unit (`25in = _cm`). 188 | 189 | ```tsx 190 | ( 191 | <> 192 | {equation('a = 2')} Renders a = 2 and defines a 193 | {equation('b = 5a =')} Renders b = 5a = 10 and defines b 194 | {equation('c = 1/b = _ %')} Renders c = 1/b = 10% and defines c 195 | {equation('f(x) = x^2')} Renders f(x) = x^2 and defines f(x) 196 | {equation('2a + f(a) =')} Renders 2a + f(a) = 8 197 | 198 | )} /> 199 | ``` 200 | 201 | It is important to note, that since order matters, the equation-function from 202 | this component should not be passed to other components. Instead, use 203 | `EquationOptions` and the `getOptions` helper. 204 | 205 | ```tsx 206 | ( 207 | <> 208 | {equation('2x =')} Renders Unknown variable 'x' 209 | Renders Unknown variable 'x' 210 | 211 | {equation('x = 7')} Renders x = 7 212 | {equation('2x =')} Renders 2x = 14 213 | Renders Unknown variable 'x', not part of the context 214 | 215 | Renders 2x = 14 216 | 217 | Renders 2x = 14 218 | 219 | 220 | )} /> 221 | ``` 222 | 223 | ## Error handling 224 | An error handler should be added either to the `Equation`-components or to `EquationOptions`, otherwise the raw `errorType` of the error is shown. If english errors suffice, simply import `defaultErrorHandler`. If custom errors (for instance for localisation), see `./src/errorHandler.ts` for easy implementation. 225 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-equation", 3 | "version": "1.0.0", 4 | "description": "A react renderer for ASTs generated by equation-parser/equation-resolver", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "typings": "dist/index.d.ts", 8 | "sideEffects": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/kgram/react-equation.git" 12 | }, 13 | "homepage": "https://github.com/kgram/react-equation", 14 | "author": "Kristoffer Gram ", 15 | "license": "MIT", 16 | "scripts": { 17 | "storybook": "start-storybook -p 9001 -c storybook", 18 | "test": "echo No testing added", 19 | "lint": "eslint \"src/**/*.{ts,tsx}\" rollup.config.js", 20 | "types": "tsc -d --emitDeclarationOnly --outDir dist", 21 | "bundle": "rollup -c rollup.config.js", 22 | "prepare": "rimraf dist && yarn lint && yarn test && yarn types && yarn bundle" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "dependencies": { 28 | "equation-parser": "^1.0.0", 29 | "equation-resolver": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.18.6", 33 | "@babel/plugin-proposal-class-properties": "^7.18.6", 34 | "@babel/preset-env": "^7.18.6", 35 | "@babel/preset-react": "^7.18.6", 36 | "@babel/preset-typescript": "^7.18.6", 37 | "@storybook/addon-actions": "^6.5.9", 38 | "@storybook/react": "^6.5.9", 39 | "@types/jest": "^28.1.4", 40 | "@types/node": "^18.0.0", 41 | "@types/react": "^18.0.14", 42 | "@typescript-eslint/eslint-plugin": "^5.30.3", 43 | "@typescript-eslint/parser": "^5.30.3", 44 | "babel-loader": "^8.2.5", 45 | "eslint": "^8.19.0", 46 | "eslint-plugin-react": "^7.30.1", 47 | "eslint-plugin-react-hooks": "^4.6.0", 48 | "react": "~18.2.0", 49 | "react-dom": "~18.2.0", 50 | "rimraf": "^3.0.0", 51 | "rollup": "^2.75.7", 52 | "rollup-plugin-babel": "^4.3.3", 53 | "rollup-plugin-multi-input": "^1.3.1", 54 | "rollup-plugin-node-resolve": "^5.2.0", 55 | "typescript": "^4.7.4", 56 | "webpack": "^5.73.0" 57 | }, 58 | "peerDependencies": { 59 | "react": "~16.8.0" 60 | }, 61 | "babel": { 62 | "presets": [ 63 | "@babel/typescript", 64 | "@babel/env", 65 | "@babel/react" 66 | ], 67 | "plugins": [ 68 | "@babel/plugin-proposal-class-properties" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // import path from 'path' 2 | import babel from 'rollup-plugin-babel' 3 | import resolve from 'rollup-plugin-node-resolve' 4 | 5 | import pkg from './package.json' 6 | 7 | export default [ 8 | { 9 | input: 'src/index.tsx', 10 | output: [ 11 | { 12 | file: pkg.main, 13 | format: 'cjs', 14 | }, 15 | { 16 | file: pkg.module, 17 | format: 'esm', 18 | }, 19 | ], 20 | external: [ 21 | ...Object.keys(pkg.dependencies || {}), 22 | ...Object.keys(pkg.peerDependencies || {}), 23 | ], 24 | plugins: [ 25 | resolve({ 26 | extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'], 27 | }), 28 | babel({ 29 | exclude: 'node_modules/**', 30 | extensions: ['.tsx', '.ts', '.jsx', '.js'], 31 | }), 32 | ], 33 | watch: { 34 | chokidar: true, 35 | }, 36 | }, 37 | ] 38 | -------------------------------------------------------------------------------- /src/components/Equation.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { RefLogger } from './StoryRefLogger' 4 | import { Equation } from './Equation' 5 | 6 | export default { 7 | title: 'components/Equation', 8 | component: Equation, 9 | } 10 | 11 | export const RefValid = () => ( 12 | } 14 | /> 15 | ) 16 | 17 | export const RefInvalidEquation = () => ( 18 | } 20 | /> 21 | ) 22 | -------------------------------------------------------------------------------- /src/components/Equation.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useImperativeHandle, Ref, memo } from 'react' 2 | 3 | import { EquationNode, EquationParserError, parse } from 'equation-parser' 4 | 5 | import { RenderOptions } from '../types/RenderOptions' 6 | 7 | import { joinClasses } from '../utils/joinClasses' 8 | import { render } from '../rendering' 9 | 10 | import { useEquationOptions } from './useEquationOptions' 11 | 12 | type Props = RenderOptions & { 13 | /** Equation as text */ 14 | value: string, 15 | } 16 | 17 | type RefValue = { 18 | /** Equation is valid */ 19 | valid: boolean, 20 | /** Parsed equation */ 21 | equation: EquationNode | EquationParserError, 22 | } 23 | 24 | export const Equation = memo(forwardRef(({ value, errorHandler, className, style }: Props, ref: Ref) => { 25 | const { 26 | errorHandler: errorHandlerGlobal, 27 | className: classNameGlobal, 28 | style: styleGlobal, 29 | } = useEquationOptions() 30 | 31 | const equation = parse(value) 32 | 33 | useImperativeHandle(ref, () => ({ 34 | valid: equation.type !== 'parser-error', 35 | equation, 36 | })) 37 | 38 | return render( 39 | equation, 40 | { 41 | errorHandler: { ...errorHandlerGlobal, ...errorHandler }, 42 | className: joinClasses(classNameGlobal, className), 43 | style: { ...styleGlobal, ...style }, 44 | }, 45 | ) 46 | })) 47 | -------------------------------------------------------------------------------- /src/components/EquationContext/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { 4 | resolve, 5 | createResolverFunction, 6 | format, 7 | FormatOptions, 8 | formatPreresolved, 9 | } from 'equation-resolver' 10 | import { parse, EquationNodeVariable } from 'equation-parser' 11 | 12 | import { RenderOptions } from '../../types/RenderOptions' 13 | import { useEquationOptions } from '../useEquationOptions' 14 | import { EquationPreparsed } from '../EquationPreparsed' 15 | import { unionArrays } from '../../utils/unionArrays' 16 | import { isEqualPlaceholder } from './isEqualPlaceholder' 17 | import { isComparison } from './isComparison' 18 | 19 | type Props = Pick & Pick & { 20 | /** 21 | * Render content with equations interspersed. 22 | */ 23 | render: ( 24 | equation: (value: string) => JSX.Element, 25 | getOptions: () => FormatOptions & RenderOptions, 26 | ) => JSX.Element | null, 27 | } 28 | 29 | /** 30 | * Render multiple, interconnected equations, variables and functions. Variables 31 | * and functions can be defined by simply assigning them (`x=2`, `f(x)=x^2`), and expressions are 32 | * evalutaed by ending them on an equals-sign (`2*3=`). Force conversion to a specific 33 | * unit by ending on equals underscore-placeholder and the unit (`25in = _cm`). 34 | * 35 | * @example 36 | * ```tsx 37 | * ( 38 | * <> 39 | * {equation('a = 2')} Renders a = 2 and defines a 40 | * {equation('b = 5a =')} Renders b = 5a = 10 and defines b 41 | * {equation('c = 1/b = _ %')} Renders c = 1/b = 10% and defines c 42 | * {equation('f(x) = x^2')} Renders f(x) = x^2 and defines f(x) 43 | * {equation('2a + f(a) =')} Renders 2a + f(a) = 8 44 | * 45 | * )} /> 46 | * ``` 47 | * 48 | * It is important to note, that since order matters, the equation-function from 49 | * this component should not be passed to other components. Instead, use 50 | * `EquationOptions` and the `getOptions` helper. 51 | * 52 | * @example 53 | * ```tsx 54 | * ( 55 | * <> 56 | * {equation('2x =')} Renders Unknown variable 'x' 57 | * Renders Unknown variable 'x' 58 | * 59 | * {equation('x = 7')} Renders x = 7 60 | * {equation('2x =')} Renders 2x = 14 61 | * Renders Unknown variable 'x', not part of the context 62 | * 63 | * Renders 2x = 14 64 | * 65 | * Renders 2x = 14 66 | * 67 | * 68 | * )} /> 69 | * ``` 70 | */ 71 | export const EquationContext = ({ 72 | render, 73 | errorHandler, 74 | variables: localVariables, 75 | functions: localFunctions, 76 | simplifiableUnits: localSimplifiableUnits, 77 | }: Props) => { 78 | const { 79 | variables: globalVariables, 80 | functions: globalFunctions, 81 | simplifiableUnits: globalSimplifiableUnits, 82 | decimals: globalDecimals, 83 | ...options 84 | } = useEquationOptions() 85 | 86 | const functions = { ...globalFunctions, ...localFunctions} 87 | const variables = { ...globalVariables, ...localVariables} 88 | const simplifiableUnits = unionArrays(localSimplifiableUnits, globalSimplifiableUnits) 89 | 90 | return render( 91 | (equation) => { 92 | // Inject placeholder if equation ends in equals 93 | const node = parse(/=\s*$/.test(equation) ? equation + '_' : equation) 94 | // Handle parser-error early 95 | if (node.type === 'parser-error') { 96 | return 97 | } 98 | 99 | // Handle function assignment 100 | if ( 101 | node.type === 'equals' && 102 | node.a.type === 'function' && 103 | node.a.args.every((arg) => arg.type === 'variable') 104 | ) { 105 | const { name, args } = node.a 106 | // Add the function to the context 107 | functions[name] = createResolverFunction(args.map((arg) => (arg as EquationNodeVariable).name), node.b, { variables, functions }) 108 | 109 | return 110 | } 111 | 112 | // Handle variable assignment 113 | if ( 114 | node.type === 'equals' && 115 | node.a.type === 'variable' && 116 | (!isComparison(node.b) || isEqualPlaceholder(node.b)) 117 | ) { 118 | const { a: variable, b } = node 119 | const showResult = isEqualPlaceholder(b) 120 | const equation = showResult ? b.a : b 121 | 122 | const result = resolve(equation, { variables, functions }) 123 | // Add the variable to the context 124 | if (result.type !== 'resolve-error') { 125 | variables[variable.name] = result 126 | } 127 | if (showResult) { 128 | const unitResolved = b.b.type === 'operand-placeholder' 129 | ? null 130 | : resolve(b.b.b, { variables, functions }) 131 | const formatted = formatPreresolved( 132 | equation, 133 | b.b.type === 'operand-placeholder' ? null : b.b.b, 134 | result, 135 | unitResolved, 136 | { decimals: globalDecimals, simplifiableUnits }, 137 | ) 138 | 139 | switch (formatted.type) { 140 | case 'parser-error': return ( 141 | 142 | ) 143 | case 'resolve-error': return ( 144 | 145 | ) 146 | default: return ( 147 | 148 | ) 149 | } 150 | } else { 151 | return 152 | } 153 | } 154 | 155 | if (isEqualPlaceholder(node)) { 156 | const unit = node.b.type === 'operand-placeholder' ? null : node.b.b 157 | const formatted = format(node.a, unit, { variables, functions, simplifiableUnits, decimals: globalDecimals }) 158 | 159 | return 160 | } 161 | 162 | return 163 | }, 164 | () => ({ 165 | ...options, 166 | decimals: globalDecimals, 167 | variables: { ...variables }, 168 | functions: { ...functions }, 169 | simplifiableUnits, 170 | }), 171 | ) 172 | } 173 | -------------------------------------------------------------------------------- /src/components/EquationContext/isComparison.ts: -------------------------------------------------------------------------------- 1 | import { EquationNode } from 'equation-parser' 2 | 3 | const comparisons = [ 4 | 'equals', 5 | 'less-than', 6 | 'greater-than', 7 | 'less-than-equals', 8 | 'greater-than-equals', 9 | 'approximates', 10 | ] as const 11 | 12 | type Comparisons = typeof comparisons[number] 13 | 14 | export const isComparison = (node: EquationNode): node is Extract => ( 15 | comparisons.includes(node.type as Comparisons) 16 | ) 17 | -------------------------------------------------------------------------------- /src/components/EquationContext/isEqualPlaceholder.ts: -------------------------------------------------------------------------------- 1 | import { EquationNode, EquationNodeEquals } from 'equation-parser' 2 | import { EquationNodeMultiplyCross, EquationNodeMultiplyDot, EquationNodeMultiplyImplicit, EquationNodeOperandPlaceholder } from 'equation-parser/dist/EquationNode' 3 | 4 | type EquationNodeEqualPlaceholder = 5 | & EquationNodeEquals 6 | & { 7 | b: 8 | |EquationNodeOperandPlaceholder 9 | | ( 10 | & (EquationNodeMultiplyImplicit | EquationNodeMultiplyDot | EquationNodeMultiplyCross) 11 | & { a: EquationNodeOperandPlaceholder } 12 | ), 13 | } 14 | 15 | export const isEqualPlaceholder = (node: EquationNode): node is EquationNodeEqualPlaceholder => ( 16 | node.type === 'equals' && ( 17 | node.b.type === 'operand-placeholder' || (( 18 | node.b.type === 'multiply-implicit' || 19 | node.b.type === 'multiply-dot' || 20 | node.b.type === 'multiply-cross' 21 | ) && node.b.a.type === 'operand-placeholder') 22 | ) 23 | ) 24 | -------------------------------------------------------------------------------- /src/components/EquationContext/stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | 3 | import { EquationEvaluate } from '../EquationEvaluate' 4 | 5 | import { EquationContext } from '.' 6 | import { EquationOptions } from '../EquationOptions' 7 | 8 | export default { 9 | title: 'components/EquationContext', 10 | component: EquationContext, 11 | } 12 | 13 | const Wrapper = ({ children }: { children: ReactNode }) => ( 14 | {children} 15 | ) 16 | 17 | export const Equation = () => ( 18 | ( 20 | <> 21 |

{equation('a = 2')} Renders a = 2 and defines a

22 |

{equation('b = 5a =')} Renders b = 5a = 10 and defines b

23 |

{equation('c = 1/b = _ %')} Renders c = 1/b = 10% and defines c

24 |

{equation('f(x) = x^2')} Renders f(x) = x^2 and defines f(x)

25 |

{equation('2a + f(a) =')} Renders 2a + f(a) = 8

26 | 27 | )} 28 | /> 29 | ) 30 | 31 | export const GetOptions = () => ( 32 | ( 33 | <> 34 |

{equation('2x =')} Renders Unknown variable x

35 |

Renders Unknown variable x

36 | 37 |

{equation('x = 7')} Renders x = 7

38 |

{equation('2x =')} Renders 2x = 14

39 |

Renders Unknown variable x, not part of the context

40 | 41 |

Renders 2x = 14

42 | 43 |

Renders 2x = 14

44 |
45 | 46 | )} /> 47 | ) 48 | -------------------------------------------------------------------------------- /src/components/EquationEvaluate.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { RefLogger } from './StoryRefLogger' 4 | import { Equation } from './Equation' 5 | import { EquationEvaluate } from './EquationEvaluate' 6 | 7 | export default { 8 | title: 'components/EquationEvaluate', 9 | component: EquationEvaluate, 10 | } 11 | 12 | export const RefValid = () => ( 13 | } 15 | /> 16 | ) 17 | 18 | export const RefInvalidEquation = () => ( 19 | } 21 | /> 22 | ) 23 | 24 | export const RefInvalidResult = () => ( 25 | } 27 | /> 28 | ) 29 | 30 | export const RefInvalidVariablesEvaluated = () => ( 31 | } 33 | /> 34 | ) 35 | 36 | export const VariablesEvaluatedSimple = () => ( 37 | <> 38 |
39 | 42 |
43 |
44 | 50 |
51 | 52 | ) 53 | 54 | export const VariablesEvaluatedCascade = () => ( 55 | <> 56 |
57 | 60 |
61 |
62 | 65 |
66 |
67 | 74 |
75 | 76 | ) 77 | 78 | export const VariablesEvaluatedErrors = () => ( 79 | <> 80 |
81 | 84 |
85 |
86 | 89 |
90 |
91 | 99 |
100 | 101 | ) 102 | -------------------------------------------------------------------------------- /src/components/EquationEvaluate.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, memo, Ref, useImperativeHandle } from 'react' 2 | 3 | import { EquationNode, EquationParserError, parse } from 'equation-parser' 4 | import { resolve, formatPreresolved, wrapError, FormatOptions, ResultNode, ResultResolveError, EquationResolveError } from 'equation-resolver' 5 | 6 | import { EquationRenderError } from '../types/EquationRenderError' 7 | 8 | import { joinClasses } from '../utils/joinClasses' 9 | import { unionArrays } from '../utils/unionArrays' 10 | import { render } from '../rendering' 11 | import { getError } from '../errorHandler' 12 | 13 | import { useEquationOptions } from './useEquationOptions' 14 | import { RenderOptions } from '../types/RenderOptions' 15 | 16 | type Props = FormatOptions & RenderOptions & { 17 | /** Equation as text */ 18 | value: string, 19 | /** Optionally provide a unit to convert the result into */ 20 | unit?: string, 21 | /** Variables expressed as strings, evaluated as equations in their own right */ 22 | variablesEvaluated?: Record, 23 | 24 | style?: React.CSSProperties, 25 | className?: string, 26 | } 27 | 28 | type RefValue = { 29 | /** Equation and result valid */ 30 | valid: boolean, 31 | /** Parsed equation */ 32 | equation: EquationNode | EquationParserError, 33 | /** Parsed equation for the display unit */ 34 | unitEquation: EquationNode | EquationParserError | null, 35 | /** Evaluated result of the equation */ 36 | result: ResultNode | ResultResolveError, 37 | /** Evaluated result of the unit passed */ 38 | unitResult: ResultNode | ResultResolveError | null, 39 | /** Equation combined with result expressed as unit */ 40 | resultEquation: EquationNode | EquationParserError | EquationResolveError | EquationRenderError, 41 | } 42 | 43 | export const EquationEvaluate = memo(forwardRef(({ 44 | value, 45 | errorHandler: localErrorHandler, 46 | className, 47 | style, 48 | unit, 49 | variables: localVariables, 50 | functions: localFunctions, 51 | simplifiableUnits: localSimplifiableUnits, 52 | decimals: localDecimals, 53 | variablesEvaluated = {}, 54 | }: Props, ref: Ref) => { 55 | const { 56 | errorHandler: errorHandlerGlobal, 57 | className: classNameGlobal, 58 | style: styleGlobal, 59 | 60 | variables: globalVariables, 61 | functions: globalFunctions, 62 | simplifiableUnits: globalSimplifiableUnits, 63 | decimals: globalDecimals, 64 | } = useEquationOptions() 65 | 66 | const errorHandler = { ...errorHandlerGlobal, ...localErrorHandler } 67 | 68 | const functions = { ...globalFunctions, ...localFunctions} 69 | let variableError: [string, ResultResolveError] | undefined 70 | const variables = Object.entries(variablesEvaluated).reduce((current, [name, equation]) => { 71 | const result = resolve(parse(equation), { functions, variables: current }) 72 | 73 | if (result.type !== 'resolve-error') { 74 | current[name] = result 75 | } else if (!variableError) { 76 | variableError = [name, result] 77 | } 78 | 79 | return current 80 | }, { ...globalVariables, ...localVariables }) 81 | 82 | const formatOptions: FormatOptions = { 83 | variables, 84 | functions, 85 | simplifiableUnits: unionArrays(localSimplifiableUnits, globalSimplifiableUnits), 86 | decimals: localDecimals || globalDecimals, 87 | } 88 | 89 | const equation = parse(value) 90 | const unitEquation = unit ? parse(unit) : null 91 | const unitResult = unitEquation ? resolve(unitEquation, formatOptions) : null 92 | const result = variableError ? variableError[1] : resolve(equation, formatOptions) 93 | const resultEquation = variableError && equation.type !== 'parser-error' 94 | ? { 95 | type: 'render-error', 96 | errorType: 'variableResolution', 97 | node: wrapError(equation, unitEquation?.type === 'parser-error' ? null : unitEquation), 98 | name: variableError[0], 99 | errorMessage: getError(variableError[1], errorHandler), 100 | } as EquationRenderError 101 | : formatPreresolved(equation, unitEquation, result, unitResult, formatOptions) 102 | 103 | useImperativeHandle(ref, () => ({ 104 | valid: resultEquation.type !== 'resolve-error' && resultEquation.type !== 'parser-error' && resultEquation.type !== 'render-error', 105 | equation, 106 | unitEquation, 107 | result, 108 | unitResult, 109 | resultEquation, 110 | })) 111 | 112 | return render( 113 | resultEquation, 114 | { 115 | errorHandler, 116 | className: joinClasses(classNameGlobal, className), 117 | style: { ...styleGlobal, ...style }, 118 | }, 119 | ) 120 | })) 121 | -------------------------------------------------------------------------------- /src/components/EquationEvaluatePreparsed.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { parse } from 'equation-parser' 3 | 4 | import { RefLogger } from './StoryRefLogger' 5 | import { EquationEvaluatePreparsed } from './EquationEvaluatePreparsed' 6 | 7 | export default { 8 | title: 'components/EquationEvaluatePreparsed', 9 | component: EquationEvaluatePreparsed, 10 | } 11 | 12 | export const RefValid = () => ( 13 | } 15 | /> 16 | ) 17 | 18 | export const RefInvalidEquation = () => ( 19 | } 21 | /> 22 | ) 23 | 24 | export const RefInvalidResult = () => ( 25 | } 27 | /> 28 | ) 29 | -------------------------------------------------------------------------------- /src/components/EquationEvaluatePreparsed.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, memo, Ref, useImperativeHandle } from 'react' 2 | import { EquationNode, EquationParserError } from 'equation-parser' 3 | import { EquationResolveError, FormatOptions, formatPreresolved, resolve, ResultNode, ResultResolveError } from 'equation-resolver' 4 | 5 | import { RenderOptions } from '../types/RenderOptions' 6 | 7 | import { joinClasses } from '../utils/joinClasses' 8 | import { render } from '../rendering' 9 | 10 | import { useEquationOptions } from './useEquationOptions' 11 | 12 | type Props = FormatOptions & RenderOptions & { 13 | value: EquationNode | EquationParserError, 14 | unit?: EquationNode | EquationParserError, 15 | 16 | style?: React.CSSProperties, 17 | className?: string, 18 | } 19 | 20 | const unionArrays = (a: T[] | undefined, b: T[] | undefined): T[] | undefined => { 21 | if (!a) { 22 | return b 23 | } else if (!b) { 24 | return a 25 | } else { 26 | return [...a, ...b] 27 | } 28 | } 29 | 30 | type RefValue = { 31 | /** Equation can be evaluated */ 32 | valid: boolean, 33 | /** Evaluated result of the equation */ 34 | result: ResultNode | ResultResolveError, 35 | /** Evaluated result of the unit passed */ 36 | unitResult: ResultNode | ResultResolveError | null, 37 | /** Equation combined with result expressed as unit */ 38 | resultEquation: EquationNode | EquationParserError | EquationResolveError, 39 | } 40 | 41 | export const EquationEvaluatePreparsed = memo(forwardRef(({ 42 | value, 43 | errorHandler, 44 | className, 45 | style, 46 | unit, 47 | variables: localVariables, 48 | functions: localFunctions, 49 | simplifiableUnits: localSimplifiableUnits, 50 | decimals: localDecimals, 51 | }: Props, ref: Ref) => { 52 | const { 53 | errorHandler: errorHandlerGlobal, 54 | className: classNameGlobal, 55 | style: styleGlobal, 56 | 57 | variables: globalVariables, 58 | functions: globalFunctions, 59 | simplifiableUnits: globalSimplifiableUnits, 60 | decimals: globalDecimals, 61 | } = useEquationOptions() 62 | 63 | const formatOptions: FormatOptions = { 64 | variables: { ...globalVariables, ...localVariables }, 65 | functions: { ...globalFunctions, ...localFunctions}, 66 | simplifiableUnits: unionArrays(localSimplifiableUnits, globalSimplifiableUnits), 67 | decimals: localDecimals || globalDecimals, 68 | } 69 | 70 | const result = resolve(value, formatOptions) 71 | const unitResult = unit ? resolve(unit, formatOptions) : null 72 | const resultEquation = formatPreresolved(value, unit, result, unitResult, formatOptions) 73 | 74 | useImperativeHandle(ref, () => ({ 75 | valid: resultEquation.type !== 'resolve-error' && resultEquation.type !== 'parser-error', 76 | result, 77 | unitResult, 78 | resultEquation, 79 | })) 80 | 81 | return render( 82 | resultEquation, 83 | { 84 | errorHandler: { ...errorHandlerGlobal, ...errorHandler }, 85 | className: joinClasses(classNameGlobal, className), 86 | style: { ...styleGlobal, ...style }, 87 | }, 88 | ) 89 | })) 90 | -------------------------------------------------------------------------------- /src/components/EquationOptions.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { FormatOptions } from 'equation-resolver' 3 | 4 | import { RenderOptions } from '../types/RenderOptions' 5 | 6 | import { context } from './context' 7 | 8 | type Props = FormatOptions & RenderOptions & { 9 | children?: ReactNode, 10 | } 11 | 12 | export const EquationOptions = ({ children, ...options }: Props) => ( 13 | {children} 14 | ) 15 | -------------------------------------------------------------------------------- /src/components/EquationPreparsed.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { parse } from 'equation-parser' 3 | 4 | import { RefLogger } from './StoryRefLogger' 5 | import { EquationPreparsed } from './EquationPreparsed' 6 | 7 | export default { 8 | title: 'components/EquationPreparsed', 9 | component: EquationPreparsed, 10 | } 11 | 12 | export const RefValid = () => ( 13 | } 15 | /> 16 | ) 17 | 18 | export const RefInvalidEquation = () => ( 19 | } 21 | /> 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/EquationPreparsed.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, memo, Ref, useImperativeHandle } from 'react' 2 | 3 | import { EquationNode, EquationParserError } from 'equation-parser' 4 | import { EquationResolveError } from 'equation-resolver' 5 | 6 | import { RenderOptions } from '../types/RenderOptions' 7 | import { EquationRenderError } from '../types/EquationRenderError' 8 | 9 | import { joinClasses } from '../utils/joinClasses' 10 | import { render } from '../rendering' 11 | 12 | import { useEquationOptions } from './useEquationOptions' 13 | 14 | type Props = RenderOptions & { 15 | value: EquationNode | EquationParserError | EquationResolveError | EquationRenderError, 16 | } 17 | 18 | type RefValue = { 19 | } 20 | 21 | export const EquationPreparsed = memo(forwardRef(({ value, errorHandler, className, style }: Props, ref: Ref) => { 22 | const { 23 | errorHandler: errorHandlerGlobal, 24 | className: classNameGlobal, 25 | style: styleGlobal, 26 | } = useEquationOptions() 27 | 28 | useImperativeHandle(ref, () => ({})) 29 | 30 | return render( 31 | value, 32 | { 33 | errorHandler: { ...errorHandlerGlobal, ...errorHandler }, 34 | className: joinClasses(classNameGlobal, className), 35 | style: { ...styleGlobal, ...style }, 36 | }, 37 | ) 38 | })) 39 | -------------------------------------------------------------------------------- /src/components/StoryRefLogger.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { action } from '@storybook/addon-actions' 3 | 4 | type Props = { 5 | render: (ref: React.MutableRefObject) => JSX.Element, 6 | } 7 | 8 | export const RefLogger = ({ render }: Props) => { 9 | const ref = React.useRef() 10 | 11 | return ( 12 |
13 |
{render(ref)}
14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | import { FormatOptions } from 'equation-resolver' 3 | 4 | import { RenderOptions } from '../types/RenderOptions' 5 | 6 | export const context = createContext({}) 7 | -------------------------------------------------------------------------------- /src/components/useEquationOptions.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import { context } from './context' 4 | 5 | export const useEquationOptions = () => useContext(context) 6 | -------------------------------------------------------------------------------- /src/errorHandler.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import React from 'react' 3 | 4 | import { CombinedError, ErrorHandler } from './types/ErrorHandler' 5 | 6 | export const getError = (node: CombinedError, handlers: ErrorHandler): ReactNode => { 7 | const handler = handlers[node.errorType] 8 | 9 | if (!handler) return `Error: ${node.errorType}` 10 | 11 | return handler(node as any) 12 | } 13 | 14 | export const defaultErrorHandler: ErrorHandler = { 15 | // Parser errors 16 | /** `2 3` */ 17 | numberWhitespace: () => 'Cannot have spaces inside numbers', 18 | /** `1.2.3` */ 19 | invalidNumber: () => 'Invalid number', 20 | /** `2+*3` */ 21 | adjecentOperator: () => 'Two operators cannot be adjecent', 22 | /** `2 & 3` */ 23 | invalidChar: ({ character }) => `Invalid character '${character}'`, 24 | /** `* 3` */ 25 | invalidUnary: ({ symbol }) => `'${symbol}' cannot be a unary operator`, 26 | /** Theoretical case, no known reproduction */ 27 | multipleExpressions: () => 'An unexpected parsing error occured', 28 | /** `[[1,2][1,2,3]]` */ 29 | matrixMixedDimension: ({ lengthExpected, lengthReceived }) => `Matrix-row has length ${lengthReceived}, but should be ${lengthExpected}`, 30 | /** `[[]]` */ 31 | matrixEmpty: () => 'Matrix must contain at least one expression', 32 | /** `[]` */ 33 | vectorEmpty: () => 'Vector must contain at least one expression', 34 | /** Closing an un-opened parenthesis, `2+3)` */ 35 | expectedEnd: () => 'Expected end of equation', 36 | /** `[2,3` */ 37 | expectedSquareBracket: () => 'Missing closing square bracket', 38 | /** `5 * (2 + 3` */ 39 | expectedCloseParens: () => 'Missing closing parenthesis', 40 | /** `2 + 3 +` */ 41 | operatorLast: () => 'Equation cannot end on an operator', 42 | /** `()` */ 43 | emptyBlock: () => 'Parentheses must have content', 44 | 45 | // Resolver errors 46 | functionUnknown: ({ name }) => `Unknown function ${name}`, 47 | functionArgLength: ({ name, minArgs, maxArgs }) => minArgs === maxArgs 48 | ? `${name} must have ${minArgs} arguments` 49 | : `${name} must have ${minArgs}-${maxArgs} arguments`, 50 | functionNumberOnly: ({ name }) => `Arguments of ${name} must be unitless numbers`, 51 | 52 | functionSqrt1Positive: ({ name }) => `First argument of ${name} must be positive`, 53 | functionRoot1PositiveInteger: ({ name }) => `First argument of ${name} must be a positive integer`, 54 | functionRoot2Positive: ({ name }) => `Second argument of ${name} must be positive`, 55 | functionSum1Variable: ({ name, variableType }) => `First argument of ${name} must be a variable, was ${variableType}`, 56 | functionSum2Integer: ({ name }) => `Second argument of ${name} must be an integer`, 57 | functionSum3Integer: ({ name }) => `Third argument of ${name} must be an integer`, 58 | 59 | variableUnknown: ({ name }) => `Unknown variable ${name}`, 60 | 61 | plusDifferentUnits: () => `Cannot add numbers with different units`, 62 | plusMatrixMismatch: ({ aDimensions, bDimensions }) => `Cannot add matrices of dimensions ${aDimensions} and ${bDimensions}`, 63 | plusminusUnhandled: () => `Plus-minus operator is currently not supported`, 64 | scalarProductUnbalanced: ({ aLength, bLength }) => `Cannot calculate scalar (dot) product of vectors of size ${aLength} and ${bLength}`, 65 | vectorProduct3VectorOnly: () => `Vector (cross) product requires 2 3-vectors`, 66 | matrixProductMatrixMismatch: ({ aDimensions, bDimensions }) => `Cannot multiply matrices of dimensions ${aDimensions} and ${bDimensions}`, 67 | multiplyImplicitNoVectors: () => `Cannot multiply vectors without symbol, use either dot or cross`, 68 | divideNotZero: () => `Cannot divide by zero`, 69 | divideMatrixMatrix: () => `Cannot divide matrices with each other`, 70 | powerUnitlessNumberExponent: () => `Exponent must be a unitless number`, 71 | 72 | operatorInvalidArguments: ({ operator, a, b }) => `Operator '${operator}' not defined for ${a} and ${b}`, 73 | 74 | noComparison: () => `Cannot evaluate a comparison`, 75 | 76 | matrixDifferentUnits: () => `All matrix-cells must have the same unit`, 77 | matrixNoNesting: () => `Cannot nest matrices`, 78 | 79 | invalidEquation: () => `Cannot resolve an invalid equation`, 80 | 81 | placeholder: () => `Cannot evaluate a placeholder`, 82 | 83 | invalidUnit: () => `Must be a valid unit`, 84 | 85 | // Render errors 86 | variableResolution: ({ name, errorMessage }) => <>Failed to evaluate {name}: {errorMessage}, 87 | variableNaming: ({ name }) => `Invalid variable name '${name}'`, 88 | functionSignature: ({ signature }) => `Invalid function signature '${signature}'`, 89 | } 90 | -------------------------------------------------------------------------------- /src/generic.stories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { defaultVariables, defaultFunctions } from 'equation-resolver' 4 | 5 | import { 6 | EquationContext, 7 | EquationEvaluate, 8 | EquationOptions, 9 | defaultErrorHandler, 10 | } from '.' 11 | 12 | function getPersistantState(): string { 13 | return window.localStorage.persistantEquationState || '' 14 | } 15 | 16 | function setPersistantState(state: string) { 17 | window.localStorage.persistantEquationState = state 18 | } 19 | 20 | class EditorComponent extends React.Component<{}, {value: string, largeSize: boolean}> { 21 | constructor(props: {}) { 22 | super(props) 23 | this.state = { 24 | value: getPersistantState(), 25 | largeSize: false, 26 | } 27 | } 28 | 29 | handleSizeChange = (e: React.FormEvent) => this.setState({largeSize: e.currentTarget.checked}) 30 | 31 | handleChange = (e: React.FormEvent) => { 32 | const value = e.currentTarget.value 33 | this.setState({ value }) 34 | setPersistantState(value) 35 | } 36 | 37 | render() { 38 | const { largeSize } = this.state 39 | const equations = this.state.value.split(/\n/g).map((s) => s.trim()).filter((s) => s) 40 | return ( 41 | 47 |
48 |
49 | 57 |
58 |
59 |