UNPKG

mathup

Version:

Easy MathML authoring tool with a quick to write syntax

310 lines (256 loc) 6.33 kB
import { addZeroLSpaceToOperator, maybeFixDoublePipe } from "../utils.js"; import expr from "./expr.js"; import handlers from "./index.js"; /** * @typedef {import("../parse.js").State} State * @typedef {import("../index.js").Node} Node * @typedef {import("../index.js").Term} Term * @typedef {import("../index.js").BinaryOperation} BinaryOperation * @typedef {import("../index.js").TernaryOperation} TernaryOperation */ /** @returns {Term} */ function empty() { return { type: "Term", items: [] }; } const SHOULD_STOP = ["ident", "number", "operator", "text"]; /** * Remove surrounding brackets. * * @template {BinaryOperation | TernaryOperation} Operation * @param {Operation} node * @returns {Operation} */ function maybeRemoveFence(node) { const mutated = node; mutated.items.forEach((item, i) => { if (item.type !== "FencedGroup" || item.items.length !== 1) { // No fences to remove. return; } if (i === 0 && node.name !== "frac") { // Keep fences around base in sub- and superscripts. return; } const [cell] = item.items; if (cell.length !== 1) { mutated.items[i] = { type: "Term", items: cell }; return; } const [first] = cell; const term = first.type === "Term" && first.items.length === 1 ? first.items[0] : first; if (term.type.endsWith("Literal")) { // We fenced a single item for a reason, lets keep them. return; } mutated.items[i] = term; }); return mutated; } /** * Change `lim` to `under`, and `sum` and `prod` to `under` or `over`. * * @template {BinaryOperation | TernaryOperation} Operation * @param {Operation} node * @returns {Operation} */ function maybeApplyUnderOver(node) { const mutated = node; const [operator] = node.items; if (operator.type !== "OperatorLiteral") { return mutated; } if ( node.name === "sub" && ["lim", "∑", "∏", "⋂", "⋃", "⋀", "⋁"].includes(operator.value) ) { mutated.name = "under"; return mutated; } if ( node.name === "subsup" && ["∑", "∏", "⋂", "⋃", "⋀", "⋁"].includes(operator.value) ) { mutated.name = "underover"; return mutated; } return mutated; } /** * @template {BinaryOperation | TernaryOperation} Operation * @param {Operation} node * @returns {Operation} */ function fixFracSpacing(node) { if (node.name !== "frac") { return node; } for (const item of node.items) { addZeroLSpaceToOperator(item); } return node; } /** * @template {BinaryOperation | TernaryOperation} Operation * @param {Operation} node * @returns {Operation} */ function post(node) { return fixFracSpacing(maybeRemoveFence(maybeApplyUnderOver(node))); } /** * @param {string} op * @param {BinaryOperation} left * @param {Node} right * @returns {BinaryOperation | TernaryOperation} */ function maybeTernary(op, left, right) { if (left.name === "sub" && op === "sup") { const [base, sub] = left.items; return { type: "TernaryOperation", name: "subsup", items: [base, sub, right], }; } if (left.name === "sup" && op === "sub") { const [base, sup] = left.items; return { type: "TernaryOperation", name: "subsup", items: [base, right, sup], }; } if (left.name === "under" && (op === "over" || op === "sup")) { const [base, under] = left.items; return { type: "TernaryOperation", name: "underover", items: [base, under, right], }; } if (left.name === "over" && (op === "under" || op === "sub")) { const [base, over] = left.items; return { type: "TernaryOperation", name: "underover", items: [base, right, over], }; } const node = post({ type: "BinaryOperation", name: op, items: [left, right], }); return rightAssociate(node.name, node.items); } /** * @param {string} op * @param {[Node, Node]} operands * @returns {BinaryOperation} */ function rightAssociate(op, [left, right]) { if (left.type !== "BinaryOperation" || op === "frac") { return { type: "BinaryOperation", name: op, items: [left, right], }; } const [a, b] = left.items; return { type: "BinaryOperation", name: left.name, items: [a, rightAssociate(op, [b, right])], }; } /** * @param {Node[]} nodes * @returns {boolean} */ function isPipeDelimited(nodes) { if (nodes.length < 3) { return false; } const open = nodes.at(0); const close = nodes.at(-1); return ( open?.type === "OperatorLiteral" && close?.type === "OperatorLiteral" && (open.value === "|" || open.value === "∥" || open.value === "‖") && open.value === close.value ); } /** * @param {State} state * @returns {{ node: BinaryOperation | TernaryOperation; end: number }} */ export default function infix(state) { const { tokens, start, stack } = state; const nestLevel = state.nestLevel + 1; const token = tokens[start]; /** @type {Node | undefined} */ let left; if (isPipeDelimited(stack)) { maybeFixDoublePipe(stack); left = { type: "Term", items: [...stack], }; stack.splice(0, stack.length); } else { left = stack.pop(); if (left?.type === "SpaceLiteral") { left = stack.pop(); } } if (!left) { left = empty(); } const nextToken = tokens[start + 1]; let next; if (nextToken && SHOULD_STOP.includes(nextToken.type)) { const handleRight = handlers.get(nextToken.type); if (!handleRight) { throw new Error("Unknown handler"); } next = handleRight({ ...state, stack: [], start: start + 1, nestLevel, }); } else { next = expr({ ...state, stack: [], start: start + 1, nestLevel, }); } if (next && next.node && next.node.type === "SpaceLiteral") { next = expr({ ...state, stack: [], start: next.end, nestLevel, }); } const { end, node: right } = next; if (left.type === "BinaryOperation") { return { end, node: post(maybeTernary(token.value, left, right)), }; } return { end, node: post({ type: "BinaryOperation", name: token.value, items: [left, right], }), }; }