mathup
Version:
Easy MathML authoring tool with a quick to write syntax
175 lines (148 loc) • 3.69 kB
JavaScript
import { isDoublePipeOperator, isPipeOperator } from "../utils.js";
import expr from "./expr.js";
/**
* @typedef {import("../../tokenizer/index.js").Token} Token
* @typedef {import("../index.js").Node} Node
* @typedef {import("../index.js").UnaryOperation} UnaryOperation
* @typedef {import("../index.js").BinaryOperation} BinaryOperation
* @typedef {import("../index.js").TernaryOperation} TernaryOperation
* @typedef {UnaryOperation | BinaryOperation | TernaryOperation} Operation
* @typedef {import("../index.js").Term} Term
* @typedef {import("../parse.js").State} State
*/
/**
* @param {Node} node
* @param {string[]} transforms
* @returns {Operation | Term}
*/
function insertTransformNode(node, transforms) {
if (node.type === "Term" && node.items.length > 0) {
// Only apply transform to first node.
const [first, ...rest] = node.items;
return {
...node,
items: [insertTransformNode(first, transforms), ...rest],
};
}
if (node.type === "BinaryOperation") {
const [left, right] = node.items;
return {
...node,
items: [insertTransformNode(left, transforms), right],
};
}
if (node.type === "TernaryOperation") {
const [a, b, c] = node.items;
return {
...node,
items: [insertTransformNode(a, transforms), b, c],
};
}
return {
type: "UnaryOperation",
name: "command",
transforms,
items: [node],
};
}
/**
* @param {State} state
* @returns {((token: Token) => boolean) | undefined}
*/
function maybeStopAtPipe({ start, tokens, stack, stopAt }) {
if (stopAt) {
return stopAt;
}
if (stack.length !== 1) {
return undefined;
}
const lastToken = start > 0 ? tokens[start - 1] : undefined;
if (!lastToken || lastToken.type !== "operator") {
return undefined;
}
if (lastToken.value === "|") {
return isPipeOperator;
}
if (lastToken.value === "∥") {
return isDoublePipeOperator;
}
return undefined;
}
/**
* @param {State} state
* @returns {{ node: Operation | Term; end: number }}
*/
export default function command(state) {
const token = state.tokens[state.start];
if (!token.name) {
throw new Error("Got command token without a name");
}
/** @type {string[]} */
const textTransforms = [];
/** @type {Map<string, string>} */
const styles = new Map();
/**
* @param {Token} token
* @returns {void}
*/
function handleCommandToken({ name, value }) {
if (!value) {
return;
}
if (name === "text-transform") {
textTransforms.push(value);
} else if (name) {
styles.set(name, value);
}
}
const stopAt = maybeStopAtPipe(state);
handleCommandToken(token);
let pos = state.start + 1;
let nextToken = state.tokens[pos];
while (
nextToken &&
(nextToken.type === "command" || nextToken.type === "space")
) {
if (nextToken.type === "command") {
handleCommandToken(nextToken);
}
pos += 1;
nextToken = state.tokens[pos];
}
const next = expr({
...state,
stack: [],
start: pos,
nestLevel: state.nestLevel + 1,
textTransforms,
stopAt,
});
if (textTransforms.length === 0) {
// Only apply styles.
return {
node: {
type: "UnaryOperation",
name: "command",
styles,
items: [next.node],
},
end: next.end,
};
}
const node = insertTransformNode(next.node, textTransforms);
if (styles.size > 0) {
return {
node: {
type: "UnaryOperation",
name: "command",
styles,
items: [node],
},
end: next.end,
};
}
return {
node,
end: next.end,
};
}