mathup
Version:
Easy MathML authoring tool with a quick to write syntax
2,241 lines (2,152 loc) • 93.8 kB
JavaScript
/*! mathup v1.0.0 | (c) 2015-2025 (undefined) | undefined */
var mathup = (function () {
'use strict';
/**
* @typedef {import("../tokenizer/index.js").Token} Token
* @typedef {import("./index.js").Node} Node
*/
/**
* @param {Token} token
* @returns {boolean}
*/
function isPipeOperator(token) {
return token.type === "operator" && token.value === "|";
}
/**
* @param {Token} token
* @returns {boolean}
*/
function isDoublePipeOperator(token) {
return token.type === "operator" && token.value === "∥";
}
/**
* Double pipe defaults to the parallel-to character which is behaves
* wrong when used as a fence.
* @param {Node[]} items
* @returns {void}
*/
function maybeFixDoublePipe(items) {
if (items.length < 2) {
return;
}
const first = items.at(0);
if (first?.type === "OperatorLiteral" && first.value === "∥") {
first.value = "‖";
}
const last = items.at(-1);
if (last?.type === "OperatorLiteral" && last.value === "∥") {
last.value = "‖";
}
}
/**
* @param {Node} node
* @returns {void}
*/
function addZeroLSpaceToOperator(node) {
let first = node;
while (first && (first.type === "Term" || first.type === "UnaryOperation" || first.type === "BinaryOperation" || first.type === "TernaryOperation")) {
[first] = first.items;
}
if (!first) {
return;
}
if (first.type === "OperatorLiteral") {
if (!first.attrs) {
first.attrs = {};
}
if (typeof first.attrs.lspace === "undefined") {
first.attrs.lspace = 0;
}
}
}
/**
* @typedef {import("../../tokenizer/index.js").Token} Token
* @typedef {import("../index.js").Node} Node
* @typedef {import("../index.js").Term} Term
* @typedef {import("../index.js").MultiScripts} MultiScripts
*/
/** @returns {Term} */
function empty$1() {
return {
type: "Term",
items: []
};
}
/**
* @param {Token} token
* @returns {boolean}
*/
function isIndexBreak(token) {
if (token.type === "sep.row" || token.type === "sep.col") {
return true;
}
if (token.type !== "infix") {
return false;
}
return token.value === "sub" || token.value === "sup";
}
/**
* @param {Node[] | null} nodes
* @returns {Node[]}
*/
function prepareScript(nodes) {
if (!nodes) {
return [];
}
if (nodes.at(-1)?.type === "SpaceLiteral") {
// ignore trailing space
nodes.pop();
}
if (nodes.length !== 1) {
return nodes;
}
const [node] = nodes;
if (node.type !== "FencedGroup" || node.items.length !== 1) {
return nodes;
}
const [cell] = node.items;
if (cell.length === 1) {
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 nodes;
}
}
return [{
type: "Term",
items: cell
}];
}
/**
* Parse the series of sub- and sup indices of the multiscript. Note
* we assume the first two tokens have already been checked.
*
* @param {import("../parse.js").State} state
* @returns {{ scripts: [Node[], Node[]][], end: number }}
*/
function parseScripts(state) {
let i = state.start + 1;
let token = state.tokens.at(i);
/** @type {[Node[], Node[]][]} */
const scripts = [];
/** @type {Node[] | null} */
let sub = null;
/** @type {Node[] | null} */
let sup = null;
/**
* @returns {void}
*/
function commit() {
if (sub && sub.length > 0 || sup && sup.length > 0) {
scripts.push([prepareScript(sub), prepareScript(sup)]);
}
sub = null;
sup = null;
}
// Remember previous position to allow repeat positions.
let position = token?.value ?? "sub";
while (token && isIndexBreak(token)) {
if (token.type === "infix") {
// Update current position
position = token.value;
}
i += 1;
token = state.tokens[i];
if (token && token.type === "space") {
i += 1;
token = state.tokens[i];
}
/** @type {Node[]} */
const items = [];
while (token && token.type !== "paren.close" && !isIndexBreak(token)) {
const next = expr({
...state,
start: i,
stack: [],
nestLevel: state.nestLevel + 1,
stopAt(other) {
return other.type === "infix" && (other.value === "sub" || other.value === "sup");
}
});
items.push(next.node);
i = next.end;
token = state.tokens[i];
}
if (position === "sup") {
if (sup) {
commit();
}
sup = items;
} else {
if (sub) {
commit();
}
sub = items;
}
}
if (sub || sup) {
commit();
}
if (token?.type === "paren.close") {
i += 1;
}
return {
scripts,
end: i
};
}
/**
* Parse a multiscript. Note that we assume the first two tokens been
* checked.
*
* @param {import("../parse.js").State} state
* @returns {{ node: MultiScripts, end: number }}
*/
function multiscripts$1(state) {
let {
scripts,
end: i
} = parseScripts(state);
let token = state.tokens.at(i);
/** @type {Node | undefined} */
let base;
/** @type {[Node[], Node[]][] | undefined} */
let prescripts;
if (!token || token.type === "space") {
// There is nothing after the already parsed scripts. Apply as postscripts.
base = state.stack.pop();
if (base?.type === "SpaceLiteral") {
base = empty$1();
}
} else {
// existing scripts are prescripts. See if there are postscripts.
prescripts = scripts;
scripts = [];
const next = expr({
...state,
start: i,
stack: [],
nestLevel: state.nestLevel + 1,
stopAt(other) {
return other.type === "paren.open";
}
});
base = next.node;
i = next.end;
token = state.tokens[i];
if (token?.type === "paren.open") {
const nextToken = state.tokens.at(i + 1);
if (nextToken?.type === "infix" && (nextToken.value === "sub" || nextToken.value === "sup")) {
({
scripts,
end: i
} = parseScripts({
...state,
start: i
}));
}
}
}
/** @type {MultiScripts} */
const node = {
type: "MultiScripts",
base: base ?? empty$1(),
post: scripts
};
if (prescripts) {
node.pre = prescripts;
}
return {
node,
end: i
};
}
/**
* @typedef {import("../../tokenizer/index.js").Token} Token
* @typedef {import("../index.js").Node} Node
* @typedef {import("../index.js").FencedGroup} FencedGroup
* @typedef {import("../index.js").MatrixGroup} MatrixGroup
* @typedef {import("../index.js").MultiScripts} MultiScripts
* @typedef {import("../index.js").LiteralAttrs} LiteralAttrs
*/
/**
* @param {Token} token
* @returns {Omit<Token, "type">}
*/
function omitType(token) {
const {
type: _type,
...rest
} = token;
return rest;
}
/**
* @param {import("../parse.js").State} state
* @returns {{ node: FencedGroup | MatrixGroup | MultiScripts, end: number }}
*/
function group(state) {
let i = state.start;
let token = state.tokens[i];
const open = token;
/** @type {{ value: string, attrs?: LiteralAttrs }[]} */
const seps = [];
/** @type {Node[]} */
let cell = [];
/** @type {Node[][]} */
let cols = [];
/** @type {Node[][][]} */
const rows = [];
i += 1;
token = state.tokens[i];
if (token && token.type === "space") {
// Ignore leading space.
i += 1;
token = state.tokens[i];
}
if (token && token.type === "infix" && (token.value === "sub" || token.value === "sup")) {
return multiscripts$1({
...state,
start: i - 1
});
}
while (token && token.type !== "paren.close") {
if (token.type === "space" && token.value === " ") {
// No need to add tokens which don’t render elements to our
// cell.
i += 1;
token = state.tokens[i];
continue;
}
if (token.type === "sep.col") {
/** @type {{ value: string, attrs?: LiteralAttrs }} */
const sepToken = {
value: token.value
};
if (token.attrs) {
sepToken.attrs = token.attrs;
}
seps.push(sepToken);
cols.push(cell);
cell = [];
i += 1;
token = state.tokens[i];
// Ignore leading space.
if (token && token.type === "space") {
i += 1;
token = state.tokens[i];
}
continue;
}
if (token.type === "sep.row") {
cols.push(cell);
rows.push(cols);
cell = [];
cols = [];
i += 1;
token = state.tokens[i];
// Ignore leading space.
if (token && token.type === "space") {
i += 1;
token = state.tokens[i];
}
continue;
}
if (cell.length === 1) {
// If first element is an operator it may throw alignment out
// with its implicit lspace.
addZeroLSpaceToOperator(cell[0]);
}
const next = expr({
...state,
start: i,
stack: cell,
nestLevel: state.nestLevel + 1
});
cell.push(next.node);
i = next.end;
token = state.tokens[i];
}
if (cell.length > 0) {
cols.push(cell);
}
const end = i + 1;
const close = token && token.type === "paren.close" ? token : null;
const attrs = {
open: omitType(open),
close: close ? omitType(close) : null,
seps
};
if (attrs.close?.value === "|" && !open.value) {
// Add a small space before the "evaluate at" operator
if (!attrs.close.attrs) {
attrs.close.attrs = {};
}
attrs.close.attrs.lspace = "0.35ex";
}
if (rows.length === 0) {
return {
node: {
type: "FencedGroup",
items: cols,
attrs
},
end
};
}
const rowItems = rows;
if (cols.length > 0) {
rowItems.push(cols);
}
return {
node: {
type: "MatrixGroup",
items: rowItems,
attrs
},
end
};
}
/**
* @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$1({
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 }}
*/
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$1(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
};
}
/**
* @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("../parse.js").State} State
*/
/**
* @param {Node[]} nodes
* @returns {Node}
*/
function toTermOrUnwrap(nodes) {
if (nodes.length === 1) {
return nodes[0];
}
return {
type: "Term",
items: nodes
};
}
/**
* @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 token = tokens[start];
if (!token || token.arity && token.arity !== 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 {import("../parse.js").State} state
* @returns {{ node: UnaryOperation | BinaryOperation; end: number }}
*/
function prefix(state) {
const {
tokens,
start
} = state;
const token = tokens[start];
const nestLevel = state.nestLevel + 1;
if (!token.name) {
throw new Error("Got prefix token without a name");
}
const stopAt = maybeStopAtPipe(state);
let next = expr({
...state,
stack: [],
start: start + 1,
nestLevel,
stopAt
});
if (next && next.node && next.node.type === "SpaceLiteral") {
next = expr({
...state,
stack: [],
start: next.end,
nestLevel,
stopAt
});
}
// XXX: Arity > 2 not implemented.
if (token.arity === 2) {
if (next && next.node && next.node.type === "FencedGroup" && next.node.items.length === 2) {
const [first, second] = next.node.items;
/** @type {[Node, Node]} */
const items = token.name === "root" ? [toTermOrUnwrap(second), toTermOrUnwrap(first)] : [toTermOrUnwrap(first), toTermOrUnwrap(second)];
return {
node: {
type: "BinaryOperation",
name: token.name,
attrs: token.attrs,
items
},
end: next.end
};
}
const first = next;
let second = next && expr({
...state,
stack: [],
start: next.end,
nestLevel
});
if (second && second.node && second.node.type === "SpaceLiteral") {
second = expr({
...state,
stack: [],
start: second.end,
nestLevel
});
}
/** @type {BinaryOperation} */
const node = {
type: "BinaryOperation",
name: token.name,
items: [first.node, second.node]
};
if (token.name === "root") {
node.items = [second.node, first.node];
}
if (token.attrs) {
node.attrs = token.attrs;
}
return {
node,
end: second.end
};
}
/** @type {UnaryOperation} */
const node = {
type: "UnaryOperation",
name: token.name,
items: [next.node]
};
if (token.accent) {
node.accent = token.accent;
}
if (token.attrs) {
node.attrs = token.attrs;
}
if (next && next.node && next.node.type === "FencedGroup" && next.node.items.length === 1) {
// The operand is not a matrix.
node.items = [toTermOrUnwrap(next.node.items[0])];
}
return {
node,
end: next.end
};
}
/**
* @typedef {import("../parse.js").State} State
* @typedef {import("../index.js").SpaceLiteral} SpaceLiteral
*/
/**
* @param {number} n - Number of space literals
* @returns {number} - The width in units of ex
*/
function spaceWidth(n) {
if (n <= 0) {
return 0;
}
if (n <= 3) {
return 0.35 * (n - 1);
}
if (n <= 5) {
return 0.5 * (n - 1);
}
return n - 3;
}
/**
* @param {State} state
* @returns {{ node: SpaceLiteral, end: number }}
*/
function space(state) {
const token = state.tokens[state.start];
const lineBreak = token.value.startsWith("\n");
const width = lineBreak ? 0 : token.value.length;
return {
node: {
type: "SpaceLiteral",
attrs: {
width: `${spaceWidth(width)}ex`
}
},
end: state.start + 1
};
}
/**
* @typedef {import("../../tokenizer/index.js").TokenType} TokenType
* @typedef {import("../parse.js").State} State
* @typedef {import("../index.js").Node} Node
* @typedef {import("../index.js").Literal} Literal
* @typedef {(state: State) => { node: Node, end: number }} Handler
* @typedef {"Ident" | "Number" | "Operator" | "Text"} LiteralType
*/
/**
* @param {LiteralType} type
* @returns {Handler}
*/
const literal$1 = type => ({
start,
tokens
}) => {
const {
value,
attrs
} = tokens[start];
/** @type {Literal} */
const node = {
type: `${type}Literal`,
value
};
if (attrs) {
node.attrs = attrs;
}
return {
node,
end: start + 1
};
};
/** @type {[TokenType, Handler][]} */
const handlers = [["command", command], ["ident", literal$1("Ident")], ["number", literal$1("Number")], ["operator", literal$1("Operator")], ["text", literal$1("Text")], ["infix", infix], ["paren.open", group], ["prefix", prefix], ["space", space]];
var handlers$1 = new Map(handlers);
/**
* @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 }}
*/
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$1.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]
})
};
}
/**
* @typedef {import("../index.js").IdentLiteral} IdentLiteral
* @typedef {import("../index.js").Literal} Literal
* @typedef {import("../index.js").LiteralAttrs} LiteralAttrs
* @typedef {import("../index.js").Node} Node
* @typedef {import("../index.js").OperatorLiteral} OperatorLiteral
* @typedef {import("../index.js").Term} Term
* @typedef {import("../index.js").UnaryOperation} UnaryOperation
* @typedef {import("../parse.js").State} State
*/
const KEEP_GOING_TYPES = ["command", "ident", "infix", "number", "operator", "paren.open", "prefix", "text"];
/**
* @param {Node[]} items
* @param {string[]} [textTransforms]
* @returns {void}
*/
function maybeFixDifferential(items, textTransforms) {
// We may want to make the differnetial d operator an actual
// operator to fix some spacing during integration.
if (items.length < 2) {
return;
}
const [first, second] = items;
if (first.type !== "IdentLiteral" || first.value !== "d") {
return;
}
let operand = second;
while (operand.type === "UnaryOperation" || operand.type === "BinaryOperation" || operand.type === "TernaryOperation") {
[operand] = operand.items;
}
if (operand.type !== "IdentLiteral") {
return;
}
const value = (textTransforms?.length ?? 0) > 0 ? first.value : "𝑑";
/** @type {OperatorLiteral & { attrs: LiteralAttrs }} */
const node = {
...items[0],
type: "OperatorLiteral",
value,
attrs: {
...(first.attrs ?? {}),
rspace: "0"
}
};
items[0] = node;
}
/**
* @param {State} state
* @returns {{ node: Term; end: number }}
*/
function term$1(state) {
let i = state.start;
let token = state.tokens[i];
/** @type {Node[]} */
const items = [];
while (token && KEEP_GOING_TYPES.includes(token.type) &&
// Perhaps the parent handler wants to use this token.
!state.stopAt?.(token)) {
const handler = handlers$1.get(token.type);
if (!handler) {
throw new Error("Unknown Hander");
}
const next = handler({
...state,
start: i,
stack: items
});
items.push(next.node);
i = next.end;
token = state.tokens[i];
}
maybeFixDifferential(items, state.textTransforms);
maybeFixDoublePipe(items);
return {
node: {
type: "Term",
items
},
end: i
};
}
/** @typedef {import("../index.js").Node} Node */
/**
* @param {import("../parse.js").State} state
* @returns {{ node: Node; end: number }}
*/
function expr(state) {
if (state.start >= state.tokens.length) {
return {
node: {
type: "Term",
items: []
},
end: state.start
};
}
const {
type
} = state.tokens[state.start];
if (type === "paren.open") {
return group(state);
}
if (type === "space") {
return space(state);
}
if (type === "infix") {
return infix(state);
}
if (type === "prefix") {
return prefix(state);
}
return term$1(state);
}
/**
* @typedef {import("../tokenizer/index.js").Token} Token
* @typedef {import("./index.js").Node} Node
* @typedef {import("./index.js").Sentence} Sentence
*
* @typedef {object} State
* @property {Token[]} tokens
* @property {number} start
* @property {Node[]} stack
* @property {number} nestLevel
* @property {(token: Token) => boolean} [stopAt]
* @property {string[]} [textTransforms]
*
* @param {Token[]} tokens
* @returns {Sentence}
*/
function parse(tokens) {
const body = [];
let pos = 0;
while (pos < tokens.length) {
const state = {
tokens,
start: pos,
stack: body,
nestLevel: 1
};
const next = expr(state);
pos = next.end;
body.push(next.node);
}
return {
type: "Sentence",
body
};
}
/* eslint-env browser */
const NS = "http://www.w3.org/1998/Math/MathML";
/**
* @typedef {Required<import("./index.js").RenderOptions>} Options
* @param {import("../transformer/index.js").Tag} node
* @param {Options} options
* @returns {Element | DocumentFragment}
*/
function toDOM(node, {
bare
}) {
/** @type {Element | DocumentFragment} */
let element;
if (node.tag === "math" && bare) {
element = document.createDocumentFragment();
} else {
element = document.createElementNS(NS, node.tag);
}
if (element instanceof Element && node.attrs) {
for (const [name, value] of Object.entries(node.attrs)) {
element.setAttribute(name, `${value}`);
}
}
if (node.textContent) {
element.textContent = node.textContent;
}
if (node.childNodes) {
for (const childNode of node.childNodes) {
if (childNode) {
element.appendChild(toDOM(childNode, {
bare: false
}));
}
}
}
return element;
}
/**
* @param {string} str
* @returns {string}
*/
function escapeTextContent(str) {
return str.replace(/[&<]/g, c => {
if (c === "&") {
return "&";
}
return "<";
});
}
/**
* @param {string} str
* @returns {string}
*/
function escapeAttrValue(str) {
return str.replace(/"/g, """);
}
/**
* @param {import("../transformer/index.js").Tag} node
* @param {Required<import("./index.js").RenderOptions>} options
* @returns {string}
*/
function toString(node, {
bare
}) {
const attrString = Object.entries(node.attrs || {}).map(([name, value]) => `${name}="${escapeAttrValue(`${value}`)}"`).join(" ");
const openContent = attrString ? `${node.tag} ${attrString}` : node.tag;
if (node.textContent) {
const textContent = escapeTextContent(node.textContent);
return `<${openContent}>${textContent}</${node.tag}>`;
}
if (node.childNodes) {
const content = node.childNodes.map(child => child ? toString(child, {
bare: false
}) : "").join("");
if (node.tag === "math" && bare) {
return content;
}
return `<${openContent}>${content}</${node.tag}>`;
}
return `<${openContent} />`;
}
/**
* @yields {never}
*/
function* nullIter() {}
/**
* @template {unknown[]} T - Tuple type with item type of each input iterator
* @param {{ [K in keyof T]: Iterable<T[K]> }} iterables - The iterators to be
* zipped
* @yields {T}
*/
function* zip(iterables) {
const iterators = iterables.map(iterable => iterable ? iterable[Symbol.iterator]() : nullIter());
while (true) {
const next = iterators.map(iterator => iterator.next());
if (next.every(({
done
}) => done)) {
return;
}
yield (/** @type {T} */next.map(({
value
}) => value));
}
}
/**
* @typedef {import("../transformer/index.js").Tag} Tag
*
* @param {Element} parent
* @param {Tag} node
* @param {Required<import("./index.js").RenderOptions>} options
* @returns {void}
*/
function updateDOM(parent, node, options) {
if (!parent) {
throw new Error("updateDOM called on null");
}
if (parent.tagName.toLowerCase() !== node.tag) {
throw new Error("tag name mismatch");
}
if (!(node.tag === "math" && options.bare)) {
const desiredAttrs = node.attrs || {};
const removeAttrs = [];
for (const attr of parent.attributes) {
const newValue = desiredAttrs[attr.name];
if (!newValue) {
removeAttrs.push(attr.name);
} else if (newValue !== attr.value) {
parent.setAttribute(attr.name, `${newValue}`);
}
}
for (const name of removeAttrs) {
parent.removeAttribute(name);
}
for (const [name, value] of Object.entries(desiredAttrs)) {
if (!parent.getAttribute(name)) {
parent.setAttribute(name, `${value}`);
}
}
}
if (["mi", "mn", "mo", "mspace", "mtext"].includes(node.tag)) {
if (parent.textContent !== node.textContent) {
parent.textContent = node.textContent ?? "";
}
return;
}
// Collect in arrays to prevent the live updating from interfering
// with the schedule.
const appendChilds = [];
const removeChilds = [];
const replaceChilds = [];
for (const [child, desired] of zip(/** @type {[HTMLCollection, (Tag | null)[]]} */[parent.children, node.childNodes])) {
if (!child && !desired) {
continue;
}
if (!desired) {
// parent.removeChild(child);
removeChilds.push(child);
} else if (!child) {
// parent.appendChild(toDOM(desired, options));
appendChilds.push(toDOM(desired, options));
} else if (child.tagName.toLowerCase() !== desired.tag) {
// parent.replaceChild(toDOM(desired, options), child);
replaceChilds.push([child, toDOM(desired, options)]);
} else {
updateDOM(child, desired, {
bare: false
});
}
}
for (const child of removeChilds) {
parent.removeChild(child);
}
for (const child of appendChilds) {
parent.appendChild(child);
}
for (const [oldChild, desired] of replaceChilds) {
parent.replaceChild(desired, oldChild);
}
}
/**
* @typedef {import("./index.js").Token} Token
* @typedef {(char: string) => boolean} LeximeTest
*/
const LETTER_RE = /^\p{L}/u;
/** @type {LeximeTest} */
function isAlphabetic(char) {
if (!char) {
return false;
}
return LETTER_RE.test(char);
}
const LETTER_NUMBER_RE = /^[\p{L}\p{N}]/u;
/** @type {LeximeTest} */
function isAlphanumeric(char) {
if (!char) {
return false;
}
return LETTER_NUMBER_RE.test(char);
}
const MARK_RE = /^\p{M}/u;
/** @type {LeximeTest} */
function isMark(char) {
if (!char) {
return false;
}
return MARK_RE.test(char);
}
// Duodecimal literals are in the So category.
const NUMBER_RE = /^[\p{N}\u{218a}-\u{218b}]/u;
/** @type {LeximeTest} */
function isNumeric(char) {
if (!char) {
return false;
}
return NUMBER_RE.test(char);
}
// Invisible opperators are in the Cf category.
const OPERATOR_RE = /^[\p{P}\p{Sm}\p{So}\u{2061}-\u{2064}]/u;
/** @type {LeximeTest} */
function isOperational(char) {
if (!char) {
return false;
}
return OPERATOR_RE.test(char);
}
const PUNCT_OPEN_RE = /^\p{Pe}/u;
/** @type {LeximeTest} */
function isPunctClose(char) {
if (!char) {
return false;
}
return PUNCT_OPEN_RE.test(char);
}
const PUNCT_CLOSE_RE = /^\p{Ps}/u;
/** @type {LeximeTest} */
function isPunctOpen(char) {
if (!char) {
return false;
}
return PUNCT_CLOSE_RE.test(char);
}
const FUNCTION_IDENT_ATTRS = {
class: "mathup-function-ident"
};
const KNOWN_IDENTS = new Map([["CC", {
value: "ℂ"
}], ["Delta", {
value: "Δ",
attrs: {
mathvariant: "normal"
}
}], ["Gamma", {
value: "Γ",
attrs: {
mathvariant: "normal"
}
}], ["Lambda", {
value: "Λ",
attrs: {
mathvariant: "normal"
}
}], ["NN", {
value: "ℕ"
}], ["O/", {
value: "∅"
}], ["Omega", {
value: "Ω",
attrs: {
mathvariant: "normal"
}
}], ["Phi", {
value: "Φ",
attrs: {
mathvariant: "normal"
}
}], ["Pi", {
value: "Π",
attrs: {
mathvariant: "normal"
}
}], ["Psi", {
value: "Ψ",
attrs: {
mathvariant: "normal"
}
}], ["QQ", {
value: "ℚ"
}], ["RR", {
value: "ℝ"
}], ["Sigma", {
value: "Σ",
attrs: {
mathvariant: "normal"
}
}], ["Theta", {
value: "Θ",
attrs: {
mathvariant: "normal"
}
}], ["Xi", {
value: "Ξ",
attrs: {
mathvariant: "normal"
}
}], ["ZZ", {
value: "ℤ"
}], ["alpha", {
value: "α"
}], ["beta", {
value: "β"
}], ["chi", {
value: "χ"
}], ["cos", {
value: "cos",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["cosh", {
value: "cosh",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["cot", {
value: "cot",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["csc", {
value: "csc",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["cosec", {
value: "cosec",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["delta", {
value: "δ"
}], ["det", {
value: "det",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["dim", {
value: "dim",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["epsilon", {
value: "ɛ"
}], ["eta", {
value: "η"
}], ["gamma", {
value: "γ"
}], ["gcd", {
value: "gcd",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["iota", {
value: "ι"
}], ["kappa", {
value: "κ"
}], ["lambda", {
value: "λ"
}], ["lcm", {
value: "lcm",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["ln", {
value: "ln",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["log", {
value: "log",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["max", {
value: "max",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["min", {
value: "min",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["mu", {
value: "μ"
}], ["nu", {
value: "ν"
}], ["omega", {
value: "ω"
}], ["oo", {
value: "∞"
}], ["phi", {
value: "φ"
}], ["phiv", {
value: "ϕ"
}], ["pi", {
value: "π"
}], ["psi", {
value: "ψ"
}], ["rho", {
value: "ρ"
}], ["sec", {
value: "sec",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["sigma", {
value: "σ"
}], ["sin", {
value: "sin",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["sinh", {
value: "sinh",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["tan", {
value: "tan",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["tanh", {
value: "tanh",
attrs: {
...FUNCTION_IDENT_ATTRS
}
}], ["tau", {
value: "τ"
}], ["theta", {
value: "θ"
}], ["upsilon", {
value: "υ"
}], ["xi", {
value: "ξ"
}], ["zeta", {
value: "ζ"
}]]);
const KNOWN_OPS = new Map([["-", {
value: "−"
}], ["!=", {
value: "≠"
}], ["!==", {
value: "≢"
}], ["!in", {
value: "∉"
}], [".$", {
value: "\u2061",
attrs: {
class: "mathup-function-application"
}
}], [".*", {
value: "\u2062",
attrs: {
class: "mathup-invisible-times"
}
}], [".+", {
value: "\u2064",
attrs: {
class: "mathup-invisible-add"
}
}], [".,", {
value: "\u2063",
attrs: {
class: "mathup-invisible-separator"
}
}], ["'", {
value: "′",
attrs: {
lspace: 0,
rspace: 0
}
}], ["''", {
value: "″",
attrs: {
lspace: 0,
rspace: 0
}
}], ["'''", {
value: "‴",
attrs: {
lspace: 0,
rspace: 0
}
}], ["''''", {
value: "⁗",
attrs: {
lspace: 0,
rspace: 0
}
}], ["*", {
value: "·"
}], ["**", {
value: "∗"
}], ["***", {
value: "⋆"
}], ["+-", {
value: "±"
}], ["-+", {
value: "∓"
}], ["-:", {
value: "÷"
}], ["-<", {
value: "≺"
}], ["-<=", {
value: "⪯"
}], ["-=", {
value: "≡"
}], ["->", {
value: "→"
}], ["->>", {
value: "↠"
}], ["...", {
value: "…"
}], ["//", {
value: "⁄"
}], ["/_", {
value: "∠"
}], ["/_\\", {
value: "△"
}], [":.", {
value: "∴"
}], [":|:", {
value: "|",
attrs: {
stretchy: true
},
sep: true
}], ["<-", {
value: "←"
}], ["<<<", {
value: "≪"
}], ["<=", {
value: "≤"
}], ["<=>", {
value: "⇔"
}], ["<>", {
value: "⋄"
}], ["<|", {
value: "⊲"
}], ["==", {
value: "≡"
}], ["=>", {
value: "⇒"
}], [">-", {
value: "≻"
}], [">-=", {
value: "⪰"
}], [">->", {
value: "↣"
}], [">->>", {
value: "⤖"
}], ["><|", {
value: "⋊"
}], [">=", {
value: "≥"
}], [">>>", {
value: "≫"
}], ["@", {
value: "∘"
}], ["AA", {
value: "∀"
}], ["EE", {
value: "∃"
}], ["TT", {
value: "⊤"
}], ["[]", {
value: "□"
}], ["^^", {
value: "∧"
}], ["^^^", {
value: "⋀"
}], ["_|_", {
value: "⊥"
}], ["aleph", {
value: "ℵ"
}], ["and", {
value: "and"
}], ["cdots", {
value: "⋯"
}], ["darr", {
value: "↓"
}], ["ddots", {
value: "⋱"
}], ["del", {
value: "∂"
}], ["diamond", {
value: "⋄"
}], ["dint", {
value: "∬"
}], ["grad", {
value: "∇"
}], ["hArr", {
value: "⇔"
}], ["harr", {
value: "↔"
}], ["if", {
value: "if"
}], ["iff", {
value: "⇔"
}], ["in", {
value: "∈"
}], ["int", {
value: "∫"
}], ["lArr", {
value: "⇐"
}], ["larr", {
value: "←"
}], ["lim", {
value: "lim"
}], ["mod", {
value: "mod"
}], ["nn", {
value: "∩"
}], ["nnn", {
value: "⋂"
}], ["not", {
value: "¬"
}], ["o+", {
value: "⊕"
}], ["o.", {
value: "⊙"
}], ["oc", {
value: "∝"
}], ["oint", {
value: "∮"
}], ["or", {
value: "or"
}], ["otherwise", {
value: "otherwise"
}], ["ox", {
value: "⊗"
}], ["prod", {
value: "∏"
}], ["prop", {
value: "∝"
}], ["rArr", {
value: "⇒"
}], ["rarr", {
value: "→"
}], ["square", {
value: "□"
}], ["sub", {
value: "⊂"
}], ["sube", {
value: "⊆"
}], ["sum", {
value: "∑"
}], ["sup", {
value: "⊃"
}], ["supe", {
value: "⊇"
}], ["uarr", {
value: "↑"
}], ["uu", {
value: "∪"
}], ["uuu", {
value: "⋃"
}], ["vdots", {
value: "⋮"
}], ["vv", {
value: "∨"
}], ["vvv", {
value: "⋁"
}], ["xx", {
value: "×"
}], ["|--", {
value: "⊢"
}], ["|->", {
value: "↦"
}], ["|==", {
value: "⊨"
}], ["|>", {
value: "⊳"
}], ["|><", {
value: "⋉"
}], ["|><|", {
value: "⋈"
}], ["||", {
value: "∥"
}], ["~=", {
value: "≅"
}], ["~~", {
value: "≈"
}]]);
/** @type {Map<string, Omit<Token, "type">>} */
const KNOWN_PARENS_OPEN = new Map([["(:", {
value: "⟨"
}], ["<<", {
value: "⟨"
}], ["{:", {
value: ""
}], ["|(", {
value: "|"
}], ["|:", {
value: "|"
}], ["|__", {
value: "⌊"
}], ["||(", {
value: "‖"
}], ["||:", {
value: "‖"
}], ["|~", {
value: "⌈"
}], ["(mod", {
value: "(",
attrs: {
lspace: "1.65ex"
},
extraTokensAfter: [{
type: "operator",
value: "mod",
attrs: {
lspace: 0
}
}]
}]]);
const KNOWN_PARENS_CLOSE = new Map([[")|", {
value: "|"
}], [")||", {
value: "‖"
}], [":)", {
value: "⟩"
}], [":|", {
value: "|"
}], [":||", {
value: "‖"
}], [":}", {
value: ""
}], [">>", {
value: "⟩"
}], ["__|", {
value: "⌋"
}], ["~|", {
value: "⌉"
}]]);
const KNOWN_PREFIX = new Map([
// Accents
["bar", {
name: "over",
accent: "‾"
}], ["ddot", {
name: "over",
accent: "⋅⋅"
}], ["dot", {
name: "over",
accent: "⋅"
}], ["hat", {
name: "over",
accent: "^"
}], ["obrace", {
name: "over",
accent: "⏞"
}], ["obracket", {
name: "over",
accent: "⎴"
}], ["oparen", {
name: "over",
accent: "⏜"
}], ["oshell", {
name: "over",
accent: "⏠"
}], ["tilde", {
name: "over",
accent: "˜"
}], ["ubrace", {
name: "under",
accent: "⏟"
}], ["ubrace", {
name: "under",
accent: "⏟"
}], ["ubracket", {
name: "under",
accent: "⎵"
}], ["ul", {
name: "under",
accent: "_"
}], ["uparen", {
name: "under",
accent: "⏝"
}], ["ushell", {
name: "under",
accent: "⏡"
}], ["vec", {
name: "over",
accent: "→"
}],
// Groups
["abs", {
name: "fence",
attrs: {
open: "|",
close: "|"
}
}], ["binom", {
name: "frac",
arity: 2,
attrs: {
linethickness: 0,
open: "(",
close: ")"
}
}], ["ceil", {
name: "fence",
attrs: {
open: "⌈",
close: "⌉"
}
}], ["floor", {
name: "fence",
attrs: {
open: "⌊",
close: "⌋"
}
}], ["norm", {
name: "fence",
attrs: {
open: "‖",
close: "‖"
}
}],
// Roots
["root", {
name: "root",
arity: 2
}], ["sqrt", {
name: "sqrt"
}],
// Enclose
["cancel", {
name: "row",
attrs: {
class: "mathup-enclose-cancel"
}
}]]);
const KNOWN_COMMANDS = new Map([
// Fonts
["rm", {
name: "text-transform",
value: "normal"
}], ["bf", {
name: "text-transform",
value: "bold"
}], ["it", {
name: "text-transform",
value: "italic"
}], ["bb", {
name: "text-transform",
value: "double-struck"
}], ["cc", {
name: "text-transform",
value: "script"
}], ["tt", {
name: "text-transform",
value: "monospace"
}], ["fr", {
name: "text-transform",
value: "fraktur"
}], ["sf", {
name: "text-transform",
value: "sans-serif"
}],
// Colors
["black", {
name: "color",
value: "black"
}], ["\u{26ab}", {
name: "color",
value: "black"
}], ["blue", {
name: "color",
value: "blue"
}], ["\u{1f535}", {
name: "color",
value: "blue"
}], ["brown", {
name: "color",
value: "brown"
}], ["\u{1f7e4}", {
name: "color",
value: "brown"
}], ["cyan", {
name: "color",
value: "cyan"
}], ["gray", {
name: "color",
value: "gray"
}], ["green", {
name: "color",
value: "green"
}], ["\u{1f7e2}", {
name: "color",
value: "green"
}], ["lightgray", {
name: "color",
value: "lightgray"
}], ["orange", {
name: "color",
value: "orange"
}], ["\u{1f7e0}", {
name: "color",
value: "orange"
}], ["purple", {
name: "color",
value: "purple"
}], ["\u{1f7e3}", {
name: "color",
value: "purple"
}], ["red", {
name: "color",
value: "red"
}], ["\u{1f534}", {
name: "color",
value: "red"
}], ["white", {
name: "color",
value: "white"
}], ["\u{26aa}", {
name: "color",
value: "white"
}], ["yellow", {
name: "color",
value: "yellow"
}], ["\u{1f7e1}", {
name: "color",
value: "yellow"
}],
// Background Colors
["bg.black", {
name: "background",
value: "black"
}], ["\u{2b1b}", {
name: "background",
value: "black"
}], ["bg.blue", {
name: "background",
value: "blue"
}], ["\u{1f7e6}", {
name: "background",
value: "blue"
}], ["bg.brown", {
name: "background",
value: "brown"
}], ["\u{1f7eb}", {
name: "background",
value: "brown"
}], ["bg.cyan", {
name: "background",
value: "cyan"
}], ["bg.gray", {
name: "background",
value: "gray"
}], ["bg.green", {
name: "background",
value: "green"
}], ["