Last active
January 19, 2022 16:20
-
-
Save Sheraff/9031fcbbaa5b3557b4b5acb8a8624cd0 to your computer and use it in GitHub Desktop.
calc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | |
/> | |
</> | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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²') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"scripts": [ | |
"react", | |
"react-dom" | |
], | |
"styles": [] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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], | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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: " "; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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