mathup
Version:
Easy MathML authoring tool with a quick to write syntax
3,873 lines • 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"
}], ["\u{1f7e9}", {
name: "background",
value: "green"
}], ["bg.lightgray", {
name: "background",
value: "lightgray"
}], ["bg.orange", {
name: "background",
value: "orange"
}], ["\u{1f7e7}", {
name: "background",
value: "orange"
}], ["bg.purple", {
name: "background",
value: "purple"
}], ["\u{1f7ea}", {
name: "background",
value: "purple"
}], ["bg.red", {
name: "background",
value: "red"
}], ["\u{1f7e5}", {
name: "background",
value: "red"
}], ["bg.white", {
name: "background",
value: "white"
}], ["\u{2b1c}", {
name: "background",
value: "white"
}], ["bg.yellow", {
name: "background",
value: "yellow"
}], ["\u{1f7e8}", {
name: "background",
value: "yellow"
}]]);
/**
* @param {string} char
* @param {number} pos
* @param {string} input
* @param {{ offset?: number }} options
* @returns {{ value: string; end: number }}
*/
function handleFence(char, pos, input, {
offset = 0
} = {}) {
let len = 0;
while (input[pos + offset + len] === char) {
len += 1;
}
let value = "";
let closeLen = 0;
let i = pos + offset + len;
while (closeLen < len) {
if (i >= input.length) {
break;
}
if (input[i] === char) {
closeLen += 1;
} else {
if (closeLen > 0) {
value += char.repeat(closeLen);
closeLen = 0;
}
value += input[i];
}
i += 1;
}
return {
value: value.trim(),
end: i
};
}
/**
* @param {number} pos
* @param {string} input
* @param {{ offset?: number }} [options]
* @returns {ReturnType<typeof handleFence>}
*/
function handleBacktick(pos, input, options) {
return handleFence("`", pos, input, options);
}
/**
* @param {number} pos
* @param {string} input
* @param {{ offset?: number }} [options]
* @returns {ReturnType<typeof handleFence>}
*/
function handleQuote(pos, input, options) {
return handleFence('"', pos, input, options);
}
/**
* @param {string} prefix
* @param {number} pos
* @param {string} input
* @returns {{ value: string; end: number }}
*/
function handlePrefixed(prefix, pos, input) {
const start = pos + prefix.length;
let value = "";
let [char] = input.slice(start);
if (!char) {
return {
value: prefix,
end: start
};
}
if (isAlphanumeric(char)) {
while (isAlphanumeric(char)) {
value += char;
[char] = input.slice(start + value.length);
}
return {
value,
end: start + value.length
};
}
if (char === "`") {
return handleBacktick(pos, input, {
offset: prefix.length
});
}
return {
value: char,
end: start + char.length
};
}
/**
* @param {string} input
* @param {number} start
* @returns {boolean}
*/
function isKnownParenCloseAt(input, start) {
for (const key of KNOWN_PARENS_CLOSE.keys()) {
if (input.startsWith(key, start)) {
return true;
}
}
return false;
}
/**
* @param {string} input
* @param {number} start
* @param {boolean} isGrouping
* @returns {boolean}
*/
function hasOperand(input, start, isGrouping) {
let pos = start;
let postPrefixChar = input.at(pos);
while (postPrefixChar === " " || postPrefixChar === "\n") {
pos += 1;
postPrefixChar = input.at(pos);
}
if (!postPrefixChar) {
return false;
}
return !(isGrouping && (isPunctClose(postPrefixChar) || isKnownParenCloseAt(input, pos)));
}
/**
* @param {Pick<import("./index.js").Token, "name">} prefix
* @returns {boolean}
*/
function allowedStandalonePrefix(prefix) {
return prefix.name === "root" || prefix.name === "sqrt";
}
/** @type {import("./index.js").Scanner} */
function alphaScanner(char, input, {
start,
grouping
}) {
if (!isAlphabetic(char)) {
return null;
}
let nextChar = char;
let value = "";
while (isAlphabetic(nextChar) || isMark(nextChar) || value.length > 0 && nextChar === ".") {
const i = start + value.length + nextChar.length;
value += nextChar;
[nextChar] = input.slice(i);
}
// alpha may contain a period, but not never end with one.
if (value.endsWith(".")) {
value = value.slice(0, -1);
}
let end = start + value.length;
{
const operator = KNOWN_OPS.get(value);
if (operator) {
return {
type: "operator",
value: operator.value,
attrs: operator.attrs,
end: start + value.length
};
}
}
{
const ident = KNOWN_IDENTS.get(value);
if (ident) {
return {
type: "ident",
value: ident.value,
attrs: ident.attrs,
end: start + value.length
};
}
}
const potential = `${value}${nextChar}`;
const [nextNextChar] = input.slice(start + potential.length);
{
const operator = KNOWN_OPS.get(potential);
if (operator && !isAlphanumeric(nextNextChar)) {
return {
type: "operator",
value: operator.value,
attrs: operator.attrs,
end: start + potential.length
};
}
}
{
const ident = KNOWN_IDENTS.get(potential);
if (ident && !isAlphanumeric(nextNextChar)) {
return {
type: "ident",
value: ident.value,
attrs: ident.attrs,
end: start + potential.length
};
}
}
{
const prefix = KNOWN_PREFIX.get(value);
if (prefix && (allowedStandalonePrefix(prefix) || hasOperand(input, end, grouping))) {
return {
type: "prefix",
value: "",
end,
...prefix
};
}
}
{
const command = KNOWN_COMMANDS.get(value);
if (command && hasOperand(input, end, grouping)) {
return {
type: "command",
end,
...command
};
}
}
if (value.includes(".")) {
// Period not used, lets toss it.
value = value.slice(0, value.indexOf("."));
end = start + value.length;
}
return {
type: "ident",
value,
end,
split: true
};
}
/** @type {import("./index.js").Scanner} */
function backslashScanner(char, input, {
start
}) {
if (char !== "\\") {
return null;
}
return {
type: "operator",
...handlePrefixed("\\", start, input)
};
}
/** @type {import("./index.js").Scanner} */
function backtickScanner(char, input, {
start
}) {
if (char !== "`") {
return null;
}
const {
end,
value
} = handleBacktick(start, input);
return {
type: "ident",
value,
end
};
}
/** @type {import("./index.js").Scanner} */
function groupSepScanner(_char, input, {
grouping,
start
}, {
colSep,
rowSep
}) {
if (!grouping) {
return null;
}
const colSepEnd = start + colSep.length;
const rowSepEnd = start + rowSep.length;
if (input.slice(start, rowSepEnd) === rowSep) {
return {
type: "sep.row",
value: rowSep,
end: rowSepEnd
};
}
if (input.slice(start, colSepEnd) === colSep) {
return {
type: "sep.col",
value: colSep,
end: colSepEnd
};
}
return null;
}
const INFIX_MAP = new Map([["^", "sup"], ["/", "frac"], ["_", "sub"], [".^", "over"], ["._", "under"]]);
/** @type {Set<string>} */
const COLLISIONS = new Set();
for (const map of [KNOWN_COMMANDS, KNOWN_IDENTS, KNOWN_OPS, KNOWN_PARENS_CLOSE, KNOWN_PARENS_OPEN, KNOWN_PREFIX]) {
for (const key of map.keys()) {
if ("^/_".includes(key[0])) {
COLLISIONS.add(key);
}
}
}
/** @type {import("./index.js").Scanner} */
function infixScanner(char, input, {
start
}) {
if (char === ".") {
const next = input[start + 1];
const infix = INFIX_MAP.get(`.${next}`);
if (infix) {
for (const key of COLLISIONS) {
if (key === input.slice(start + 1, start + 1 + key.length)) {
return null;
}
}
return {
type: "infix",
value: infix,
end: start + 2
};
}
}
const infix = INFIX_MAP.get(char);
if (infix) {
for (const key of COLLISIONS) {
if (key === input.slice(start, start + key.length)) {
return null;
}
}
return {
type: "infix",
value: infix,
end: start + 1
};
}
return null;
}
/** @type {import("./index.js").Scanner} */
function newlineScanner(char, input, {
start,
grouping
}) {
if (char !== "\n") {
return null;
}
let i = start;
let nextChar = char;
let value = "";
while (nextChar === "\n") {
value += nextChar;
i += 1;
nextChar = input[i];
}
return {
type: grouping ? "sep.row" : "space",
value,
end: i
};
}
/** @type {import("./index.js").Scanner} */
function numberScanner(char, input, {
start
}, {
decimalMark
}) {
if (!isNumeric(char)) {
return null;
}
let nextChar = char;
let value = "";
while (isNumeric(nextChar)) {
value += nextChar;
[nextChar] = input.slice(start + value.length);
}
if (nextChar === decimalMark) {
[nextChar] = input.slice(start + value.length + decimalMark.length);
if (isNumeric(nextChar)) {
value += decimalMark;
while (isNumeric(nextChar)) {
value += nextChar;
[nextChar] = input.slice(start + value.length);
}
}
}
return {
type: "number",
value,
end: start + value.length
};
}
/** @type {import("./index.js").Scanner} */
function octothorpeScanner(char, input, {
start
}) {
if (char !== "#") {
return null;
}
const {
end,
value
} = handlePrefixed("#", start, input);
return {
type: "number",
value,
end
};
}
/**
* @typedef {import("./index.js").Token} Token
*/
/**
* @param {string} partial
* @returns {boolean}
*/
function opsPotential(partial) {
for (const [op] of KNOWN_OPS) {
if (op.startsWith(partial)) {
return true;
}
}
return false;
}
/** @type {import("./index.js").Scanner} */
function operatorScanner(char, input, {
start,
grouping
}) {
let value = char;
if (opsPotential(value)) {
let [nextChar] = input.slice(start + value.length);
let nextValue = value + nextChar;
while (nextChar && opsPotential(nextValue)) {
value += nextChar;
[nextChar] = input.slice(start + value.length);
nextValue = value + nextChar;
}
}
const known = KNOWN_OPS.get(value);
if (known) {
return {
type: known.sep && grouping ? "sep.col" : "operator",
value: known.value,
attrs: known.attrs,
end: start + value.length
};
}
const command = KNOWN_COMMANDS.get(value);
if (command) {
return {
type: "command",
end: start + value.length,
...command
};
}
if (!isOperational(char)) {
return null;
}
/** @type {Token & { end: number }} */
const token = {
type: "operator",
value: char,
end: start + char.length
};
const isStretchy = char === "|" && input.at(start - 1) === " " && input.at(start + 1) === " ";
if (isStretchy) {
token.attrs = {
stretchy: true
};
}
return token;
}
/**
* @param {string} partial
* @returns {boolean}
*/
function parenClosePotential(partial) {
for (const [paren] of KNOWN_PARENS_CLOSE) {
if (paren.startsWith(partial)) {
return true;
}
}
return false;
}
/** @type {import("./index.js").Scanner} */
function parenCloseScanner(char, input, {
grouping,
start
}) {
if (!grouping) {
return null;
}
let value = char;
if (parenClosePotential(value)) {
let [nextChar] = input.slice(start + value.length);
let nextValue = value + nextChar;
while (nextChar && parenClosePotential(nextValue)) {
value += nextChar;
[nextChar] = input.slice(start + value.length);
nextValue = value + nextChar;
}
}
{
const [nextChar] = input.slice(start + value.length);
const nextValue = value + nextChar;
if (KNOWN_OPS.has(nextValue)) {
return null;
}
}
const known = KNOWN_PARENS_CLOSE.get(value);
if (known) {
return {
type: "paren.close",
value: known.value,
end: start + value.length
};
}
if (isPunctClose(char)) {
return {
type: "paren.close",
value: char,
end: start + char.length
};
}
return null;
}
/**
* @param {string} partial
* @returns {boolean}
*/
function parenOpenPotential(partial) {
for (const [paren] of KNOWN_PARENS_OPEN) {
if (paren.startsWith(partial)) {
return true;
}
}
return false;
}
/** @type {import("./index.js").Scanner} */
function parenOpenScanner(char, input, {
start
}) {
let value = char;
if (parenOpenPotential(value)) {
let [nextChar] = input.slice(start + value.length);
let nextValue = value + nextChar;
while (nextChar && parenOpenPotential(nextValue)) {
value += nextChar;
[nextChar] = input.slice(start + value.length);
nextValue = value + nextChar;
}
}
{
const [nextChar] = input.slice(start + value.length);
const nextValue = value + nextChar;
if (KNOWN_OPS.has(nextValue)) {
return null;
}
}
const known = KNOWN_PARENS_OPEN.get(value);
if (known) {
return {
...known,
type: "paren.open",
end: start + value.length
};
}
if (isPunctOpen(char)) {
return {
type: "paren.open",
value: char,
end: start + char.length
};
}
return null;
}
/** @type {import("./index.js").Scanner} */
function quoteScanner(char, input, {
start
}) {
if (char !== '"') {
return null;
}
const {
end,
value
} = handleQuote(start, input);
return {
type: "text",
value,
end
};
}
/** @type {import("./index.js").Scanner} */
function spaceScanner(char, input, {
start
}) {
if (char !== " ") {
return null;
}
let i = start;
let nextChar = char;
let value = "";
while (nextChar === " ") {
value += nextChar;
i += 1;
nextChar = input[i];
}
return {
type: "space",
value,
end: i
};
}
/**
* @typedef {import("./index.js").Scanner} Scanner
* @type {(
* ...args: Parameters<Scanner>
* ) => NonNullable<ReturnType<Scanner>>}
*/
function unhandledScanner(char, _input, {
start
}) {
return {
type: "ident",
value: char,
end: start + char.length
};
}
/**
* @typedef {Required<import("../index.js").TokenizerOptions>} Options
* @typedef {import("../index.js").State} State
* @typedef {import("../index.js").Token} Token
* @typedef {(
* char: string,
* input: string,
* state: State,
* options: Options,
* ) => (Token & { end: number }) | null} Scanner
* @type {Scanner[]}
*/
var scanners = [spaceScanner, alphaScanner, numberScanner, backtickScanner, quoteScanner, octothorpeScanner, backslashScanner, newlineScanner, infixScanner, groupSepScanner, parenOpenScanner, parenCloseScanner, operatorScanner];
/**
* @typedef {"command"
* | "ident"
* | "infix"
* | "number"
* | "operator"
* | "paren.close"
* | "paren.open"
* | "prefix"
* | "sep.col"
* | "sep.row"
* | "space"
* | "text"} TokenType
*
* @typedef {object} Token
* @property {TokenType} type
* @property {string} value
* @property {string} [accent]
* @property {number} [arity]
* @property {string} [name]
* @property {boolean} [split] - Can be broken up into smaller units.
* @property {boolean} [sep] - Can function as a group seperator.
* @property {Record<string, string | number | boolean | null | undefined>} [attrs]
* @property {Token[]} [extraTokensAfter]
*
* @typedef {object} State
* @property {number} start
* @property {boolean} grouping
*
* @typedef {object} TokenizerOptions
* @property {string} [decimalMark="."] - Decimal separator. Default is `"."`
* @property {string} [colSep=","] - Column separator e.g. for arrays and
* matrices. Default is `","`
* @property {string} [rowSep=";"] - Row separator e.g. for matrices. Default is
* `";"`
*
* @param {Required<TokenizerOptions>} options
* @returns {(input: string) => Generator<Token>}
*/
function createTokenizer(options) {
/**
* @param {string} input
* @param {State} state
* @returns {Token & { end: number }}
*/
function nextToken(input, state) {
const [char] = input.slice(state.start);
for (const scan of scanners) {
const token = scan(char, input, state, options);
if (token) {
return token;
}
}
return unhandledScanner(char, input, state);
}
/**
* @param {string} input
* @yields {Token}
*/
return function* tokenize(input) {
let pos = 0;
let nestLevel = 0;
while (pos < input.length) {
const {
type,
value,
end,
split,
extraTokensAfter,
...attrs
} = nextToken(input, {
start: pos,
grouping: nestLevel > 0
});
if (split) {
let char = "";
for (const codePoint of value) {
if (isMark(codePoint)) {
char += codePoint;
} else if (char) {
yield {
type,
value: char
};
char = codePoint;
} else {
char = codePoint;
}
}
if (char) {
yield {
type,
value: char
};
}
} else {
yield {
type,
value,
...attrs
};
}
if (extraTokensAfter) {
for (const token of extraTokensAfter) {
yield token;
}
}
pos = end;
if (type === "paren.open") {
nestLevel += 1;
} else if (type === "paren.close" && nestLevel > 0) {
nestLevel -= 1;
}
}
};
}
/**
* @typedef {import("../../parser/index.js").FencedGroup} FencedGroup
*/
/**
* @param {import("../index.js").Tag | null} tag
* @returns {boolean}
*/
function notNull$2(tag) {
return tag !== null;
}
/** @type {import("../index.js").TransformFn<FencedGroup>} */
function fencedGroup(node, transform) {
const cols = node.items.map(col => {
if (col.length === 1) {
return transform(col[0]);
}
return {
tag: "mrow",
childNodes: col.map(transform).filter(notNull$2)
};
}).reduce((joined, next, i) => {
joined?.childNodes?.push(next);
const sep = node.attrs.seps[i];
if (sep) {
const {
value,
attrs = {}
} = sep;
joined?.childNodes?.push({
tag: "mo",
textContent: value,
attrs: {
...attrs,
separator: true
}
});
}
return joined;
}, {
tag: "mrow",
childNodes: []
});
const {
open,
close
} = node.attrs;
if (cols && !open.value && !close?.value) {
return cols;
}
const childNodes = [];
if (open.value) {
childNodes.push({
tag: "mo",
textContent: open.value,
attrs: {
...open.attrs,
fence: "true"
}
});
}
if (cols?.childNodes?.length === 1) {
childNodes.push(cols.childNodes[0]);
} else {
childNodes.push(cols);
}
if (close?.value) {
childNodes.push({
tag: "mo",
textContent: close.value,
attrs: {
...close.attrs,
fence: "true"
}
});
}
return {
tag: "mrow",
childNodes
};
}
/**
* @typedef {import("../../parser/index.js").Literal} Literal
* @typedef {import("../../parser/index.js").SpaceLiteral} SpaceLiteral
*/
/**
* @param {string} tag
* @returns {import("../index.js").TransformFn<
* Exclude<Literal, SpaceLiteral>
* >}
*/
function literal(tag) {
return node => {
return {
tag,
attrs: node.attrs,
textContent: node.value
};
};
}
/**
* @typedef {import("../../parser/index.js").Node} Node
* @typedef {import("../../parser/index.js").MatrixGroup} MatrixGroup
* @typedef {import("../index.js").Tag} Tag
*/
/**
* @param {Tag | null} tag
* @returns {boolean}
*/
function notNull$1(tag) {
return tag !== null;
}
/** @type {import("../index.js").TransformFn<MatrixGroup>} */
function matrixGroup(node, transform) {
const childNodes = [];
const colCount = node.items.reduce((max, row) => Math.max(max, row.length), 0);
const {
open,
close
} = node.attrs;
// auto start-align tables with no close param.
const aligns = !close?.value ? Array.from({
length: colCount
}).fill("start") : [];
if (open.value) {
childNodes.push({
tag: "mo",
textContent: open.value,
attrs: {
...open.attrs,
fence: "true"
}
});
}
childNodes.push({
tag: "mtable",
childNodes: node.items.map(row => ({
tag: "mtr",
childNodes: row.map((col, i) => ({
tag: "mtd",
attrs: aligns[i] ? {
style: `text-align: ${aligns[i]}`
} : {},
childNodes: col.map(transform).filter(notNull$1)
}))
}))
});
if (close?.value) {
childNodes.push({
tag: "mo",
textContent: close.value,
attrs: {
...close.attrs,
fence: "true"
}
});
}
return {
tag: "mrow",
childNodes
};
}
/**
* @typedef {import("../../parser/index.js").MultiScripts} MultiScripts
* @type {import("../index.js").TransformFn<MultiScripts>}
*/
function multiscripts(node, transform) {
const postscripts = node.post.flatMap(pair => pair.map(nodes => {
if (nodes.length === 1) {
return transform(nodes[0]);
}
return {
tag: "mrow",
childNodes: nodes.map(transform)
};
}));
const childNodes = [transform(node.base), ...postscripts];
if (node.pre && node.pre.length > 0) {
childNodes.push({
tag: "mprescripts"
});
const prescripts = node.pre.flatMap(pair => pair.map(nodes => {
if (nodes.length === 1) {
return transform(nodes[0]);
}
return {
tag: "mrow",
childNodes: nodes.map(transform)
};
}));
childNodes.push(...prescripts);
}
return {
tag: "mmultiscripts",
childNodes
};
}
/**
* @typedef {import("../../parser/index.js").BinaryOperation} BinaryOperation
* @typedef {import("../../parser/index.js").TernaryOperation} TernaryOperation
* @type {import("../index.js").TransformFn<
* BinaryOperation | TernaryOperation
* >}
*/
function operation(node, transform) {
const {
open,
close,
...attrs
} = node.attrs || {};
const opNode = {
tag: `m${node.name}`,
childNodes: node.items.map(transform),
attrs
};
if (open || close) {
return {
tag: "mrow",
childNodes: [{
tag: "mo",
textContent: `${open}`,
attrs: {
fence: true
}
}, opNode, {
tag: "mo",
textContent: `${close}`,
attrs: {
fence: true
}
}]
};
}
return opNode;
}
/**
* @typedef {import("../../parser/index.js").Sentence} Sentence
* @type {import("../index.js").TransformFn<Sentence>}
*/
function sentence(node, transform, options) {
/** @type {Record<string, string>} */
const attrs = {};
if (options.dir) {
attrs.dir = options.dir;
}
if (options.display) {
attrs.display = options.display;
}
return {
tag: "math",
attrs,
childNodes: node.body.map(transform).filter(x => x !== null)
};
}
/**
* @typedef {import("../../parser/index.js").SpaceLiteral} SpaceLiteral
* @type {import("../index.js").TransformFn<SpaceLiteral>}
*/
function spaceLiteral(node) {
if (node.attrs.width === "0ex") {
// No 0 width whitespace.
return null;
}
return {
tag: "mspace",
attrs: node.attrs
};
}
/** @typedef {import("../index.js").Tag} Tag */
/**
* @param {string} name
* @param {string} value
* @returns {string}
*/
function withCustomProperty(name, value) {
return `var(--mathup-${name}-${value}, ${value})`;
}
/**
* @param {Map<string, string> | undefined} styles
* @param {Tag | null} tag
* @returns {Tag | null}
*/
function applyStyles(styles, tag) {
if (!styles || !tag) {
return tag;
}
/** @type {Map<string, string>} */
const combinedStyles = new Map();
for (const [name, value] of styles) {
if (name === "background") {
const currentValue = combinedStyles.get("background");
const backgroundValue = withCustomProperty(name, value);
if (currentValue) {
combinedStyles.set("background", `${currentValue},${backgroundValue}`);
} else {
combinedStyles.set("background", backgroundValue);
}
} else {
combinedStyles.set(name, withCustomProperty(name, value));
}
}
let style = tag.attrs?.style ?? "";
for (const [name, value] of combinedStyles) {
style += `${name}:${value};`;
}
if (!style) {
return tag;
}
return {
...tag,
attrs: tag.attrs ? {
...tag.attrs,
style
} : {
style
}
};
}
/**
* @typedef {import("../../parser/index.js").Node} Node
* @typedef {import("../../parser/index.js").Term} Term
* @typedef {import("../index.js").Tag} Tag
*/
/**
* @template T
* @param {T} node
* @returns {boolean}
*/
function notNull(node) {
return node !== null;
}
/** @type {import("../index.js").TransformFn<Term>} */
function term(node, transform) {
if (node.items.length === 1 && notNull(node.items[0])) {
return applyStyles(node.styles, transform(node.items[0]));
}
return applyStyles(node.styles, {
tag: "mrow",
childNodes: node.items.map(transform).filter(notNull)
});
}
// See tables at https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols
/**
* @param {(char: string) => string} fn
* @param {string} string
* @returns {string}
*/
function mapChars(fn, string) {
let mapped = "";
for (const char of string) {
mapped += fn(char);
}
return mapped;
}
/**
* @param {[[number, number], number][]} ranges
* @param {Map<string, string>} [defaults]
* @returns {(char: string) => string}
*/
function createCharMapping(ranges, defaults) {
return char => {
if (defaults) {
const found = defaults.get(char);
if (found) {
return found;
}
}
const codePoint = char.codePointAt(0);
if (!codePoint) {
return char;
}
for (const [[min, max], offset] of ranges) {
if (min <= codePoint && codePoint <= max) {
return String.fromCodePoint(codePoint + offset);
}
}
return char;
};
}
/**
* @param {[number, number]} range
* @returns {(offset: number) => [[number, number], number]}
*/
function createRange([min, max]) {
return offset => [[min, max], offset - min];
}
const digitRange = createRange([0x30, 0x39]);
const latinCapitalLetterRange = createRange([0x41, 0x5a]);
const latinSmallLetterRange = createRange([0x61, 0x7a]);
const greekCapitalLetterRange = createRange([0x0391, 0x03a9]);
const greekSmallLetterRange = createRange([0x03b1, 0x03c9]);
const bold = createCharMapping([digitRange(0x1d7ce), latinCapitalLetterRange(0x1d400), latinSmallLetterRange(0x1d41a), greekCapitalLetterRange(0x1d6a8), greekSmallLetterRange(0x1d6c2)], new Map([["ϴ", "𝚹"], ["∇", "𝛁"], ["∂", "𝛛"], ["ϵ", "𝛜"], ["ϑ", "𝛝"], ["ϰ", "𝛞"], ["ϕ", "𝛟"], ["ϱ", "𝛠"], ["ϖ", "𝛡"], ["Ϝ", "𝟊"], ["ϝ", "𝟋"]]));
const italic = createCharMapping([latinCapitalLetterRange(0x1d434), latinSmallLetterRange(0x1d44e), greekCapitalLetterRange(0x1d6e2), greekSmallLetterRange(0x1d6fc)], new Map([["h", "ℎ"], ["ı", "𝚤"], ["ȷ", "𝚥"], ["ϴ", "𝛳"], ["∇", "𝛻"], ["∂", "𝜕"], ["ϵ", "𝜖 "], ["ϑ", "𝜗"], ["ϰ", "𝜘"], ["ϕ", "𝜙"], ["ϱ", "𝜚"], ["ϖ", "𝜛"]]));
const boldItalic = createCharMapping([latinCapitalLetterRange(0x1d468), latinSmallLetterRange(0x1d482), greekCapitalLetterRange(0x1d71c), greekSmallLetterRange(0x1d736)], new Map([["ϴ", "𝜭"], ["∇", "𝜵"], ["∂", "𝝏"], ["ϵ", "𝝐"], ["ϑ", "𝝑"], ["ϰ", "𝝒"], ["ϕ", "𝝓"], ["ϱ", "𝝔"], ["ϖ", "𝝕"]]));
const doubleStruck = createCharMapping([digitRange(0x1d7d8), latinCapitalLetterRange(0x1d538), latinSmallLetterRange(0x1d552)], new Map([["C", "ℂ"], ["H", "ℍ"], ["N", "ℕ"], ["P", "ℙ"], ["Q", "ℚ"], ["R", "ℝ"], ["Z", "ℤ"], ["π", "ℼ"], ["γ", "ℽ"], ["Π", "ℿ"], ["∑", "⅀"]]));
const italicDoubleStruck = createCharMapping([], new Map([["D", "ⅅ"], ["d", "ⅆ"], ["i", "ⅈ"], ["j", "ⅉ"]]));
const fraktur = createCharMapping([latinCapitalLetterRange(0x1d504), latinSmallLetterRange(0x1d51e)], new Map([["C", "ℭ"], ["H", "ℌ"], ["I", "ℑ"], ["R", "ℜ"], ["Z", "ℨ"]]));
const boldFraktur = createCharMapping([latinCapitalLetterRange(0x1d56c), latinSmallLetterRange(0x1d586)]);
const monospace = createCharMapping([digitRange(0x1d7f6), latinCapitalLetterRange(0x1d670), latinSmallLetterRange(0x1d68a)]);
const sansSerif = createCharMapping([digitRange(0x1d7e2), latinCapitalLetterRange(0x1d5a0), latinSmallLetterRange(0x1d5ba)]);
const boldSansSerif = createCharMapping([digitRange(0x1d7ec), latinCapitalLetterRange(0x1d5d4), latinSmallLetterRange(0x1d5ee), greekCapitalLetterRange(0x1d756), greekSmallLetterRange(0x1d770)], new Map([["ϴ", "𝝧"], ["∇", "𝝯"], ["∂", "𝞉"], ["ϵ", "𝞊"], ["ϑ", "𝞋"], ["ϰ", "𝞌"], ["ϕ", "𝞍"], ["ϱ", "𝞎"], ["ϖ", "𝞏"]]));
const italicSansSerif = createCharMapping([latinCapitalLetterRange(0x1d608), latinSmallLetterRange(0x1d622)]);
const boldItalicSansSerif = createCharMapping([latinCapitalLetterRange(0x1d63c), latinSmallLetterRange(0x1d656), greekCapitalLetterRange(0x1d790), greekSmallLetterRange(0x1d7aa)], new Map([["ϴ", "𝞡"], ["∇", "𝞩"], ["∂", "𝟃"], ["ϵ", "𝟄"], ["ϑ", "𝟅"], ["ϰ", "𝟆"], ["ϕ", "𝟇"], ["ϱ", "𝟈"], ["ϖ", "𝟉"]]));
const script = createCharMapping([latinCapitalLetterRange(0x1d49c), latinSmallLetterRange(0x1d4b6)], new Map([["B", "ℬ"], ["E", "ℰ"], ["F", "ℱ"], ["H", "ℋ"], ["I", "ℐ"], ["L", "ℒ"], ["M", "ℳ"], ["R", "ℛ"], ["e", "ℯ"], ["g", "ℊ"], ["o", "ℴ"]]));
const boldScript = createCharMapping([latinCapitalLetterRange(0x1d4d0), latinSmallLetterRange(0x1d4ea)]);
/**
* @param {string} family
* @returns {string}
*/
function fontFamilyStyleValue(family) {
return `var(--mathup-font-family-${family}, ${family})`;
}
/**
* @typedef {import("../../parser/index.js").LiteralAttrs} LiteralAttrs
* @param {LiteralAttrs} attrs
* @param {string[]} transforms
* @returns {LiteralAttrs}
*/
function textLiteralAttrs(attrs, transforms) {
/** @type {Map<string, string>} */
const styles = new Map();
if (transforms.includes("bold")) {
styles.set("font-weight", "bold");
}
if (transforms.includes("italic")) {
styles.set("font-style", "italic");
}
// serif is default font-family, so no need to check for `rm`.
if (transforms.includes("sans-serif")) {
styles.set("font-family", fontFamilyStyleValue("sans-serif"));
} else if (transforms.includes("monospace")) {
styles.set("font-family", fontFamilyStyleValue("monospace"));
}
if (styles.size === 0) {
return attrs;
}
let style = "";
for (const [name, value] of styles) {
style += `${name}:${value};`;
}
return {
...attrs,
style
};
}
/**
* @param {string} text
* @param {string[]} transforms
* @returns {string}
*/
function textLiteralValue(text, transforms) {
if (transforms.includes("double-struck")) {
return mapChars(doubleStruck, text);
}
if (transforms.includes("script")) {
return mapChars(script, text);
}
if (transforms.includes("fraktur")) {
return mapChars(fraktur, text);
}
return text;
}
/**
* @param {string} text
* @param {string[]} transforms
* @returns {string}
*/
function literalValue(text, transforms) {
if (transforms.includes("bold")) {
if (transforms.includes("italic")) {
if (transforms.includes("sans-serif")) {
return mapChars(boldItalicSansSerif, text);
}
return mapChars(boldItalic, text);
}
if (transforms.includes("script")) {
return mapChars(boldScript, text);
}
if (transforms.includes("fraktur")) {
return mapChars(boldFraktur, text);
}
if (transforms.includes("sans-serif")) {
return mapChars(boldSansSerif, text);
}
return mapChars(bold, text);
}
if (transforms.includes("italic")) {
if (transforms.includes("double-struck")) {
return mapChars(italicDoubleStruck, text);
}
if (transforms.includes("sans-serif")) {
return mapChars(italicSansSerif, text);
}
return mapChars(italic, text);
}
if (transforms.includes("sans-serif")) {
return mapChars(sansSerif, text);
}
if (transforms.includes("monospace")) {
return mapChars(monospace, text);
}
return textLiteralValue(text, transforms);
}
/**
* @typedef {import("../../parser/index.js").Node} ASTNode
* @typedef {import("../../parser/index.js").UnaryOperation} UnaryOperation
* @typedef {import("../../parser/index.js").Literal} Literal
* @typedef {import("../../parser/index.js").SpaceLiteral} SpaceLiteral
*/
/**
* @template {ASTNode} Node
* @overload
* @param {Node} childNode
* @param {UnaryOperation} node
* @returns {Node}
*/
/**
* @template {ASTNode} Node
* @overload
* @param {Node[]} childNode
* @param {UnaryOperation} node
* @returns {Node[]}
*/
/**
* @template {ASTNode} Node
* @overload
* @param {Node[][]} childNode
* @param {UnaryOperation} node
* @returns {Node[][]}
*/
/**
* @template {ASTNode} Node
* @param {Node | Node[] | Node[][]} childNode
* @param {UnaryOperation} node
* @returns {Node | Node[] | Node[][]}
*/
function handleCommand(childNode, node) {
if (Array.isArray(childNode)) {
return childNode.map(grandChild => handleCommand(/** @type {Node} */grandChild, node));
}
if (childNode.type === "TextLiteral") {
const {
attrs = {},
transforms = []
} = node;
return {
...childNode,
attrs: textLiteralAttrs(attrs, transforms),
value: textLiteralValue(childNode.value, transforms)
};
}
if (childNode.type === "IdentLiteral" && childNode.value.length === 1 && node.transforms && node.transforms.length === 1 && node.transforms[0] === "normal") {
// Disable auto-italic idents
return {
...childNode,
attrs: {
...(childNode.attrs ?? {}),
mathvariant: "normal"
}
};
}
if (childNode.type === "IdentLiteral" || childNode.type === "NumberLiteral" || childNode.type === "OperatorLiteral") {
const {
transforms = []
} = node;
return {
...childNode,
value: literalValue(childNode.value, transforms)
};
}
if (childNode.type === "MultiScripts") {
return {
...childNode,
base: handleCommand(childNode.base, node)
};
}
if ("items" in childNode) {
return {
...childNode,
items: childNode.items.map(grandChild => handleCommand(/** @type {Node} */grandChild, node))
};
}
return childNode;
}
/** @type {import("../index.js").TransformFn<UnaryOperation>} */
function unaryOperation(node, transform) {
if (node.name === "command") {
return applyStyles(node.styles, transform(handleCommand(node.items[0], node)));
}
if (node.name === "fence") {
const {
open,
close
} = node.attrs || {};
/** @type {(import("../index.js").Tag | null)[]} */
const childNodes = [{
tag: "mo",
attrs: {
fence: true
},
textContent: `${open}`
}, node.items.length === 1 ? transform(node.items[0]) : {
tag: "mrow",
childNodes: node.items.map(transform)
}, {
tag: "mo",
attrs: {
fence: true
},
textContent: `${close}`
}];
return {
tag: "mrow",
childNodes
};
}
const tag = `m${node.name}`;
let [operand] = node.items;
if (operand.type === "Term" && operand.items.length === 1) {
// There is no need of a singleton term.
[operand] = operand.items;
}
if (node.accent) {
const attrs = node.attrs ? {
...node.attrs
} : {};
if (node.name === "over") {
attrs.accent = true;
// Remove the dot from i and j to make way for the accent.
if (operand.type === "IdentLiteral") {
if (operand.value === "i") {
operand.value = "ı";
} else if (operand.value === "j") {
operand.value = "ȷ";
}
}
} else if (node.name === "under") {
attrs.accentunder = true;
}
const childNodes = [transform(operand), {
tag: "mo",
textContent: node.accent
}];
return {
tag,
childNodes,
attrs
};
}
return {
tag,
childNodes: [transform(operand)],
attrs: node.attrs
};
}
/** @typedef {import("../index.js").TransformFn} TransformFn */
var transforms = new Map([["BinaryOperation", (/** @type {TransformFn} */operation)], ["FencedGroup", (/** @type {TransformFn} */fencedGroup)], ["IdentLiteral", (/** @type {TransformFn} */literal("mi"))], ["MatrixGroup", (/** @type {TransformFn} */matrixGroup)], ["MultiScripts", (/** @type {TransformFn} */multiscripts)], ["NumberLiteral", (/** @type {TransformFn} */literal("mn"))], ["OperatorLiteral", (/** @type {TransformFn} */literal("mo"))], ["Sentence", (/** @type {TransformFn} */sentence)], ["SpaceLiteral", (/** @type {TransformFn} */spaceLiteral)], ["Term", (/** @type {TransformFn} */term)], ["TernaryOperation", (/** @type {TransformFn} */operation)], ["TextLiteral", (/** @type {TransformFn} */literal("mtext"))], ["UnaryOperation", (/** @type {TransformFn} */unaryOperation)]]);
/**
* @typedef {import("../parser/index.js").Node} ASTNode
* @typedef {object} TransformerOptions
* @property {"ltr" | "rtl" | null} [dir=null] - The reading direction of the
* output. Default is `null`
* @property {"block" | "inline" | null} [display=null] - Whether the expression
* should be in display mode. Default is `null` Default is `null` Default is
* `null` Default is `null`
* @typedef {object} Tag
* @property {string} tag
* @property {Record<string, string | number | boolean | undefined | null>} [attrs]
* @property {(Tag | null)[]} [childNodes]
* @property {string} [textContent]
*/
/**
* @template {ASTNode} [Node=ASTNode] Default is `ASTNode`
* @typedef {(
* node: Node,
* transform: (node: ASTNode) => Tag | null,
* options: Required<TransformerOptions>,
* ) => Tag | null} TransformFn
*/
/**
* @param {Required<TransformerOptions>} options
* @returns {(node: ASTNode) => Tag | null}
*/
function transformer(options) {
return function transform(node) {
const transformNode = transforms.get(node.type);
if (!transformNode) {
return null;
}
return transformNode(node, transform, options);
};
}
/**
* @typedef {import("./tokenizer/index.js").TokenizerOptions} TokenizerOptions
* @typedef {import("./transformer/index.js").TransformerOptions} TransformerOptions
* @typedef {import("./renders/index.js").RenderOptions} RenderOptions
* @typedef {TokenizerOptions & TransformerOptions & RenderOptions} Options
*
* @typedef {object} Result
* @property {() => string} toString
* @property {() => Element | DocumentFragment} toDOM
* @property {(root: Element) => void} updateDOM
*
* @param {Options} options
* @returns {(input: string) => Result}
*/
function compiler({
bare = false,
dir = null,
display = null,
decimalMark = ".",
colSep = decimalMark === "," ? ";" : ",",
rowSep = colSep === ";" ? ";;" : ";"
}) {
const tokenizerOptions = {
decimalMark,
colSep,
rowSep
};
const transformerOptions = {
dir,
display
};
const renderOptions = {
bare
};
/**
* @param {string} input - Mathup expression
* @returns {Result}
*/
return function compile(input) {
const tokenize = createTokenizer(tokenizerOptions);
const tokens = tokenize(input);
const ast = parse([...tokens]);
const transform = transformer(transformerOptions);
const domTree = transform(ast);
if (!domTree) {
throw new Error("Failed parsing AST");
}
return {
toDOM() {
return toDOM(domTree, renderOptions);
},
toString() {
return toString(domTree, renderOptions);
},
updateDOM(root) {
return updateDOM(root, domTree, renderOptions);
}
};
};
}
/**
* @typedef {import("./compiler/index.js").Result} Mathup
* @typedef {import("./compiler/index.js").Options} Options
* @param {string} input
* @param {Options} [options]
* @returns {Mathup}
*/
function mathup(input, options = {}) {
const compile = compiler(options);
return compile(input);
}
return mathup;
})();
//# sourceMappingURL=mathup.iife.js.map