├── .gitignore ├── src ├── types │ ├── Rendering.ts │ ├── RenderingPart.ts │ ├── RenderOptions.ts │ ├── EquationRenderError.ts │ └── ErrorHandler.tsx ├── components │ ├── useEquationOptions.tsx │ ├── context.ts │ ├── EquationOptions.tsx │ ├── StoryRefLogger.tsx │ ├── EquationContext │ │ ├── isComparison.ts │ │ ├── isEqualPlaceholder.ts │ │ ├── stories.tsx │ │ └── index.tsx │ ├── Equation.stories.tsx │ ├── EquationPreparsed.stories.tsx │ ├── EquationEvaluatePreparsed.stories.tsx │ ├── EquationPreparsed.tsx │ ├── Equation.tsx │ ├── EquationEvaluate.stories.tsx │ ├── EquationEvaluatePreparsed.tsx │ └── EquationEvaluate.tsx ├── utils │ ├── joinClasses.ts │ ├── unionArrays.tsx │ └── throwUnknownType.ts ├── rendering │ ├── special │ │ ├── abs │ │ │ ├── stories.tsx │ │ │ └── index.tsx │ │ ├── sqrt │ │ │ ├── stories.tsx │ │ │ ├── index.tsx │ │ │ └── root-symbol.tsx │ │ ├── sum │ │ │ ├── stories.tsx │ │ │ └── index.tsx │ │ └── root │ │ │ ├── stories.tsx │ │ │ └── index.tsx │ ├── variable │ │ ├── stories.tsx │ │ └── index.tsx │ ├── block │ │ ├── stories.tsx │ │ └── index.tsx │ ├── power │ │ ├── stories.tsx │ │ └── index.tsx │ ├── func │ │ ├── stories.tsx │ │ └── index.tsx │ ├── Wrapper.tsx │ ├── comparison.stories.tsx │ ├── operator.stories.tsx │ ├── matrix │ │ ├── stories.tsx │ │ └── index.tsx │ ├── StoryEquationWrapper.tsx │ ├── fraction │ │ ├── stories.tsx │ │ └── index.tsx │ ├── parens │ │ └── index.tsx │ └── index.tsx ├── index.tsx ├── generic.stories.tsx └── errorHandler.tsx ├── storybook ├── main.js ├── preview.js └── styles.css ├── .editorconfig ├── tsconfig.json ├── rollup.config.js ├── package.json ├── .eslintrc.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | docs 4 | dist 5 | deploy-key 6 | -------------------------------------------------------------------------------- /src/types/Rendering.ts: -------------------------------------------------------------------------------- 1 | export type Rendering = { 2 | elements: JSX.Element[], 3 | height: number, 4 | aboveMiddle: number, 5 | belowMiddle: number, 6 | } 7 | -------------------------------------------------------------------------------- /src/types/RenderingPart.ts: -------------------------------------------------------------------------------- 1 | export type RenderingPart = { 2 | type: any, 3 | props: any, 4 | children?: any, 5 | aboveMiddle: number, 6 | belowMiddle: number, 7 | } 8 | -------------------------------------------------------------------------------- /src/components/useEquationOptions.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | 3 | import { context } from './context' 4 | 5 | export const useEquationOptions = () => useContext(context) 6 | -------------------------------------------------------------------------------- /src/utils/joinClasses.ts: -------------------------------------------------------------------------------- 1 | export const joinClasses = (a: string | undefined, b: string | undefined) => { 2 | if (a && b) return a + ' ' + b 3 | 4 | return a || b || undefined 5 | } 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: [ 3 | '../src/**/stories.tsx', 4 | '../src/**/*.stories.tsx', 5 | ], 6 | addons: [ 7 | '@storybook/addon-actions', 8 | ], 9 | framework: '@storybook/react', 10 | } 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/types/RenderOptions.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | import { ErrorHandler } from './ErrorHandler' 4 | 5 | export type RenderOptions = { 6 | errorHandler?: ErrorHandler, 7 | className?: string, 8 | style?: CSSProperties, 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/unionArrays.tsx: -------------------------------------------------------------------------------- 1 | export const unionArrays = (a: T[] | undefined, b: T[] | undefined): T[] | undefined => { 2 | if (!a) { 3 | return b 4 | } else if (!b) { 5 | return a 6 | } else { 7 | return [...a, ...b] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/types/EquationRenderError.ts: -------------------------------------------------------------------------------- 1 | import { EquationNode } from 'equation-parser' 2 | import { ReactNode } from 'react' 3 | 4 | /** Errors that occur specifically during rendering */ 5 | export type EquationRenderError = { type: 'render-error', node: EquationNode } & ( 6 | | { errorType: 'variableResolution', name: string, errorMessage: ReactNode } 7 | | { errorType: 'variableNaming', name: String } 8 | | { errorType: 'functionSignature', signature: String } 9 | ) 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictNullChecks": true, 5 | "noUnusedLocals": false, 6 | "stripInternal": true, 7 | "esModuleInterop": true, 8 | "jsx": "react", 9 | "lib": [ 10 | "dom", 11 | ], 12 | "types": ["jest", "node", "react"], 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.tsx"], 15 | "files": [ 16 | "src/index.tsx", 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /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/types/ErrorHandler.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { EquationParserError } from 'equation-parser' 3 | import { ResultResolveError } from 'equation-resolver' 4 | import { EquationRenderError } from './EquationRenderError' 5 | 6 | export type CombinedError = EquationParserError | ResultResolveError | EquationRenderError 7 | 8 | export type ErrorHandler = { 9 | [Key in CombinedError['errorType']]?: (node: Extract) => ReactNode 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/throwUnknownType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Compiler-error and runtime-error on unhandled type 3 | * 4 | * @param typed: Object with type-property 5 | * @param getMessage: get an error message for runtime errors 6 | */ 7 | export function throwUnknownType(typed: never, getMessage: (type: string) => string): never; 8 | export function throwUnknownType(typed: { type: string }, getMessage: (type: string) => string) { 9 | throw new Error(getMessage((typed && typed.type) || 'unknown')) 10 | } 11 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /storybook/preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { defaultFunctions, defaultVariables } from 'equation-resolver' 3 | 4 | import { EquationOptions, defaultErrorHandler } from '../src' 5 | 6 | import './styles.css' 7 | 8 | export const decorators = [ 9 | (story) => ( 10 | 15 | {story()} 16 | 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /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/rendering/special/abs/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/special/abs', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const TallExpression = () => ( 15 | 16 | ) 17 | 18 | export const ComplexCombinations = () => ( 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { render } from './rendering' 2 | 3 | export { EquationOptions } from './components/EquationOptions' 4 | export { useEquationOptions } from './components/useEquationOptions' 5 | export { Equation } from './components/Equation' 6 | export { EquationEvaluate } from './components/EquationEvaluate' 7 | export { EquationPreparsed } from './components/EquationPreparsed' 8 | export { EquationEvaluatePreparsed } from './components/EquationEvaluatePreparsed' 9 | export { EquationContext } from './components/EquationContext' 10 | 11 | export { defaultErrorHandler } from './errorHandler' 12 | -------------------------------------------------------------------------------- /src/rendering/variable/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/variable', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const Numbers = () => ( 15 | 16 | ) 17 | 18 | export const Index = () => ( 19 | 20 | ) 21 | 22 | export const MultipleIndices = () => ( 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /src/rendering/special/sqrt/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/special/sqrt', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const Long = () => ( 15 | 16 | ) 17 | 18 | export const TallAbove = () => ( 19 | 20 | ) 21 | 22 | export const TallBelow = () => ( 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /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/rendering/special/sum/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/special/sum', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const LongArguments = () => ( 15 | 16 | ) 17 | 18 | export const TallAbove = () => ( 19 | 20 | ) 21 | 22 | export const TallBelow = () => ( 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /src/rendering/block/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/block', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const ImpliedMultAlignment = () => ( 15 | 16 | ) 17 | 18 | export const TallAbove = () => ( 19 | 20 | ) 21 | 22 | export const TallBelow = () => ( 23 | 24 | ) 25 | 26 | export const Nested = () => ( 27 | 28 | ) 29 | -------------------------------------------------------------------------------- /src/rendering/power/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/power', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const ComplexBase = () => ( 15 | 16 | ) 17 | 18 | export const ComplexExponent = () => ( 19 | 20 | ) 21 | 22 | export const LongerExponent = () => ( 23 | 24 | ) 25 | 26 | export const TallExponentAlignment = () => ( 27 | 28 | ) 29 | -------------------------------------------------------------------------------- /src/rendering/func/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/func', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const TallArgument = () => ( 15 | 16 | ) 17 | 18 | export const Nested = () => ( 19 | 20 | ) 21 | 22 | export const MultipleArguments = () => ( 23 | 24 | ) 25 | 26 | export const MultipleTallArguments = () => ( 27 | 28 | ) 29 | -------------------------------------------------------------------------------- /src/rendering/block/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNode, EquationNodeBlock } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../types/RenderingPart' 5 | 6 | import { renderInternal } from '..' 7 | 8 | import Parens from '../parens' 9 | 10 | export default function block(node: EquationNodeBlock, errorNode: EquationNode | null): RenderingPart { 11 | const content = renderInternal(node.child, errorNode) 12 | 13 | return { 14 | type: 'span', 15 | props: { style: { height: `${content.height}em` } }, 16 | aboveMiddle: content.aboveMiddle, 17 | belowMiddle: content.belowMiddle, 18 | children: <> 19 | 20 | {content.elements} 21 | 22 | , 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /storybook/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .editor { 6 | border: 1px solid #aaa; 7 | padding: 10px; 8 | } 9 | 10 | .equation-wrapper { 11 | position: relative; 12 | margin: 5px 10px 10px; 13 | border: 3px solid #cceeff; 14 | display: inline-block; 15 | } 16 | 17 | .equation-wrapper-raw { 18 | font-family: monospace; 19 | white-space: pre-wrap; 20 | display: inline-block; 21 | margin: 10px 10px 5px; 22 | border: 1px solid #aaa; 23 | border-radius: 2px; 24 | padding: 4px 6px; 25 | background: #eee; 26 | } 27 | 28 | .equation-wrapper-raw:empty:before { 29 | content: ' '; 30 | } 31 | 32 | textarea.equation-wrapper-raw { 33 | min-width: 300px; 34 | min-height: 160px; 35 | resize: none; 36 | background: white; 37 | } 38 | 39 | .size-control { 40 | margin-left: 10px; 41 | } 42 | -------------------------------------------------------------------------------- /src/rendering/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, ReactNode } from 'react' 2 | 3 | type Props = { 4 | children?: ReactNode, 5 | height?: number, 6 | aboveMiddle?: number, 7 | className?: string, 8 | style?: CSSProperties, 9 | } 10 | 11 | export const Wrapper = ({ children, height, aboveMiddle, className, style } : Props) => ( 12 | 13 | 24 | {children} 25 | 26 | ) 27 | -------------------------------------------------------------------------------- /src/rendering/special/root/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/special/root', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const Long = () => ( 15 | 16 | ) 17 | 18 | export const TallAbove = () => ( 19 | 20 | ) 21 | 22 | export const TallBelow = () => ( 23 | 24 | ) 25 | 26 | export const TallIndex = () => ( 27 | 28 | ) 29 | 30 | export const ComplexCombination = () => ( 31 | 32 | ) 33 | -------------------------------------------------------------------------------- /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/rendering/comparison.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from './StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/comparisons', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Equals = () => ( 11 | 12 | ) 13 | 14 | export const LessThan = () => ( 15 | 16 | ) 17 | 18 | export const GreaterThan = () => ( 19 | 20 | ) 21 | 22 | export const LessThanOrEqual = () => ( 23 | 24 | ) 25 | 26 | export const GreaterThanOrEqual = () => ( 27 | 28 | ) 29 | 30 | export const AlmostEqual = () => ( 31 | 32 | ) 33 | -------------------------------------------------------------------------------- /src/rendering/operator.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from './StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/operator', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Addition = () => ( 11 | 12 | ) 13 | 14 | export const Subtraction = () => ( 15 | 16 | ) 17 | 18 | export const Multiplication = () => ( 19 | 20 | ) 21 | export const MultiplicationImpliedSpace = () => ( 22 | 23 | ) 24 | 25 | export const Division = () => ( 26 | 27 | ) 28 | 29 | export const PlusMinus = () => ( 30 | 31 | ) 32 | 33 | export const LeadingMinus = () => ( 34 | 35 | ) 36 | 37 | export const LeadingPlusMinus = () => ( 38 | 39 | ) 40 | -------------------------------------------------------------------------------- /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/rendering/matrix/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/matrix', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Vector = () => ( 11 | 12 | ) 13 | 14 | export const VectorTallCells = () => ( 15 | 16 | ) 17 | 18 | export const HVector = () => ( 19 | 20 | ) 21 | 22 | export const HVectorTallCells = () => ( 23 | 24 | ) 25 | 26 | export const Matrix = () => ( 27 | 28 | ) 29 | 30 | export const MatrixTallCells = () => ( 31 | 32 | ) 33 | 34 | export const MatrixAlignment = () => ( 35 | 36 | ) 37 | -------------------------------------------------------------------------------- /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/rendering/special/abs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNode, EquationNodeFunction } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../../types/RenderingPart' 5 | 6 | import { renderInternal } from '../..' 7 | 8 | const styles = { 9 | wrapper: { 10 | display: 'inline-block', 11 | verticalAlign: 'top', 12 | padding: '0 0.3em', 13 | }, 14 | line: { 15 | position: 'absolute', 16 | borderLeft: '0.08em solid currentColor', 17 | top: '0.2em', 18 | height: `calc(100% - 0.4em)`, 19 | }, 20 | } as const 21 | 22 | export default function abs({args: [expression]}: EquationNodeFunction, errorNode: EquationNode | null): RenderingPart { 23 | const content = renderInternal(expression || { type: 'operand-placeholder' }, errorNode) 24 | 25 | return { 26 | type: 'span', 27 | props: { style: { ...styles.wrapper, height: `${content.height}em` } }, 28 | aboveMiddle: content.aboveMiddle, 29 | belowMiddle: content.belowMiddle, 30 | children: <> 31 | 32 | {content.elements} 33 | 34 | , 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/rendering/power/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNode, EquationNodePower } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../types/RenderingPart' 5 | 6 | import { renderInternal } from '..' 7 | 8 | const fontFactor = 0.7 9 | const exponentOffset = 0.8 10 | 11 | const styles = { 12 | exponent: { 13 | fontSize: `${fontFactor * 100}%`, 14 | display: 'inline-block', 15 | verticalAlign: 'top', 16 | }, 17 | } 18 | 19 | export default function power({ a, b }: EquationNodePower, errorNode: EquationNode | null): RenderingPart { 20 | const base = renderInternal(a, errorNode, false) 21 | const exponent = renderInternal(b, errorNode, true) 22 | const baseOffset = exponent.height * fontFactor - exponentOffset 23 | return { 24 | type: 'span', 25 | props: { style: { height: `${base.height + baseOffset}em` } }, 26 | aboveMiddle: base.height / 2 + baseOffset, 27 | belowMiddle: base.height / 2, 28 | children: <> 29 | {base.elements} 30 | {exponent.elements} 31 | , 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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/rendering/variable/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNodeVariable } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../types/RenderingPart' 5 | 6 | const indexOffset = 0.3 7 | const indexFactor = 0.8 8 | 9 | const styles = { 10 | main: { 11 | display: 'inline-block', 12 | fontStyle: 'italic', 13 | }, 14 | index: { 15 | fontSize: `${indexFactor * 100}%`, 16 | position: 'relative', 17 | }, 18 | } as const 19 | 20 | export default function variable({ name }: EquationNodeVariable): RenderingPart { 21 | 22 | const [main, ...indices] = name.split('_') 23 | 24 | return { 25 | type: 'span', 26 | props: { 27 | style: { 28 | ...styles.main, 29 | marginLeft: /^[%‰°'"]/.test(main) ? '-0.2em' : null, 30 | }, 31 | }, 32 | aboveMiddle: 0.7, 33 | belowMiddle: 0.7 + indices.length * indexOffset, 34 | children: <> 35 | {main} 36 | {indices.map((indexName, idx) => ( 37 | {indexName} 44 | ))} 45 | , 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/rendering/StoryEquationWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { defaultVariables, defaultFunctions } from 'equation-resolver' 3 | 4 | import { EquationEvaluate } from '../components/EquationEvaluate' 5 | import { Equation } from '../components/Equation' 6 | 7 | type Props = { 8 | value: string, 9 | disableEvaluation?: boolean, 10 | } 11 | 12 | export const EquationWrapper = ({ value, disableEvaluation }: Props) => { 13 | const [isLargeSize, setLargeSize] = useState(false) 14 | 15 | return ( 16 |
17 |
18 | 26 |
27 |
{value}
28 |
29 | {disableEvaluation 30 | ? 31 | : 32 | } 33 | 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /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/rendering/special/sqrt/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNode, EquationNodeFunction } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../../types/RenderingPart' 5 | 6 | import { renderInternal } from '../..' 7 | 8 | import RootSymbol from './root-symbol' 9 | 10 | const padding = 0.1 11 | 12 | const styles = { 13 | wrapper: { 14 | position: 'relative', 15 | display: 'inline-block', 16 | marginTop: '0.1em', 17 | }, 18 | 19 | symbol: { 20 | verticalAlign: 'top', 21 | fill: 'currentcolor', 22 | }, 23 | 24 | line: { 25 | position: 'absolute', 26 | width: 'calc(100% - 0.7em)', 27 | borderTop: '0.08em solid currentColor', 28 | top: 0, 29 | left: '0.8em', 30 | }, 31 | } as const 32 | 33 | export default function sqrt({args: [expression]}: EquationNodeFunction, errorNode: EquationNode | null): RenderingPart { 34 | const content = renderInternal(expression || { type: 'operand-placeholder' }, errorNode) 35 | 36 | return { 37 | type: 'span', 38 | props: { style: { ...styles.wrapper, height: `${content.height + padding}em` } }, 39 | aboveMiddle: content.aboveMiddle + padding, 40 | belowMiddle: content.belowMiddle, 41 | children: <> 42 | 43 | 44 | {content.elements} 45 | , 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/rendering/fraction/stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { EquationWrapper } from '../StoryEquationWrapper' 4 | 5 | export default { 6 | title: 'rendering/fraction', 7 | component: EquationWrapper, 8 | } 9 | 10 | export const Simple = () => ( 11 | 12 | ) 13 | 14 | export const LargeNumerator = () => ( 15 | 16 | ) 17 | 18 | export const LargeDenominator = () => ( 19 | 20 | ) 21 | 22 | export const Negated = () => ( 23 | 24 | ) 25 | 26 | export const NegatedUnevenHeight = () => ( 27 | 28 | ) 29 | 30 | export const Nested = () => ( 31 | 32 | ) 33 | 34 | export const NestedComplex = () => ( 35 | 36 | ) 37 | 38 | export const TallAbove = () => ( 39 | 40 | ) 41 | 42 | export const TallBelow = () => ( 43 | 44 | ) 45 | 46 | export const TallAboveWMore = () => ( 47 | 48 | ) 49 | 50 | export const TallBelowWMore = () => ( 51 | 52 | ) 53 | 54 | export const UnevenHeights = () => ( 55 | 56 | ) 57 | -------------------------------------------------------------------------------- /src/rendering/fraction/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNode, EquationNodeDivideFraction } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../types/RenderingPart' 5 | 6 | import { renderInternal } from '..' 7 | 8 | const fontFactor = 0.9 9 | const separatorSize = 0.06 10 | 11 | const styles = { 12 | wrapper: { 13 | display: 'inline-block', 14 | verticalAlign: 'top', 15 | }, 16 | 17 | part: { 18 | fontSize: `${fontFactor * 100}%`, 19 | display: 'block', 20 | textAlign: 'center' as const, 21 | padding: '0 0.4em', 22 | }, 23 | 24 | separator: { 25 | display: 'block', 26 | background: 'currentColor', 27 | borderTop: `${separatorSize}em solid currentColor`, 28 | }, 29 | } 30 | 31 | export default function fraction({ a, b }: EquationNodeDivideFraction, errorNode: EquationNode | null): RenderingPart { 32 | const top = renderInternal(a, errorNode, true) 33 | const bottom = renderInternal(b, errorNode, true) 34 | return { 35 | type: 'span', 36 | props: { style: { ...styles.wrapper } }, 37 | aboveMiddle: top.height * fontFactor - separatorSize / 2, 38 | belowMiddle: bottom.height * fontFactor + separatorSize * 3 / 2, 39 | children: <> 40 | {top.elements} 41 | 42 | {bottom.elements} 43 | , 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/rendering/func/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNodeFunction, EquationNode } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../types/RenderingPart' 5 | 6 | import { toRendering, pushTree, simplePart, renderInternal } from '..' 7 | 8 | import Parens from '../parens' 9 | 10 | export default function func(node: EquationNodeFunction | { type: 'function-placeholder', args: EquationNode[], name?: undefined }, errorNode: EquationNode | null): RenderingPart { 11 | // Use manual rendering to allow commas to be pushed between args 12 | // without having to resort to manual alignment 13 | const argParts: RenderingPart[] = [] 14 | node.args.forEach((arg, i) => { 15 | if (i > 0) { 16 | argParts.push(simplePart(',', { paddingRight: '0.4em' })) 17 | } 18 | pushTree(arg, argParts, errorNode) 19 | }) 20 | 21 | // Render name as variable or placeholder 22 | const nameRendering = renderInternal(node.type === 'function' ? { type: 'variable', name: node.name } : { type: 'operand-placeholder' }, errorNode) 23 | const argRendering = toRendering(argParts) 24 | 25 | return { 26 | type: 'span', 27 | props: { style: { height: `${Math.max(nameRendering.height, argRendering.height)}em` } }, 28 | aboveMiddle: Math.max(nameRendering.aboveMiddle, argRendering.aboveMiddle), 29 | belowMiddle: Math.max(nameRendering.belowMiddle, argRendering.belowMiddle), 30 | children: <> 31 | {nameRendering.elements} 32 | 33 | {argRendering.elements} 34 | 35 | , 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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/rendering/parens/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Parens({ height, type = '()', flip = false }: { height: number, className?: string, type?: '()' | '[]' | '{}', flip?: boolean }) { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } 10 | 11 | const pathBuilders = { 12 | '()'(height: number) { 13 | const offsetHeight = height - 1.4 14 | return `M0.094 0.681q0 -0.312 0.18 -0.476l0.028 -0.024l0.013 0q0.012 0 0.015 0.003t0.003 0.006q0 0.004 -0.011 0.015q-0.155 0.164 -0.155 0.476l0 ${ offsetHeight }q0 0.321 0.155 0.476q0.011 0.011 0.011 0.015q0 0.009 -0.018 0.009l-0.013 0l-0.028 -0.024q-0.18 -0.164 -0.18 -0.476Z` 15 | }, 16 | '[]'(height: number) { 17 | const offsetHeight = height - 0.55 18 | return `M0.134 0.19h0.24v0.08h-0.16v${ offsetHeight }h0.16v0.08h-0.24Z` 19 | }, 20 | '{}'(height: number) { 21 | const offsetHeight = height - 1.37 22 | return `M0.3472 ${ 1.161 + offsetHeight }q0 0.014 -0.0048 0.02h-0.0144q-0.0608 0 -0.104 -0.026t-0.0544 -0.08q-0.0016 -0.006 -0.0024 -0.1445v${ -offsetHeight / 2 }q0 -0.021 0 -0.053q-0.0008 -0.089 -0.004 -0.1q-0.0008 -0.001 -0.0008 -0.002q-0.0096 -0.031 -0.0368 -0.053t-0.06 -0.023q-0.0088 0 -0.0112 -0.003t-0.0024 -0.016t0.0024 -0.016t0.0112 -0.003q0.0328 0 0.06 -0.022t0.0368 -0.054q0.0032 -0.012 0.0032 -0.025t0.0016 -0.131v${ -offsetHeight / 2 }q0.0008 -0.137 0.0024 -0.144q0.0064 -0.032 0.0256 -0.053q0.0208 -0.026 0.064 -0.042q0.0296 -0.008 0.0424 -0.009q0.0016 0 0.0104 0t0.0144 -0.001h0.016q0.0048 0.006 0.0048 0.018q0 0.013 -0.0024 0.016q-0.0016 0.003 -0.0128 0.003q-0.0448 0.003 -0.0744 0.032q-0.016 0.015 -0.0208 0.034q-0.004 0.013 -0.004 0.148v${ offsetHeight / 2 }q0 0.13 -0.0008 0.136q-0.004 0.039 -0.0272 0.067t-0.0576 0.041l-0.0112 0.005l0.0112 0.005q0.0336 0.013 0.0568 0.04t0.028 0.068q0.0008 0.006 0.0008 0.136v${ offsetHeight / 2 }q0 0.1355 0.004 0.1475q0.008 0.028 0.0352 0.046t0.06 0.02q0.0112 0 0.0128 0.004q0.0024 0.002 0.0024 0.014Z` 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /src/rendering/special/root/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNode, EquationNodeFunction } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../../types/RenderingPart' 5 | 6 | import { renderInternal, toRendering } from '../..' 7 | 8 | import sqrt from '../sqrt' 9 | 10 | const rootIndexFactor = 0.7 11 | const rootIndexOffset = 0.8 12 | 13 | const styles = { 14 | wrapper: { 15 | display: 'inline-block', 16 | position: 'relative', 17 | }, 18 | indexOuter: { 19 | display: 'inline-block', 20 | }, 21 | indexInner: { 22 | fontSize: `${rootIndexFactor * 100}%`, 23 | verticalAlign: 'top', 24 | }, 25 | } as const 26 | 27 | export default function root({args: [rootIndex, expression]}: EquationNodeFunction, errorNode: EquationNode | null): RenderingPart { 28 | const rootIndexContent = renderInternal(rootIndex || { type: 'operand-placeholder' }, errorNode) 29 | // Pretend this is a sqrt to avoid repeat of logic 30 | const sqrtContent = sqrt({ type: 'function', name: 'sqrt', args: [expression] }, errorNode) 31 | const bottom = sqrtContent.belowMiddle - rootIndexOffset 32 | const offset = 1 - Math.atan(sqrtContent.aboveMiddle + sqrtContent.belowMiddle) * 0.6 33 | const rendering = toRendering([ 34 | { 35 | type: 'span', 36 | props: { 37 | style: { 38 | ...styles.indexOuter, 39 | height: `${rootIndexContent.height * rootIndexFactor}em`, 40 | marginRight: `${-offset}em`, 41 | minWidth: `${offset}em`, 42 | }, 43 | }, 44 | aboveMiddle: rootIndexContent.height * rootIndexFactor - bottom, 45 | belowMiddle: bottom, 46 | children: ( 47 | {rootIndexContent.elements} 48 | ), 49 | }, 50 | sqrtContent, 51 | ]) 52 | 53 | return { 54 | type: 'span', 55 | props: { style: styles.wrapper }, 56 | aboveMiddle: rendering.aboveMiddle, 57 | belowMiddle: rendering.belowMiddle, 58 | children: rendering.elements, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/rendering/matrix/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNode, EquationNodeMatrix } from 'equation-parser' 3 | 4 | import { RenderingPart } from '../../types/RenderingPart' 5 | 6 | import { renderInternal } from '..' 7 | 8 | import Parens from '../parens' 9 | 10 | const padding = 0.2 11 | const cellPadding = 0.4 12 | const fontFactor = 0.9 13 | 14 | const styles = { 15 | wrapper: { 16 | padding: '0.1em 0', 17 | }, 18 | table: { 19 | display: 'inline-table', 20 | verticalAlign: 'top', 21 | borderCollapse: 'collapse', 22 | fontSize: `${fontFactor * 100}%`, 23 | marginTop: '0.1em', 24 | }, 25 | cell: { 26 | padding: '0.2em 0.5em', 27 | textAlign: 'center', 28 | verticalAlign: 'top', 29 | }, 30 | cellContent: { 31 | width: '100%', 32 | height: '100%', 33 | }, 34 | } as const 35 | 36 | export default function matrix({ values, m }: EquationNodeMatrix, errorNode: EquationNode | null): RenderingPart { 37 | const content = values.map((row) => row.map((value) => renderInternal(value, errorNode))) 38 | 39 | const cellHeight = sumOf(content, (row) => maxOf(row, ({height}) => height)) 40 | 41 | const height = fontFactor * (m * cellPadding + cellHeight) 42 | 43 | return { 44 | type: 'span', 45 | props: { style: styles.wrapper }, 46 | aboveMiddle: (height + padding) / 2, 47 | belowMiddle: (height + padding) / 2, 48 | children: <> 49 | 50 | 51 | 52 | {content.map((row, rowIdx) => { 53 | const rowHeight = maxOf(row, (cell) => cell.height) + cellPadding 54 | const aboveMiddle = maxOf(row, (cell) => cell.aboveMiddle) 55 | return ( 56 | 57 | {row.map((cell, cellIdx) => ( 58 | 61 | ))} 62 | 63 | ) 64 | })} 65 | 66 |
59 | {cell.elements} 60 |
67 | 68 | , 69 | } 70 | } 71 | 72 | function maxOf(array: T[], get: (value: T) => number): number { 73 | return array.reduce((current, value) => Math.max(current, get(value)), 0) 74 | } 75 | 76 | function sumOf(array: T[], get: (value: T) => number): number { 77 | return array.reduce((current, value) => current + get(value), 0) 78 | } 79 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/rendering/special/sum/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { EquationNode, EquationNodeFunction } from 'equation-parser' 3 | 4 | import { renderInternal } from '../..' 5 | 6 | const iconSize = 1.8 7 | const fontFactor = 0.8 8 | 9 | const styles = { 10 | wrapper: { 11 | display: 'inline-block', 12 | }, 13 | 14 | block: { 15 | display: 'inline-block', 16 | verticalAlign: 'top', 17 | textAlign: 'center' as const, 18 | }, 19 | 20 | icon: { 21 | display: 'block', 22 | lineHeight: 0.8, 23 | fontSize: '2.25em', 24 | padding: '0 0.1em', 25 | top: '1px', 26 | }, 27 | 28 | small: { 29 | display: 'block', 30 | fontSize: `${fontFactor * 100}%`, 31 | }, 32 | } 33 | 34 | export default function sum({args: [variable, start, end, expression]}: EquationNodeFunction, errorNode: EquationNode | null) { 35 | const top = renderInternal(end || { type: 'operand-placeholder' }, errorNode) 36 | const bottom = renderInternal({ 37 | type: 'equals', 38 | a: variable || { type: 'operand-placeholder' }, 39 | b: start || { type: 'operand-placeholder' }, 40 | }, errorNode) 41 | const rendering = renderInternal(wrapParenthesis(expression || { type: 'operand-placeholder' }), errorNode, false, { 42 | type: 'span', 43 | props: { style: styles.block }, 44 | aboveMiddle: iconSize / 2 + top.height * fontFactor, 45 | belowMiddle: iconSize / 2 + bottom.height * fontFactor, 46 | children: <> 47 | {top.elements} 48 | Σ 49 | {bottom.elements} 50 | , 51 | }) 52 | return { 53 | type: 'span', 54 | props: { style: styles.wrapper }, 55 | aboveMiddle: rendering.aboveMiddle, 56 | belowMiddle: rendering.belowMiddle, 57 | children: rendering.elements, 58 | } 59 | } 60 | 61 | function wrapParenthesis(tree: EquationNode): EquationNode { 62 | if (canStandAlone(tree)) { 63 | return tree 64 | } else { 65 | return { 66 | type: 'block', 67 | child: tree, 68 | } 69 | } 70 | } 71 | 72 | function canStandAlone(tree: EquationNode): boolean { 73 | return tree.type === 'variable' || 74 | tree.type === 'number' || 75 | tree.type === 'block' || 76 | tree.type === 'function' || 77 | tree.type === 'matrix' || 78 | tree.type === 'divide-fraction' || 79 | tree.type === 'power' || 80 | tree.type === 'operand-placeholder' || 81 | tree.type === 'function-placeholder' || 82 | (( 83 | tree.type === 'negative' || 84 | tree.type === 'positive' || 85 | tree.type === 'positive-negative' || 86 | tree.type === 'operator-unary-placeholder' 87 | ) && canStandAlone(tree.value)) 88 | } 89 | -------------------------------------------------------------------------------- /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/rendering/special/sqrt/root-symbol.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function RootSymbol({ height, style }: { height: number, style?: React.CSSProperties }) { 4 | height = Math.max(height, 1.4) 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | type SvgCommand = { 13 | c: string, 14 | v: number[], 15 | } 16 | 17 | function morphSvg(commands: SvgCommand[], height: number) { 18 | const offsetHeight = height - 1.4 19 | // Factors determined by experiment 20 | // Move top and inner elbow to keep the width of the long 21 | // line at ~0.5em 22 | const topOffsetFactor = 0.015 * Math.atan(offsetHeight * 1.25) 23 | const elbowFactor = 0.08 * Math.atan(offsetHeight * 0.556) 24 | return commands.map(({ c, v }, idx) => { 25 | // First point, anchor of top 26 | // Move full offset 27 | if (idx === 0) { 28 | return { 29 | c, 30 | v: [ 31 | v[0] - topOffsetFactor, 32 | v[1], 33 | ], 34 | } 35 | // Second point, beizer 36 | // Move to keep round top 37 | } else if (idx === 1) { 38 | return { 39 | c, 40 | v: [ 41 | v[0] - topOffsetFactor * 0.75, 42 | v[1], 43 | v[2] - topOffsetFactor * 0.5, 44 | v[3], 45 | ], 46 | } 47 | // Last point is inside elbow, should move up along short line 48 | } else if (idx === 18) { 49 | return { 50 | c, 51 | v: [ 52 | // Move along existing line 53 | v[0] - elbowFactor * 0.416, 54 | v[1] - elbowFactor * 0.909 + offsetHeight, 55 | ], 56 | } 57 | // Move other points down 58 | } else if (idx > 3) { 59 | return { 60 | c, 61 | v: v.map((value, valueIdx) => ( 62 | valueIdx % 2 === 0 63 | ? value 64 | : value + offsetHeight 65 | )), 66 | } 67 | } 68 | 69 | return { c, v } 70 | }) 71 | } 72 | 73 | // Build path-string from commands 74 | function buildPath(commands: SvgCommand[]) { 75 | return commands 76 | .map(({ c, v }) => c + v.join(' ')).join('') 77 | } 78 | 79 | // Commands used to draw a square root sign 80 | const pathCommands = [ 81 | { c: 'M', v: [0.7767, 0.014] }, 82 | { c: 'Q', v: [0.7847, 0, 0.7967, 0] }, 83 | { c: 'L', v: [0.8, 0] }, 84 | { c: 'L', v: [0.8, 0.08] }, 85 | { c: 'L', v: [0.351, 0.993] }, 86 | { c: 'Q', v: [0.347, 1, 0.332, 1] }, 87 | { c: 'Q', v: [0.323, 1, 0.32, 0.997] }, 88 | { c: 'L', v: [0.126, 0.575] }, 89 | { c: 'L', v: [0.11, 0.586] }, 90 | { c: 'Q', v: [0.095, 0.598, 0.079, 0.61] }, 91 | { c: 'T', v: [0.061, 0.622] }, 92 | { c: 'Q', v: [0.057, 0.622, 0.048, 0.614] }, 93 | { c: 'T', v: [0.038, 0.6] }, 94 | { c: 'Q', v: [0.038, 0.597, 0.039, 0.596] }, 95 | { c: 'Q', v: [0.041, 0.592, 0.106, 0.542] }, 96 | { c: 'T', v: [0.173, 0.491] }, 97 | { c: 'Q', v: [0.175, 0.489, 0.178, 0.489] }, 98 | { c: 'Q', v: [0.185, 0.489, 0.19, 0.499] }, 99 | { c: 'L', v: [0.3578, 0.8658] }, 100 | { c: 'Z', v: [] }, 101 | ] 102 | -------------------------------------------------------------------------------- /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 |