UNPKG

mathup

Version:

Easy MathML authoring tool with a quick to write syntax

2,241 lines (2,152 loc) 93.8 kB
/*! 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 "&amp;"; } return "&lt;"; }); } /** * @param {string} str * @returns {string} */ function escapeAttrValue(str) { return str.replace(/"/g, "&quot;"); } /** * @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" }], ["