Skip to content

Instantly share code, notes, and snippets.

@Sheraff
Last active January 19, 2022 16:20
Show Gist options
  • Save Sheraff/9031fcbbaa5b3557b4b5acb8a8624cd0 to your computer and use it in GitHub Desktop.
Save Sheraff/9031fcbbaa5b3557b4b5acb8a8624cd0 to your computer and use it in GitHub Desktop.
calc
import { useEffect, useRef, useState } from 'react'
import parse from './ast.js'
import { mapCaretToAST } from './mapInputToAST.js'
import AstParser from './AstClass.js'
import {
constPlugin,
groupPlugin,
numberPlugin,
stringPlugin,
leftUnaryOperatorsPlugin,
rightUnaryOperatorsPlugin,
powBinaryOperatorPlugin,
andBinaryOperatorPlugin,
orBinaryOperatorPlugin,
minusUnaryOperatorPlugin,
} from './plugins.js'
const parser = new AstParser([
constPlugin,
groupPlugin,
minusUnaryOperatorPlugin,
leftUnaryOperatorsPlugin,
rightUnaryOperatorsPlugin,
powBinaryOperatorPlugin,
andBinaryOperatorPlugin,
orBinaryOperatorPlugin,
numberPlugin,
stringPlugin,
])
function Value({value, children}) {
const [hover, setHover] = useState(false)
const [childHover, setChildHover] = useState(false)
const ref = useRef(null)
const onMouseEnter = (e) => {
setHover(true)
const self = e.target.closest('.value') === e.currentTarget
if (self) {
e.stopPropagation()
ref.current.dispatchEvent(new CustomEvent('hover:start', {bubbles: true}))
}
}
const onMouseLeave = (e) => {
setHover(false)
ref.current.dispatchEvent(new CustomEvent('hover:end', {bubbles: true}))
}
useEffect(() => {
const {current} = ref
const onHoverStart = (e) => {
if (e.target !== current) {
setChildHover(true)
}
}
const onHoverEnd = (e) => {
if (e.target !== current) {
setChildHover(false)
e.stopPropagation()
}
}
current.addEventListener('hover:start', onHoverStart)
current.addEventListener('hover:end', onHoverEnd)
return () => {
current.removeEventListener('hover:start', onHoverStart)
current.removeEventListener('hover:end', onHoverEnd)
}
}, [])
return (
<span
ref={ref}
className={'value' + ((hover && !childHover) ? ' hover' : '')}
title={value}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{children}
</span>
)
}
function Binary({left, right, operation, computed}) {
return (
<Value value={computed}>
<Dynamic key={left.asString} {...left}/>
<code> {operation} </code>
<Dynamic key={right.asString} {...right}/>
</Value>
)
}
function Unary({child, operation, computed, operatorPosition}) {
const left = operatorPosition === 'left'
return (
<Value value={computed}>
{left && <code>{operation}</code>}
<Dynamic key={child.asString} {...child}/>
{!left && <code>{operation}</code>}
</Value>
)
}
function Group({child, computed}) {
return (
<Value value={computed}>
<code>(</code>
<Dynamic key={child.asString} {...child}/>
<code>)</code>
</Value>
)
}
function Number({computed, asString}) {
return (
<Value value={computed}>
<code>{asString}</code>
</Value>
)
}
function Const({computed, asString}) {
return (
<Value value={computed}>
<code>{asString}</code>
</Value>
)
}
function Dynamic({type, ...props}) {
switch (type) {
case 'operation-binary':
return <Binary key={props.asString} {...props} />
case 'operation-unary':
return <Unary key={props.asString} {...props} />
case 'group':
return <Group key={props.asString} {...props} />
case 'number':
return <Number key={props.asString} {...props} />
case 'const':
return <Const key={props.asString} {...props} />
case undefined:
return ''
default:
throw new Error(`Unknown type: ${type}`)
}
}
function Input({
id,
onChange,
onCaret,
defaultValue,
}) {
const ref = useRef(null)
useEffect(() => {
const {current} = ref
const onSelect = (e) => {
if (document.activeElement === current) {
onCaret([current.selectionStart, current.selectionEnd])
}
}
document.addEventListener('selectionchange', onSelect)
return () => {
document.removeEventListener('selectionchange', onSelect)
}
}, [onCaret])
return (
<input
ref={ref}
id={id}
onChange={onChange}
type="text"
defaultValue={defaultValue}
/>
)
}
function Output({
htmlFor,
parsed,
caret,
}) {
const a = 1
return (
<div>
<Caret caret={caret} parsed={parsed}>
<Dynamic {...parsed}/>
</Caret>
<code>
<span> = </span>
<output for={htmlFor}>
<Value className="result" value={parsed.computed}>
{parsed.computed}
</Value>
</output>
</code>
</div>
)
}
function Caret({caret, parsed, children}) {
const ref = useRef(null)
const mapped = mapCaretToAST(parsed, caret)
// console.log(mapped)
return (
<div className='caretLine'>
<code
ref={ref}
className='caret'
style={{
'--left': mapped[0],
'--width': mapped[1] - mapped[0] + 1,
}}
/>
<div>{children}</div>
</div>
)
}
export default function App() {
const initial = 'sin(10 +2.1) * pi^2'
const [parsed, setParsed] = useState(() => parser.parse(initial))
const [caret, setCaret] = useState([0, 0])
return (
<>
<Input
id="input"
onChange={(e) => setParsed(parser.parse(e.target.value))}
onCaret={setCaret}
defaultValue={initial}
/>
<Output
htmlFor="input"
parsed={parsed}
caret={caret}
/>
</>
);
}
/**
* @typedef {Object} Token
* @property {string} type
*
*
* @typedef {Object} _GroupDelimiterToken
* @property {'group-start' | 'group-end'} type
* @property {number} depth
*
* @typedef {Token & _GroupDelimiterToken} GroupDelimiterToken
*
*
* @typedef {Object} _OperatorToken
* @property {'operator'} type
* @property {string} value
*
* @typedef {Token & _OperatorToken} OperatorToken
*
*
* @typedef {Object} _StringToken
* @property {'string'} type
* @property {string} value
*
* @typedef {Token & _StringToken} StringToken
*
*
* @typedef {Object} _NumberToken
* @property {'number'} type
* @property {number} value
*
* @typedef {Token & _NumberToken} NumberToken
*
*
* @typedef {Object} _TreeToken
* @property {TreeToken?} child
* @property {TreeToken?} left
* @property {TreeToken?} right
*
* @typedef {Token & _TreeToken} TreeToken
*
*
* @typedef {Object} _ComputedToken
* @property {number} computed
*
* @typedef {Token & _ComputedToken} ComputedToken
*
*/
/**
* @template T
* @param {number} i
* @param {Array<T>} string
* @param {(arg: T) => boolean} test
* @returns {Array<T>}
*/
function findSequence(i, string, test) {
let sequence = []
while (i < string.length && test(string[i])) {
sequence.push(string[i])
i++
}
return sequence
}
/**
* TODO:
* - add Math.PI and other literals
*/
/**
*
* @param {string} _str
* @returns {Token[]}
*/
function tokenize(_str) {
const str = _str.split('')
const tokens = []
let depth = 0
for (let i = 0; i < str.length; i++) {
const char = str[i]
switch (true) {
case char === ' ':
break
case char === '(':
tokens.push({ type: 'group-start', depth, range: [i, i] })
depth++
break
case char === ')':
depth--
tokens.push({ type: 'group-end', depth, range: [i, i] })
break
case char === '+':
case char === '-':
case char === '*':
case char === '/':
case char === '^':
case char === '!':
case char === '²':
case char === '³':
case char === '√':
tokens.push({ type: 'operator', value: char, range: [i, i] })
break
case /[0-9]/.test(char): {
const number = findSequence(i, str, (c) => ( /[0-9]/.test(c) || c === '.'))
const length = number.length - 1
tokens.push({ type: 'number', value: Number.parseFloat(number.join('')), range: [i, i + length] })
i += length
break
}
case /[a-zA-Z]/.test(char):
const string = findSequence(i, str, (c) => /[a-zA-Z]/.test(c))
const length = string.length - 1
tokens.push({ type: 'string', value: string.join(''), range: [i, i + length] })
i += length
break
default:
throw new Error(`Invalid character: ${char}`)
}
}
return tokens
}
const KNOWN_CONSTANTS = ['pi']
/**
* @param {Array<Token | GroupDelimiterToken>} stack
* @returns {Array<TreeToken>}
*/
function reduceKnownConstants(stack) {
const result = []
for (let i = 0; i < stack.length; i++) {
const token = stack[i]
if (
token.type === 'string'
&& KNOWN_CONSTANTS.includes(token.value)
) {
result.push({ type: 'const', value: token.value, range: token.range })
} else {
result.push(token)
}
}
return result
}
/**
*
* @param {Token} token
* @returns {boolean}
*/
function tokenHasIntrinsicValue(token) {
return ['number', 'group', 'operation-unary', 'operation-binary', 'const'].includes(token.type)
}
/**
* @param {Array<Token | GroupDelimiterToken>} stack
* @returns {Array<TreeToken>}
*/
function reduceExplicitGroups(stack) {
const result = []
for (let i = 0; i < stack.length; i++) {
const token = stack[i]
if (token.type === 'group-start') {
const subTokens = findSequence(i + 1, stack, (t) => t.type !== 'group-end' || t.depth !== token.depth)
i += subTokens.length + 1
const range = [token.range[0], stack[i].range[1]]
result.push({ type: 'group', child: tokensToAbstractSyntaxTree(subTokens), range })
} else {
result.push(token)
}
}
return result
}
const RIGHT_UNARY_OPERATORS = ['!', '²', '³']
const LEFT_UNARY_OPERATORS = ['sin', 'cos', 'tan', 'log', 'ln', 'sqrt', '√']
/**
* @param {Array<Token | OperatorToken | StringToken>} stack
* @returns {Array<TreeToken>}
*/
function reduceUnaryOperators(stack) {
const result = []
for (let i = 0; i < stack.length; i++) {
const token = stack[i]
if (
result.length > 0
&& (token.type === 'operator' || token.type === 'string')
&& RIGHT_UNARY_OPERATORS.includes(token.value)
&& tokenHasIntrinsicValue(result[result.length - 1])
) {
const child = result.pop()
const range = [child.range[0], token.range[1]]
result.push({ type: 'operation-unary', operation: token.value, child, operatorPosition: 'right', range })
} else if (
i + 1 < stack.length
&& (token.type === 'operator' || token.type === 'string')
&& LEFT_UNARY_OPERATORS.includes(token.value)
&& tokenHasIntrinsicValue(stack[i + 1])
) {
const child = stack[i + 1]
const range = [token.range[0], child.range[1]]
result.push({ type: 'operation-unary', operation: token.value, child, operatorPosition: 'left', range })
i++
} else if (
i + 1 < stack.length
&& token.type === 'operator'
&& token.value === '-'
&& tokenHasIntrinsicValue(stack[i + 1])
&& (result.length === 0 || !tokenHasIntrinsicValue(result[result.length - 1]))
) {
const child = stack[i + 1]
const range = [token.range[0], child.range[1]]
result.push({ type: 'operation-unary', operation: token.value, child, operatorPosition: 'left', range })
i++
} else {
result.push(token)
}
}
return result
}
const PRIORITY_BINARY_OPERATORS = [
['^'],
['*', '/'],
['+', '-'],
]
/**
* @param {Array<Token | OperatorToken>} stack
* @returns {Array<TreeToken>}
*/
function reduceBinaryOperators(stack, level = 0) {
const result = []
for (let i = 0; i < stack.length; i++) {
const token = stack[i]
if (
result.length > 0
&& i + 1 < stack.length
&& token.type === 'operator'
&& PRIORITY_BINARY_OPERATORS[level].includes(token.value)
&& tokenHasIntrinsicValue(result[result.length - 1])
&& tokenHasIntrinsicValue(stack[i + 1])
) {
const left = result.pop()
const right = stack[i + 1]
const range = [left.range[0], right.range[1]]
result.push({ type: 'operation-binary', operation: token.value, left, right, range })
i++
} else {
result.push(token)
}
}
if (level < PRIORITY_BINARY_OPERATORS.length - 1) {
return reduceBinaryOperators(result, level + 1)
}
return result
}
/**
* @param {Token[]} tokens
* @returns {TreeToken}
*/
function tokensToAbstractSyntaxTree(tokens) {
let stack = [...tokens]
stack = reduceKnownConstants(stack)
stack = reduceExplicitGroups(stack)
stack = reduceUnaryOperators(stack)
stack = reduceBinaryOperators(stack)
return stack[0]
}
/**
*
* @param {TreeToken} node
* @param {Object} walkers
* @param {Array<(arg: TreeToken) => void>} [walkers.entry]
* @param {Array<(arg: TreeToken) => void>} [walkers.exit]
*/
function walk(node, walkers) {
const clone = {...node}
const {entry = [], exit = []} = walkers
entry.forEach((fn) => fn(clone))
if (clone.left) {
clone.left = walk(clone.left, walkers)
}
if (clone.child) {
clone.child = walk(clone.child, walkers)
}
if (clone.right) {
clone.right = walk(clone.right, walkers)
}
exit.forEach((fn) => fn(clone))
return clone
}
function factorial(n) {
return n < 2
? 1
: factorial(n - 1) * n
}
/**
* @param {TreeToken} node
*/
function resolveConstant(node) {
switch (node.value) {
case 'pi':
return Math.PI
default:
throw new Error(`Invalid constant value: "${node.value}"`)
}
}
/**
* @param {TreeToken} node
*/
function resolveUnaryOperation(node) {
const input = node.child.computed
switch (node.operation) {
case '-':
return -input
case '!':
return factorial(input)
case '²':
return input**2
case '³':
return input**3
case 'sin':
return Math.sin(input)
case 'cos':
return Math.cos(input)
case 'tan':
return Math.tan(input)
case 'log':
return Math.log(input)
case 'ln':
return Math.log(input)
case '√':
case 'sqrt':
return Math.sqrt(input)
default:
throw new Error(`Invalid unary operation: "${node.operation}"`)
}
}
function getExponent(number) {
if (Number.isInteger(number)) {
return 0
}
const frac = number - Math.floor(number)
return Math.floor(Math.log(frac) / Math.log(10))
}
function getMultiplier(a, b) {
const exponent = -1 * Math.min(getExponent(a), getExponent(b))
return Math.pow(10, Math.max(0, exponent))
}
function multiply(a, b) {
const m = getMultiplier(a, b)
return (a * m) * (b * m) / (m * m)
}
function divide(a, b) {
const m = getMultiplier(a, b)
return (a * m) / (b * m)
}
function add(a, b) {
const m = getMultiplier(a, b)
return ((a * m) + (b * m)) / m
}
function subtract(a, b) {
const m = getMultiplier(a, b)
return ((a * m) - (b * m)) / m
}
/**
* @param {TreeToken} node
*/
function resolveBinaryOperation(node) {
const left = node.left.computed
const right = node.right.computed
switch (node.operation) {
case '+':
return add(left, right)
case '-':
return subtract(left, right)
case '*':
return multiply(left, right)
case '/':
return divide(left, right)
case '^':
return Math.pow(left, right)
default:
throw new Error(`Invalid binary operation: ${node.operation}`)
}
}
/**
* @param {(ComputedToken | NumberToken) & TreeToken} node
*/
function computeNode(node) {
if (node.type === 'number') {
return node.value
} else if (node.type === 'const') {
return resolveConstant(node)
} else if (node.type === 'group') {
return node.child.computed
} else if (node.type === 'operation-unary') {
return resolveUnaryOperation(node)
} else if (node.type === 'operation-binary') {
return resolveBinaryOperation(node)
} else {
return NaN
}
}
function constantNodeAsString(node) {
switch (node.value) {
case 'pi':
return 'π'
default:
throw new Error(`Invalid constant string conversion: "${node.value}"`)
}
}
function unaryNodeAsString(node) {
const input = node.child.asString
switch (node.operatorPosition) {
case 'right':
return `${input}${node.operation}`
case 'left':
return `${node.operation}${input}`
default:
throw new Error(`Invalid unary string conversion: "${node.operation}"`)
}
}
function stringifyNode(node) {
if (node.type === 'number') {
return `${node.value}`
} else if (node.type === 'const') {
return constantNodeAsString(node)
} else if (node.type === 'group') {
return `(${node.child.asString})`
} else if (node.type === 'operation-unary') {
return unaryNodeAsString(node)
} else if (node.type === 'operation-binary') {
return `${node.left.asString} ${node.operation} ${node.right.asString}`
} else {
return ''
}
}
export default function parse(string) {
const tokens = tokenize(string)
const ast = tokensToAbstractSyntaxTree(tokens)
const processed = walk(ast, {exit: [
(node) => { node.computed = computeNode(node) },
(node) => { node.asString = stringifyNode(node) },
]})
// console.log(ast)
// console.log(processed.asString + ' = ' + processed.computed)
// console.log(JSON.stringify(processed))
return processed
}
// parse('sin(10 + 2.1) * pi^2')
// parse('2 * -5')
// parse('3*0.3')
// parse('0.3 / 3')
// parse('0.1 + 0.2')
// parse('0.3 - 0.1')
// parse('0.1-0.3')
// parse('√2²')
// parse('pi * 2²')
export default class AST {
constructor(plugins) {
this.plugins = plugins
}
tokenize(code) {
const context = {
stack: code.split(''),
tokens: [],
}
for (context.i = 0; context.i < context.stack.length; context.i++) {
context.item = context.stack[context.i]
for (const plugin of this.plugins) {
if (plugin.tokenize) {
const match = plugin.tokenize(context, this)
if (match) {
context.tokens.push(match)
break
}
}
}
}
return context.tokens
}
reduce(tokens) {
const context = {
result: [],
stack: [...tokens]
}
for (const plugin of this.plugins) {
if (plugin.reduce) {
for (context.i = 0; context.i < context.stack.length; context.i++) {
context.item = context.stack[context.i]
const match = plugin.reduce(context, this)
if (match) {
context.result.push(match)
} else {
context.result.push(context.item)
}
}
context.stack = [...context.result]
context.result = []
}
}
return context.stack[0]
}
walk(node, exit) {
const clone = {...node}
if (clone.left) {
clone.left = this.walk(clone.left, exit)
}
if (clone.child) {
clone.child = this.walk(clone.child, exit)
}
if (clone.right) {
clone.right = this.walk(clone.right, exit)
}
exit.forEach((fn) => fn(clone))
return clone
}
resolve(node) {
for (const plugin of this.plugins) {
if (plugin.resolve) {
const match = plugin.resolve(node, this)
if (match !== undefined) {
node.computed = match
break
}
}
}
}
stringify(node) {
for (const plugin of this.plugins) {
if (plugin.stringify) {
const match = plugin.stringify(node, this)
if (match !== undefined) {
node.asString = match
break
}
}
}
}
parse(code) {
const tokens = this.tokenize(code)
const ast = this.reduce(tokens)
console.log(ast)
const processed = this.walk(ast, [
this.resolve.bind(this),
this.stringify.bind(this),
])
// console.log(ast)
// console.log(processed.asString + ' = ' + processed.computed)
// console.log(JSON.stringify(processed))
// console.log(processed)
return processed
}
hasIntrinsicValue(token) {
for (const plugin of this.plugins) {
if (plugin.hasIntrinsicValue && plugin.hasIntrinsicValue(token, this)) {
return true
}
}
return false
}
}
{
"scripts": [
"react",
"react-dom"
],
"styles": []
}
export function mapCaretToAST(ast, caret) {
if (ast.range[0] === caret[0] && ast.range[1] === caret[1]) {
return [
ast.range[0],
ast.range[1],
]
}
if (ast.child) {
return mapCaretToAST(ast.child, caret)
}
if (ast.left && ast.right) {
const isInLeft = ast.left.range[1] >= caret[0]
const isInRight = ast.right.range[0] <= caret[1]
if (isInLeft && isInRight) {
return [
ast.range[0],
ast.range[1],
]
}
if (isInLeft && ast.left.range[1] <= caret[1]) {
return mapCaretToAST(ast.left, caret)
}
if (isInRight && ast.right.range[1] >= caret[0]) {
return mapCaretToAST(ast.right, caret)
}
}
return [
ast.range[0],
ast.range[1],
]
}
import {
lookAhead,
findSequence,
factorial,
multiply,
divide,
add,
subtract,
} from './utils.js'
const KNOWN_CONSTANTS = ['pi']
export const constPlugin = {
reduce(context) {
if (
context.item.type === 'string'
&& KNOWN_CONSTANTS.includes(context.item.value)
) {
return { type: 'const', value: context.item.value, range: context.item.range }
}
},
resolve(node) {
if (node.type === 'const') {
if (node.value === 'pi') {
return Math.PI
}
}
},
stringify(node) {
if (node.type === 'const') {
if (node.value === 'pi') {
return 'π'
}
}
},
hasIntrinsicValue(node) {
if (node.type === 'const') {
return true
}
}
}
export const groupPlugin = {
tokenize(context) {
if (!('depth' in context)) {
context.depth = 0
}
if (context.item === '(') {
const {i, depth} = context
context.depth++
return { type: 'group-start', depth, range: [i, i] }
}
if (context.item === ')') {
context.depth--
const {i, depth} = context
return { type: 'group-end', depth, range: [i, i] }
}
},
reduce(context, parser) {
if (context.item.type === 'group-start') {
const {i, item, stack} = context
const subTokens = findSequence(i + 1, stack, (t) => t.type !== 'group-end' || t.depth !== item.depth)
context.i += subTokens.length + 1
const range = [item.range[0], stack[context.i].range[1]]
return { type: 'group', child: parser.reduce(subTokens), range }
}
},
resolve(node) {
if (node.type === 'group') {
return node.child.computed
}
},
stringify(node) {
if (node.type === 'group') {
return `(${node.child.asString})`
}
},
hasIntrinsicValue(node) {
if (node.type === 'group') {
return true
}
}
}
export const numberPlugin = {
tokenize(context) {
if (/[0-9]/.test(context.item)) {
const {i, stack} = context
const number = findSequence(i, stack, (c) => ( /[0-9]/.test(c) || c === '.'))
const length = number.length - 1
context.i += length
return { type: 'number', value: Number.parseFloat(number.join('')), range: [i, i + length] }
}
},
resolve(node) {
if (node.type === 'number') {
return node.value
}
},
stringify(node) {
if (node.type === 'number') {
return `${node.value}`
}
},
hasIntrinsicValue(node) {
if (node.type === 'number') {
return true
}
}
}
export const stringPlugin = {
tokenize(context) {
if (/[a-zA-Z]/.test(context.item)) {
const {i, stack} = context
const string = findSequence(i, stack, (c) => ( /[a-zA-Z]/.test(c) || c === '.'))
const length = string.length - 1
context.i += length
return { type: 'string', value: string.join(''), range: [i, i + length] }
}
},
resolve(node) {
if (node.type === 'string') {
return NaN
}
},
stringify(node) {
if (node.type === 'string') {
return node.value
}
}
}
const LEFT_UNARY_OPERATORS = ['sin', 'cos', 'tan', 'log', 'ln', 'sqrt', '√']
export const leftUnaryOperatorsPlugin = {
tokenize(context) {
if (context.item === '√') {
return { type: 'operator', value: context.item, range: [context.i, context.i + 1] }
}
},
reduce(context, parser) {
if (context.item.type === 'string' || context.item.type === 'operator') {
if (
context.i + 1 < context.stack.length
&& LEFT_UNARY_OPERATORS.includes(context.item.value)
&& parser.hasIntrinsicValue(context.stack[context.i + 1])
) {
const {stack, item, i} = context
const child = stack[i + 1]
const range = [item.range[0], child.range[1]]
context.i++
return { type: 'operation-unary', operation: item.value, child, operatorPosition: 'left', range }
}
}
},
resolve(node) {
if (node.type === 'operation-unary' && node.operatorPosition === 'left') {
const input = node.child.computed
switch (node.operation) {
case 'sin':
return Math.sin(input)
case 'cos':
return Math.cos(input)
case 'tan':
return Math.tan(input)
case 'log':
return Math.log(input)
case 'ln':
return Math.log(input)
case 'sqrt':
case '√':
return Math.sqrt(input)
}
}
},
stringify(node) {
if (node.type === 'operation-unary' && node.operatorPosition === 'left') {
return `${node.operation}${node.child.asString}`
}
},
hasIntrinsicValue(node) {
if (node.type === 'operation-unary' && node.operatorPosition === 'left') {
return true
}
}
}
const RIGHT_UNARY_OPERATORS = ['!', '²', '³']
export const rightUnaryOperatorsPlugin = {
tokenize(context) {
if (RIGHT_UNARY_OPERATORS.includes(context.item)) {
return { type: 'operator', value: context.item, range: [context.i, context.i + 1] }
}
},
reduce(context, parser) {
if (context.item.type === 'string' || context.item.type === 'operator') {
if (
context.result.length > 0
&& RIGHT_UNARY_OPERATORS.includes(context.item.value)
&& parser.hasIntrinsicValue(context.results[context.result.length - 1])
) {
const {result, item, i} = context
const child = result.pop()
const range = [child.range[0], item.range[1]]
return { type: 'operation-unary', operation: item.value, child, operatorPosition: 'right', range }
}
}
},
resolve(node) {
if (node.type === 'operation-unary' && node.operatorPosition === 'right') {
const input = node.child.computed
switch (node.operation) {
case '!':
return factorial(input)
case '²':
return input**2
case '³':
return input**3
}
}
},
stringify(node) {
if (node.type === 'operation-unary' && node.operatorPosition === 'right') {
return `${input}${node.operation}`
}
},
hasIntrinsicValue(node) {
if (node.type === 'operation-unary' && node.operatorPosition === 'right') {
return true
}
}
}
export const powBinaryOperatorPlugin = {
tokenize(context) {
if (context.item === '^') {
return { type: 'operator', value: context.item, range: [context.i, context.i] }
}
},
reduce(context, parser) {
if (context.item.type === 'operator' && context.item.value === '^') {
const {stack, result, item, i} = context
if (
result.length > 0
&& i + 1 < stack.length
&& parser.hasIntrinsicValue(result[result.length - 1])
&& parser.hasIntrinsicValue(stack[i + 1])
) {
const left = result.pop()
const right = stack[i + 1]
const range = [left.range[0], right.range[1]]
context.i++
return { type: 'operation-binary', operation: item.value, left, right, range }
}
}
},
resolve(node) {
if (node.type === 'operation-binary' && node.operation === '^') {
const left = node.left.computed
const right = node.right.computed
return Math.pow(left, right)
}
},
stringify(node) {
if (node.type === 'operation-binary' && node.operation === '^') {
return `${node.left.asString} ^ ${node.right.asString}`
}
},
hasIntrinsicValue(node) {
if (node.type === 'operation-binary' && node.operation === '^') {
return true
}
}
}
const AND_BINARY_OPERATORS = ['*', '/', '×']
export const andBinaryOperatorPlugin = {
tokenize(context) {
if (AND_BINARY_OPERATORS.includes(context.item)) {
return { type: 'operator', value: context.item, range: [context.i, context.i] }
}
},
reduce(context, parser) {
if (context.item.type === 'operator' && AND_BINARY_OPERATORS.includes(context.item.value)) {
const {stack, result, item, i} = context
if (
result.length > 0
&& i + 1 < stack.length
&& parser.hasIntrinsicValue(result[result.length - 1])
&& parser.hasIntrinsicValue(stack[i + 1])
) {
const left = result.pop()
const right = stack[i + 1]
const range = [left.range[0], right.range[1]]
context.i++
return { type: 'operation-binary', operation: item.value, left, right, range }
}
}
},
resolve(node) {
if (node.type === 'operation-binary' && AND_BINARY_OPERATORS.includes(node.operation)) {
const left = node.left.computed
const right = node.right.computed
switch (node.operation) {
case '*':
case '×':
return multiply(left, right)
case '/':
return divide(left, right)
}
}
},
stringify(node) {
if (node.type === 'operation-binary' && AND_BINARY_OPERATORS.includes(node.operation)) {
return `${node.left.asString} ${node.operation} ${node.right.asString}`
}
},
hasIntrinsicValue(node) {
if (node.type === 'operation-binary' && AND_BINARY_OPERATORS.includes(node.operation)) {
return true
}
}
}
const OR_BINARY_OPERATORS = ['+', '-']
export const orBinaryOperatorPlugin = {
tokenize(context) {
if (OR_BINARY_OPERATORS.includes(context.item)) {
return { type: 'operator', value: context.item, range: [context.i, context.i] }
}
},
reduce(context, parser) {
if (context.item.type === 'operator' && OR_BINARY_OPERATORS.includes(context.item.value)) {
const {stack, result, item, i} = context
if (
result.length > 0
&& i + 1 < stack.length
&& parser.hasIntrinsicValue(result[result.length - 1])
&& parser.hasIntrinsicValue(stack[i + 1])
) {
const left = result.pop()
const right = stack[i + 1]
const range = [left.range[0], right.range[1]]
context.i++
return { type: 'operation-binary', operation: item.value, left, right, range }
}
}
},
resolve(node) {
if (node.type === 'operation-binary' && OR_BINARY_OPERATORS.includes(node.operation)) {
const left = node.left.computed
const right = node.right.computed
switch (node.operation) {
case '+':
return add(left, right)
case '-':
return subtract(left, right)
}
}
},
stringify(node) {
if (node.type === 'operation-binary' && OR_BINARY_OPERATORS.includes(node.operation)) {
return `${node.left.asString} ${node.operation} ${node.right.asString}`
}
},
hasIntrinsicValue(node) {
if (node.type === 'operation-binary' && OR_BINARY_OPERATORS.includes(node.operation)) {
return true
}
}
}
export const minusUnaryOperatorPlugin = {
reduce(context, parser) {
if (context.item.type === 'operator' && context.item.value === '-') {
const {stack, result, item, i} = context
if (
i + 1 < stack.length
&& parser.hasIntrinsicValue(stack[i + 1])
&& (result.length === 0 || !parser.hasIntrinsicValue(result[result.length - 1]))
) {
const child = stack[i + 1]
const range = [item.range[0], child.range[1]]
context.i++
return { type: 'operation-unary', operation: item.value, child, operatorPosition: 'left', range }
}
}
},
resolve(node) {
if (node.type === 'operation-unary' && node.operation === '-' && node.operatorPosition === 'left') {
return -node.child.computed
}
}
}
.value {
cursor: default;
&.hover {
box-shadow: inset 0 0 0 1px lime;
}
}
.caretLine {
position: relative;
isolation: isolate;
z-index: 0;
display: inline-block;
div {
white-space: nowrap;
}
}
.caret {
--left: 0;
--width: 1;
position: absolute;
z-index: -1;
background-color: rgba(0, 255, 0, 0.432);
white-space: pre;
transform-origin: left;
transform: translateX(calc(var(--left) * 100%)) scaleX(calc(var(--width) * 100%));
&::after {
content: " ";
}
}
/**
* @template T
* @param {number} i
* @param {Array<T>} string
* @param {(arg: T) => boolean} test
* @returns {Array<T>}
*/
export function findSequence(i, string, test) {
let sequence = []
while (i < string.length && test(string[i])) {
sequence.push(string[i])
i++
}
return sequence
}
export function lookAhead(i, string, match) {
for (let j = 0; j < match.length; j++) {
if (match[j] !== string[i + j]) {
return false
}
}
return true
}
export function factorial(n) {
return n < 2
? 1
: factorial(n - 1) * n
}
function getExponent(number) {
if (Number.isInteger(number)) {
return 0
}
const frac = number - Math.floor(number)
return Math.floor(Math.log(frac) / Math.log(10))
}
function getMultiplier(a, b) {
const exponent = -1 * Math.min(getExponent(a), getExponent(b))
return Math.pow(10, Math.max(0, exponent))
}
export function multiply(a, b) {
const m = getMultiplier(a, b)
return (a * m) * (b * m) / (m * m)
}
export function divide(a, b) {
const m = getMultiplier(a, b)
return (a * m) / (b * m)
}
export function add(a, b) {
const m = getMultiplier(a, b)
return ((a * m) + (b * m)) / m
}
export function subtract(a, b) {
const m = getMultiplier(a, b)
return ((a * m) - (b * m)) / m
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment