First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+285
View File
@@ -0,0 +1,285 @@
import defineFunction, {normalizeArgument} from "../defineFunction";
import {makeOrd, makeSpan, makeVList, staticSvg, svgData} from "../buildCommon";
import {isCharacterBox} from "../utils";
import {MathNode} from "../mathMLTree";
import {stretchyMathML, stretchySvg} from "../stretchy";
import {assertNodeType} from "../parseNode";
import {assertSpan, assertSymbolDomNode, hasHtmlDomChildren, SymbolNode} from "../domTree";
import {makeEm} from "../units";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {ParseNode, AnyParseNode} from "../parseNode";
import type {HtmlBuilderSupSub, MathMLBuilder} from "../defineFunction";
import type {HtmlDomNode} from "../domTree";
const getBaseSymbol = (group: HtmlDomNode): SymbolNode | undefined => {
if (group instanceof SymbolNode) {
return group;
}
if (hasHtmlDomChildren(group) && group.children.length === 1) {
return getBaseSymbol(group.children[0]);
}
};
// NOTE: Unlike most `htmlBuilder`s, this one handles not only "accent", but
// also "supsub" since an accent can affect super/subscripting.
export const htmlBuilder: HtmlBuilderSupSub<"accent"> = (grp, options) => {
// Accents are handled in the TeXbook pg. 443, rule 12.
let base: AnyParseNode;
let group: ParseNode<"accent">;
let supSubGroup;
if (grp && grp.type === "supsub") {
// If our base is a character box, and we have superscripts and
// subscripts, the supsub will defer to us. In particular, we want
// to attach the superscripts and subscripts to the inner body (so
// that the position of the superscripts and subscripts won't be
// affected by the height of the accent). We accomplish this by
// sticking the base of the accent into the base of the supsub, and
// rendering that, while keeping track of where the accent is.
// The real accent group is the base of the supsub group
group = assertNodeType(grp.base, "accent");
// The character box is the base of the accent group
base = group.base;
// Stick the character box into the base of the supsub group
grp.base = base;
// Rerender the supsub group with its new base, and store that
// result.
supSubGroup = assertSpan(html.buildGroup(grp, options));
// reset original base
grp.base = group;
} else {
group = assertNodeType(grp, "accent");
base = group.base;
}
// Build the base group
const body = html.buildGroup(base, options.havingCrampedStyle());
// Does the accent need to shift for the skew of a character?
const mustShift = group.isShifty && isCharacterBox(base);
// Calculate the skew of the accent. This is based on the line "If the
// nucleus is not a single character, let s = 0; otherwise set s to the
// kern amount for the nucleus followed by the \skewchar of its font."
// Note that our skew metrics are just the kern between each character
// and the skewchar.
let skew = 0;
if (mustShift) {
// Read the skew from the rendered base symbol.
// This preserves font metrics from font wrappers like \mathbb.
skew = getBaseSymbol(body)?.skew ?? 0;
}
const accentBelow = group.label === "\\c";
// calculate the amount of space between the body and the accent
let clearance = accentBelow
? body.height + body.depth
: Math.min(
body.height,
options.fontMetrics().xHeight);
// Build the accent
let accentBody;
if (!group.isStretchy) {
let accent;
let width: number;
if (group.label === "\\vec") {
// Before version 0.9, \vec used the combining font glyph U+20D7.
// But browsers, especially Safari, are not consistent in how they
// render combining characters when not preceded by a character.
// So now we use an SVG.
// If Safari reforms, we should consider reverting to the glyph.
accent = staticSvg("vec", options);
width = svgData.vec[1];
} else {
accent = makeOrd({type: "textord", mode: group.mode, text: group.label},
options, "textord");
accent = assertSymbolDomNode(accent);
// Remove the italic correction of the accent, because it only serves to
// shift the accent over to a place we don't want.
accent.italic = 0;
width = accent.width;
if (accentBelow) {
clearance += accent.depth;
}
}
accentBody = makeSpan(["accent-body"], [accent]);
// "Full" accents expand the width of the resulting symbol to be
// at least the width of the accent, and overlap directly onto the
// character without any vertical offset.
const accentFull = (group.label === "\\textcircled");
if (accentFull) {
accentBody.classes.push('accent-full');
clearance = body.height;
}
// Shift the accent over by the skew.
let left = skew;
// CSS defines `.katex .accent .accent-body:not(.accent-full) { width: 0 }`
// so that the accent doesn't contribute to the bounding box.
// We need to shift the character by its width (effectively half
// its width) to compensate.
if (!accentFull) {
left -= width / 2;
}
accentBody.style.left = makeEm(left);
// \textcircled uses the \bigcirc glyph, so it needs some
// vertical adjustment to match LaTeX.
if (group.label === "\\textcircled") {
accentBody.style.top = ".2em";
}
accentBody = makeVList({
positionType: "firstBaseline",
children: [
{type: "elem", elem: body},
{type: "kern", size: -clearance},
{type: "elem", elem: accentBody},
],
}, options);
} else {
accentBody = stretchySvg(group, options);
accentBody = makeVList({
positionType: "firstBaseline",
children: [
{type: "elem", elem: body},
{
type: "elem",
elem: accentBody,
wrapperClasses: ["svg-align"],
wrapperStyle: skew > 0
? {
width: `calc(100% - ${makeEm(2 * skew)})`,
marginLeft: makeEm(2 * skew),
}
: undefined,
},
],
}, options);
}
const accentWrap =
makeSpan(["mord", "accent"], [accentBody], options);
if (supSubGroup) {
// Here, we replace the "base" child of the supsub with our newly
// generated accent.
supSubGroup.children[0] = accentWrap;
// Since we don't rerun the height calculation after replacing the
// accent, we manually recalculate height.
supSubGroup.height = Math.max(accentWrap.height, supSubGroup.height);
// Accents should always be ords, even when their innards are not.
supSubGroup.classes[0] = "mord";
return supSubGroup;
} else {
return accentWrap;
}
};
const mathmlBuilder: MathMLBuilder<"accent"> = (group, options) => {
const accentNode =
group.isStretchy ?
stretchyMathML(group.label) :
new MathNode("mo", [mml.makeText(group.label, group.mode)]);
const node = new MathNode(
"mover",
[mml.buildGroup(group.base, options), accentNode]);
node.setAttribute("accent", "true");
return node;
};
const NON_STRETCHY_ACCENT_REGEX = new RegExp([
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
"\\check", "\\hat", "\\vec", "\\dot", "\\mathring",
].map(accent => `\\${accent}`).join("|"));
// Accents
defineFunction({
type: "accent",
names: [
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
"\\check", "\\hat", "\\vec", "\\dot", "\\mathring", "\\widecheck",
"\\widehat", "\\widetilde", "\\overrightarrow", "\\overleftarrow",
"\\Overrightarrow", "\\overleftrightarrow", "\\overgroup",
"\\overlinesegment", "\\overleftharpoon", "\\overrightharpoon",
],
props: {
numArgs: 1,
},
handler: (context, args) => {
const base = normalizeArgument(args[0]);
const isStretchy = !NON_STRETCHY_ACCENT_REGEX.test(context.funcName);
const isShifty = !isStretchy ||
context.funcName === "\\widehat" ||
context.funcName === "\\widetilde" ||
context.funcName === "\\widecheck";
return {
type: "accent",
mode: context.parser.mode,
label: context.funcName,
isStretchy: isStretchy,
isShifty: isShifty,
base: base,
};
},
htmlBuilder,
mathmlBuilder,
});
// Text-mode accents
defineFunction({
type: "accent",
names: [
"\\'", "\\`", "\\^", "\\~", "\\=", "\\u", "\\.", '\\"',
"\\c", "\\r", "\\H", "\\v", "\\textcircled",
],
props: {
numArgs: 1,
allowedInText: true,
allowedInMath: true, // unless in strict mode
argTypes: ["primitive"],
},
handler: (context, args) => {
const base = args[0];
let mode = context.parser.mode;
if (mode === "math") {
context.parser.settings.reportNonstrict("mathVsTextAccents",
`LaTeX's accent ${context.funcName} works only in text mode`);
mode = "text";
}
return {
type: "accent",
mode: mode,
label: context.funcName,
isStretchy: false,
isShifty: true,
base: base,
};
},
htmlBuilder,
mathmlBuilder,
});
+59
View File
@@ -0,0 +1,59 @@
// Horizontal overlap functions
import defineFunction from "../defineFunction";
import {makeSpan, makeVList} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {stretchyMathML, stretchySvg} from "../stretchy";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {ParseNode} from "../parseNode";
defineFunction({
type: "accentUnder",
names: [
"\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow",
"\\undergroup", "\\underlinesegment", "\\utilde",
],
props: {
numArgs: 1,
},
handler: ({parser, funcName}, args) => {
const base = args[0];
return {
type: "accentUnder",
mode: parser.mode,
label: funcName,
base: base,
};
},
htmlBuilder: (group: ParseNode<"accentUnder">, options) => {
// Treat under accents much like underlines.
const innerGroup = html.buildGroup(group.base, options);
const accentBody = stretchySvg(group, options);
const kern = group.label === "\\utilde" ? 0.12 : 0;
// Generate the vlist, with the appropriate kerns
const vlist = makeVList({
positionType: "top",
positionData: innerGroup.height,
children: [
{type: "elem", elem: accentBody, wrapperClasses: ["svg-align"]},
{type: "kern", size: kern},
{type: "elem", elem: innerGroup},
],
}, options);
return makeSpan(["mord", "accentunder"], [vlist], options);
},
mathmlBuilder: (group, options) => {
const accentNode = stretchyMathML(group.label);
const node = new MathNode(
"munder",
[mml.buildGroup(group.base, options), accentNode]
);
node.setAttribute("accentunder", "true");
return node;
},
});
+142
View File
@@ -0,0 +1,142 @@
import defineFunction from "../defineFunction";
import {makeSpan, makeVList, wrapFragment} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {stretchyMathML, stretchySvg} from "../stretchy";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {ParseNode} from "../parseNode";
import type {MathDomNode} from "../mathMLTree";
// Helper function
const paddedNode = (group?: MathDomNode | null | undefined) => {
const node = new MathNode("mpadded", group ? [group] : []);
node.setAttribute("width", "+0.6em");
node.setAttribute("lspace", "0.3em");
return node;
};
// Stretchy arrows with an optional argument
defineFunction({
type: "xArrow",
names: [
"\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow",
"\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow",
"\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown",
"\\xrightharpoonup", "\\xleftharpoondown", "\\xleftharpoonup",
"\\xrightleftharpoons", "\\xleftrightharpoons", "\\xlongequal",
"\\xtwoheadrightarrow", "\\xtwoheadleftarrow", "\\xtofrom",
// The next 3 functions are here to support the mhchem extension.
// Direct use of these functions is discouraged and may break someday.
"\\xrightleftarrows", "\\xrightequilibrium", "\\xleftequilibrium",
// The next 3 functions are here only to support the {CD} environment.
"\\\\cdrightarrow", "\\\\cdleftarrow", "\\\\cdlongequal",
],
props: {
numArgs: 1,
numOptionalArgs: 1,
},
handler({parser, funcName}, args, optArgs) {
return {
type: "xArrow",
mode: parser.mode,
label: funcName,
body: args[0],
below: optArgs[0],
};
},
htmlBuilder(group: ParseNode<"xArrow">, options) {
const style = options.style;
// Build the argument groups in the appropriate style.
// Ref: amsmath.dtx: \hbox{$\scriptstyle\mkern#3mu{#6}\mkern#4mu$}%
// Some groups can return document fragments. Handle those by wrapping
// them in a span.
let newOptions = options.havingStyle(style.sup());
const upperGroup = wrapFragment(
html.buildGroup(group.body, newOptions, options), options);
const arrowPrefix = group.label.slice(0, 2) === "\\x" ? "x" : "cd";
upperGroup.classes.push(arrowPrefix + "-arrow-pad");
let lowerGroup;
if (group.below) {
// Build the lower group
newOptions = options.havingStyle(style.sub());
lowerGroup = wrapFragment(
html.buildGroup(group.below, newOptions, options), options);
lowerGroup.classes.push(arrowPrefix + "-arrow-pad");
}
const arrowBody = stretchySvg(group, options);
// Re shift: Note that stretchySvg returned arrowBody.depth = 0.
// The point we want on the math axis is at 0.5 * arrowBody.height.
const arrowShift = -options.fontMetrics().axisHeight +
0.5 * arrowBody.height;
// 2 mu kern. Ref: amsmath.dtx: #7\if0#2\else\mkern#2mu\fi
let upperShift = -options.fontMetrics().axisHeight
- 0.5 * arrowBody.height - 0.111; // 0.111 em = 2 mu
if (upperGroup.depth > 0.25 || group.label === "\\xleftequilibrium") {
upperShift -= upperGroup.depth; // shift up if depth encroaches
}
// Generate the vlist
let vlist;
if (lowerGroup) {
const lowerShift = -options.fontMetrics().axisHeight
+ lowerGroup.height + 0.5 * arrowBody.height
+ 0.111;
vlist = makeVList({
positionType: "individualShift",
children: [
{type: "elem", elem: upperGroup, shift: upperShift},
{type: "elem", elem: arrowBody, shift: arrowShift},
{type: "elem", elem: lowerGroup, shift: lowerShift},
],
}, options);
} else {
vlist = makeVList({
positionType: "individualShift",
children: [
{type: "elem", elem: upperGroup, shift: upperShift},
{type: "elem", elem: arrowBody, shift: arrowShift},
],
}, options);
}
// TODO(ts): Replace this with passing "svg-align" into makeVList.
(vlist as any).children[0].children[0].children[1].classes.push("svg-align");
return makeSpan(["mrel", "x-arrow"], [vlist], options);
},
mathmlBuilder(group, options) {
const arrowNode = stretchyMathML(group.label);
arrowNode.setAttribute(
"minsize", group.label.charAt(0) === "x" ? "1.75em" : "3.0em"
);
let node;
if (group.body) {
const upperNode = paddedNode(mml.buildGroup(group.body, options));
if (group.below) {
const lowerNode = paddedNode(mml.buildGroup(group.below, options));
node = new MathNode(
"munderover", [arrowNode, lowerNode, upperNode]
);
} else {
node = new MathNode("mover", [arrowNode, upperNode]);
}
} else if (group.below) {
const lowerNode = paddedNode(mml.buildGroup(group.below, options));
node = new MathNode("munder", [arrowNode, lowerNode]);
} else {
// This should never happen.
// Parser.js throws an error if there is no argument.
node = paddedNode();
node = new MathNode("mover", [arrowNode, node]);
}
return node;
},
});
+44
View File
@@ -0,0 +1,44 @@
import defineFunction from "../defineFunction";
import ParseError from "../ParseError";
import {assertNodeType} from "../parseNode";
// \@char is an internal function that takes a grouped decimal argument like
// {123} and converts into symbol with code 123. It is used by the *macro*
// \char defined in macros.js.
defineFunction({
type: "textord",
names: ["\\@char"],
props: {
numArgs: 1,
allowedInText: true,
},
handler({parser}, args) {
const arg = assertNodeType(args[0], "ordgroup");
const group = arg.body;
let number = "";
for (let i = 0; i < group.length; i++) {
const node = assertNodeType(group[i], "textord");
number += node.text;
}
let code = parseInt(number);
let text;
if (isNaN(code)) {
throw new ParseError(`\\@char has non-numeric argument ${number}`);
// If we drop IE support, the following code could be replaced with
// text = String.fromCodePoint(code)
} else if (code < 0 || code >= 0x10ffff) {
throw new ParseError(`\\@char with invalid code point ${number}`);
} else if (code <= 0xffff) {
text = String.fromCharCode(code);
} else { // Astral code point; split into surrogate halves
code -= 0x10000;
text = String.fromCharCode((code >> 10) + 0xd800,
(code & 0x3ff) + 0xdc00);
}
return {
type: "textord",
mode: parser.mode,
text: text,
};
},
});
+87
View File
@@ -0,0 +1,87 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeFragment} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {assertNodeType} from "../parseNode";
import type {AnyParseNode} from "../parseNode";
import type {HtmlBuilder, MathMLBuilder} from "../defineFunction";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
const htmlBuilder: HtmlBuilder<"color"> = (group, options) => {
const elements = html.buildExpression(
group.body,
options.withColor(group.color),
false
);
// \color isn't supposed to affect the type of the elements it contains.
// To accomplish this, we wrap the results in a fragment, so the inner
// elements will be able to directly interact with their neighbors. For
// example, `\color{red}{2 +} 3` has the same spacing as `2 + 3`
return makeFragment(elements);
};
const mathmlBuilder: MathMLBuilder<"color"> = (group, options) => {
const inner = mml.buildExpression(group.body,
options.withColor(group.color));
const node = new MathNode("mstyle", inner);
node.setAttribute("mathcolor", group.color);
return node;
};
defineFunction({
type: "color",
names: ["\\textcolor"],
props: {
numArgs: 2,
allowedInText: true,
argTypes: ["color", "original"],
},
handler({parser}, args) {
const color = assertNodeType(args[0], "color-token").color;
const body = args[1];
return {
type: "color",
mode: parser.mode,
color,
body: (ordargument(body) as AnyParseNode[]),
};
},
htmlBuilder,
mathmlBuilder,
});
defineFunction({
type: "color",
names: ["\\color"],
props: {
numArgs: 1,
allowedInText: true,
argTypes: ["color"],
},
handler({parser, breakOnTokenText}, args) {
const color = assertNodeType(args[0], "color-token").color;
// Set macro \current@color in current namespace to store the current
// color, mimicking the behavior of color.sty.
// This is currently used just to correctly color a \right
// that follows a \color command.
parser.gullet.macros.set("\\current@color", color);
// Parse out the implicit body that should be colored.
const body: AnyParseNode[] = parser.parseExpression(true, breakOnTokenText);
return {
type: "color",
mode: parser.mode,
color,
body,
};
},
htmlBuilder,
mathmlBuilder,
});
+60
View File
@@ -0,0 +1,60 @@
// Row breaks within tabular environments, and line breaks at top level
import defineFunction from "../defineFunction";
import {makeSpan} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {calculateSize, makeEm} from "../units";
import {assertNodeType} from "../parseNode";
// \DeclareRobustCommand\\{...\@xnewline}
defineFunction({
type: "cr",
names: ["\\\\"],
props: {
numArgs: 0,
numOptionalArgs: 0,
allowedInText: true,
},
handler({parser}, args, optArgs) {
const size = parser.gullet.future().text === "[" ?
parser.parseSizeGroup(true) : null;
const newLine = !parser.settings.displayMode ||
!parser.settings.useStrictBehavior(
"newLineInDisplayMode", "In LaTeX, \\\\ or \\newline " +
"does nothing in display mode");
return {
type: "cr",
mode: parser.mode,
newLine,
size: size && assertNodeType(size, "size").value,
};
},
// The following builders are called only at the top level,
// not within tabular/array environments.
htmlBuilder(group, options) {
const span = makeSpan(["mspace"], [], options);
if (group.newLine) {
span.classes.push("newline");
if (group.size) {
span.style.marginTop =
makeEm(calculateSize(group.size, options));
}
}
return span;
},
mathmlBuilder(group, options) {
const node = new MathNode("mspace");
if (group.newLine) {
node.setAttribute("linebreak", "newline");
if (group.size) {
node.setAttribute("height",
makeEm(calculateSize(group.size, options)));
}
}
return node;
},
});
+212
View File
@@ -0,0 +1,212 @@
import defineFunction from "../defineFunction";
import ParseError from "../ParseError";
import {assertNodeType} from "../parseNode";
import {Token} from "../Token";
import type Parser from "../Parser";
const globalMap: Record<string, string> = {
"\\global": "\\global",
"\\long": "\\\\globallong",
"\\\\globallong": "\\\\globallong",
"\\def": "\\gdef",
"\\gdef": "\\gdef",
"\\edef": "\\xdef",
"\\xdef": "\\xdef",
"\\let": "\\\\globallet",
"\\futurelet": "\\\\globalfuture",
};
const checkControlSequence = (tok: Token): string => {
const name = tok.text;
if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) {
throw new ParseError("Expected a control sequence", tok);
}
return name;
};
const getRHS = (parser: Parser): Token => {
let tok = parser.gullet.popToken();
if (tok.text === "=") { // consume optional equals
tok = parser.gullet.popToken();
if (tok.text === " ") { // consume one optional space
tok = parser.gullet.popToken();
}
}
return tok;
};
const letCommand = (parser: Parser, name: string, tok: Token, global: boolean) => {
let macro = parser.gullet.macros.get(tok.text);
if (macro == null) {
// don't expand it later even if a macro with the same name is defined
// e.g., \let\foo=\frac \def\frac{\relax} \frac12
tok.noexpand = true;
macro = {
tokens: [tok],
numArgs: 0,
// reproduce the same behavior in expansion
unexpandable: !parser.gullet.isExpandable(tok.text),
};
}
parser.gullet.macros.set(name, macro, global);
};
// <assignment> -> <non-macro assignment>|<macro assignment>
// <non-macro assignment> -> <simple assignment>|\global<non-macro assignment>
// <macro assignment> -> <definition>|<prefix><macro assignment>
// <prefix> -> \global|\long|\outer
defineFunction({
type: "internal",
names: [
"\\global", "\\long",
"\\\\globallong", // cant be entered directly
],
props: {
numArgs: 0,
allowedInText: true,
},
handler({parser, funcName}) {
parser.consumeSpaces();
const token = parser.fetch();
if (globalMap[token.text]) {
// KaTeX doesn't have \par, so ignore \long
if (funcName === "\\global" || funcName === "\\\\globallong") {
token.text = globalMap[token.text];
}
return assertNodeType(parser.parseFunction(), "internal");
}
throw new ParseError(`Invalid token after macro prefix`, token);
},
});
// Basic support for macro definitions: \def, \gdef, \edef, \xdef
// <definition> -> <def><control sequence><definition text>
// <def> -> \def|\gdef|\edef|\xdef
// <definition text> -> <parameter text><left brace><balanced text><right brace>
defineFunction({
type: "internal",
names: ["\\def", "\\gdef", "\\edef", "\\xdef"],
props: {
numArgs: 0,
allowedInText: true,
primitive: true,
},
handler({parser, funcName}) {
let tok = parser.gullet.popToken();
const name = tok.text;
if (/^(?:[\\{}$&#^_]|EOF)$/.test(name)) {
throw new ParseError("Expected a control sequence", tok);
}
let numArgs = 0;
let insert: Token | undefined;
const delimiters: string[][] = [[]];
// <parameter text> contains no braces
while (parser.gullet.future().text !== "{") {
tok = parser.gullet.popToken();
if (tok.text === "#") {
// If the very last character of the <parameter text> is #, so that
// this # is immediately followed by {, TeX will behave as if the {
// had been inserted at the right end of both the parameter text
// and the replacement text.
if (parser.gullet.future().text === "{") {
insert = parser.gullet.future();
delimiters[numArgs].push("{");
break;
}
// A parameter, the first appearance of # must be followed by 1,
// the next by 2, and so on; up to nine #s are allowed
tok = parser.gullet.popToken();
if (!(/^[1-9]$/.test(tok.text))) {
throw new ParseError(`Invalid argument number "${tok.text}"`);
}
if (parseInt(tok.text) !== numArgs + 1) {
throw new ParseError(
`Argument number "${tok.text}" out of order`);
}
numArgs++;
delimiters.push([]);
} else if (tok.text === "EOF") {
throw new ParseError("Expected a macro definition");
} else {
delimiters[numArgs].push(tok.text);
}
}
// replacement text, enclosed in '{' and '}' and properly nested
let {tokens} = parser.gullet.consumeArg();
if (insert) {
tokens.unshift(insert);
}
if (funcName === "\\edef" || funcName === "\\xdef") {
tokens = parser.gullet.expandTokens(tokens);
tokens.reverse(); // to fit in with stack order
}
// Final arg is the expansion of the macro
parser.gullet.macros.set(name, {
tokens,
numArgs,
delimiters,
}, funcName === globalMap[funcName]);
return {
type: "internal",
mode: parser.mode,
};
},
});
// <simple assignment> -> <let assignment>
// <let assignment> -> \futurelet<control sequence><token><token>
// | \let<control sequence><equals><one optional space><token>
// <equals> -> <optional spaces>|<optional spaces>=
defineFunction({
type: "internal",
names: [
"\\let",
"\\\\globallet", // cant be entered directly
],
props: {
numArgs: 0,
allowedInText: true,
primitive: true,
},
handler({parser, funcName}) {
const name = checkControlSequence(parser.gullet.popToken());
parser.gullet.consumeSpaces();
const tok = getRHS(parser);
letCommand(parser, name, tok, funcName === "\\\\globallet");
return {
type: "internal",
mode: parser.mode,
};
},
});
// ref: https://www.tug.org/TUGboat/tb09-3/tb22bechtolsheim.pdf
defineFunction({
type: "internal",
names: [
"\\futurelet",
"\\\\globalfuture", // cant be entered directly
],
props: {
numArgs: 0,
allowedInText: true,
primitive: true,
},
handler({parser, funcName}) {
const name = checkControlSequence(parser.gullet.popToken());
const middle = parser.gullet.popToken();
const tok = parser.gullet.popToken();
letCommand(parser, name, tok, funcName === "\\\\globalfuture");
parser.gullet.pushToken(tok);
parser.gullet.pushToken(middle);
return {
type: "internal",
mode: parser.mode,
};
},
});
+360
View File
@@ -0,0 +1,360 @@
import {makeSpan} from "../buildCommon";
import defineFunction from "../defineFunction";
import {makeLeftRightDelim, makeSizedDelim, sizeToMaxHeight} from "../delimiter";
import {MathNode} from "../mathMLTree";
import ParseError from "../ParseError";
import {assertNodeType, checkSymbolNodeType} from "../parseNode";
import {makeEm} from "../units";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type Options from "../Options";
import type {AnyParseNode, ParseNode, SymbolParseNode} from "../parseNode";
import type {FunctionContext} from "../defineFunction";
// Extra data needed for the delimiter handler down below
const delimiterSizes: Record<string, {
mclass: "mopen" | "mclose" | "mrel" | "mord";
size: 1 | 2 | 3 | 4;
}> = {
"\\bigl" : {mclass: "mopen", size: 1},
"\\Bigl" : {mclass: "mopen", size: 2},
"\\biggl": {mclass: "mopen", size: 3},
"\\Biggl": {mclass: "mopen", size: 4},
"\\bigr" : {mclass: "mclose", size: 1},
"\\Bigr" : {mclass: "mclose", size: 2},
"\\biggr": {mclass: "mclose", size: 3},
"\\Biggr": {mclass: "mclose", size: 4},
"\\bigm" : {mclass: "mrel", size: 1},
"\\Bigm" : {mclass: "mrel", size: 2},
"\\biggm": {mclass: "mrel", size: 3},
"\\Biggm": {mclass: "mrel", size: 4},
"\\big" : {mclass: "mord", size: 1},
"\\Big" : {mclass: "mord", size: 2},
"\\bigg" : {mclass: "mord", size: 3},
"\\Bigg" : {mclass: "mord", size: 4},
};
const delimiters = new Set([
"(", "\\lparen", ")", "\\rparen",
"[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\u230a", "\u230b",
"\\lceil", "\\rceil", "\u2308", "\u2309",
"<", ">", "\\langle", "\u27e8", "\\rangle", "\u27e9", "\\lt", "\\gt",
"\\lvert", "\\rvert", "\\lVert", "\\rVert",
"\\lgroup", "\\rgroup", "\u27ee", "\u27ef",
"\\lmoustache", "\\rmoustache", "\u23b0", "\u23b1",
"/", "\\backslash",
"|", "\\vert", "\\|", "\\Vert",
"\\uparrow", "\\Uparrow",
"\\downarrow", "\\Downarrow",
"\\updownarrow", "\\Updownarrow",
".",
]);
type IsMiddle = {delim: string, options: Options};
// Delimiter functions
function checkDelimiter(
delim: AnyParseNode,
context: FunctionContext,
): SymbolParseNode {
const symDelim = checkSymbolNodeType(delim);
if (symDelim && delimiters.has(symDelim.text)) {
return symDelim;
} else if (symDelim) {
throw new ParseError(
`Invalid delimiter '${symDelim.text}' after '${context.funcName}'`,
delim);
} else {
throw new ParseError(`Invalid delimiter type '${delim.type}'`, delim);
}
}
defineFunction({
type: "delimsizing",
names: [
"\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
"\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
"\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
"\\big", "\\Big", "\\bigg", "\\Bigg",
],
props: {
numArgs: 1,
argTypes: ["primitive"],
},
handler: (context, args) => {
const delim = checkDelimiter(args[0], context);
return {
type: "delimsizing",
mode: context.parser.mode,
size: delimiterSizes[context.funcName].size,
mclass: delimiterSizes[context.funcName].mclass,
delim: delim.text,
};
},
htmlBuilder: (group, options) => {
if (group.delim === ".") {
// Empty delimiters still count as elements, even though they don't
// show anything.
return makeSpan([group.mclass]);
}
return makeSizedDelim(
group.delim, group.size, options, group.mode, [group.mclass]);
},
mathmlBuilder: (group) => {
const children = [];
if (group.delim !== ".") {
children.push(mml.makeText(group.delim, group.mode));
}
const node = new MathNode("mo", children);
if (group.mclass === "mopen" ||
group.mclass === "mclose") {
// Only some of the delimsizing functions act as fences, and they
// return "mopen" or "mclose" mclass.
node.setAttribute("fence", "true");
} else {
// Explicitly disable fencing if it's not a fence, to override the
// defaults.
node.setAttribute("fence", "false");
}
node.setAttribute("stretchy", "true");
const size = makeEm(sizeToMaxHeight[group.size]);
node.setAttribute("minsize", size);
node.setAttribute("maxsize", size);
return node;
},
});
function assertParsed(group: ParseNode<"leftright">) {
if (!group.body) {
throw new Error("Bug: The leftright ParseNode wasn't fully parsed.");
}
}
defineFunction({
type: "leftright-right",
names: ["\\right"],
props: {
numArgs: 1,
primitive: true,
},
handler: (context, args) => {
// \left case below triggers parsing of \right in
// `const right = parser.parseFunction();`
// uses this return value.
const color = context.parser.gullet.macros.get("\\current@color");
if (color && typeof color !== "string") {
throw new ParseError(
"\\current@color set to non-string in \\right");
}
return {
type: "leftright-right",
mode: context.parser.mode,
delim: checkDelimiter(args[0], context).text,
color: color as string | null | undefined, // undefined if not set via \color
};
},
});
defineFunction({
type: "leftright",
names: ["\\left"],
props: {
numArgs: 1,
primitive: true,
},
handler: (context, args) => {
const delim = checkDelimiter(args[0], context);
const parser = context.parser;
// Parse out the implicit body
++parser.leftrightDepth;
// parseExpression stops before '\\right'
const body = parser.parseExpression(false);
--parser.leftrightDepth;
// Check the next token
parser.expect("\\right", false);
const right = assertNodeType(parser.parseFunction(), "leftright-right");
return {
type: "leftright",
mode: parser.mode,
body,
left: delim.text,
right: right.delim,
rightColor: right.color,
};
},
htmlBuilder: (group, options) => {
assertParsed(group);
// Build the inner expression
const inner = html.buildExpression(group.body, options, true,
["mopen", "mclose"]);
let innerHeight = 0;
let innerDepth = 0;
let hadMiddle = false;
// Calculate its height and depth
for (let i = 0; i < inner.length; i++) {
// Property `isMiddle` not defined on `span`. See comment in
// "middle"'s htmlBuilder.
// TODO(ts)
if ((inner[i] as any).isMiddle) {
hadMiddle = true;
} else {
innerHeight = Math.max(inner[i].height, innerHeight);
innerDepth = Math.max(inner[i].depth, innerDepth);
}
}
// The size of delimiters is the same, regardless of what style we are
// in. Thus, to correctly calculate the size of delimiter we need around
// a group, we scale down the inner size based on the size.
innerHeight *= options.sizeMultiplier;
innerDepth *= options.sizeMultiplier;
let leftDelim;
if (group.left === ".") {
// Empty delimiters in \left and \right make null delimiter spaces.
leftDelim = html.makeNullDelimiter(options, ["mopen"]);
} else {
// Otherwise, use leftRightDelim to generate the correct sized
// delimiter.
leftDelim = makeLeftRightDelim(
group.left, innerHeight, innerDepth, options,
group.mode, ["mopen"]);
}
// Add it to the beginning of the expression
inner.unshift(leftDelim);
// Handle middle delimiters
if (hadMiddle) {
for (let i = 1; i < inner.length; i++) {
const middleDelim = inner[i];
// Property `isMiddle` not defined on `span`. See comment in
// "middle"'s htmlBuilder.
// TODO(ts)
const isMiddle: IsMiddle = (middleDelim as any).isMiddle;
if (isMiddle) {
// Apply the options that were active when \middle was called
inner[i] = makeLeftRightDelim(
isMiddle.delim, innerHeight, innerDepth,
isMiddle.options, group.mode, []);
}
}
}
let rightDelim;
// Same for the right delimiter, but using color specified by \color
if (group.right === ".") {
rightDelim = html.makeNullDelimiter(options, ["mclose"]);
} else {
const colorOptions = group.rightColor ?
options.withColor(group.rightColor) : options;
rightDelim = makeLeftRightDelim(
group.right, innerHeight, innerDepth, colorOptions,
group.mode, ["mclose"]);
}
// Add it to the end of the expression.
inner.push(rightDelim);
return makeSpan(["minner"], inner, options);
},
mathmlBuilder: (group, options) => {
assertParsed(group);
const inner = mml.buildExpression(group.body, options);
if (group.left !== ".") {
const leftNode = new MathNode(
"mo", [mml.makeText(group.left, group.mode)]);
leftNode.setAttribute("fence", "true");
inner.unshift(leftNode);
}
if (group.right !== ".") {
const rightNode = new MathNode(
"mo", [mml.makeText(group.right, group.mode)]);
rightNode.setAttribute("fence", "true");
if (group.rightColor) {
rightNode.setAttribute("mathcolor", group.rightColor);
}
inner.push(rightNode);
}
return mml.makeRow(inner);
},
});
defineFunction({
type: "middle",
names: ["\\middle"],
props: {
numArgs: 1,
primitive: true,
},
handler: (context, args) => {
const delim = checkDelimiter(args[0], context);
if (!context.parser.leftrightDepth) {
throw new ParseError("\\middle without preceding \\left", delim);
}
return {
type: "middle",
mode: context.parser.mode,
delim: delim.text,
};
},
htmlBuilder: (group, options) => {
let middleDelim;
if (group.delim === ".") {
middleDelim = html.makeNullDelimiter(options, []);
} else {
middleDelim = makeSizedDelim(
group.delim, 1, options,
group.mode, []);
const isMiddle: IsMiddle = {delim: group.delim, options};
// Property `isMiddle` not defined on `span`. It is only used in
// this file above.
// TODO: Fix this violation of the `span` type and possibly rename
// things since `isMiddle` sounds like a boolean, but is a struct.
// TODO(ts)
(middleDelim as any).isMiddle = isMiddle;
}
return middleDelim;
},
mathmlBuilder: (group, options) => {
// A Firefox \middle will stretch a character vertically only if it
// is in the fence part of the operator dictionary at:
// https://www.w3.org/TR/MathML3/appendixc.html.
// So we need to avoid U+2223 and use plain "|" instead.
const textNode = (group.delim === "\\vert" || group.delim === "|")
? mml.makeText("|", "text")
: mml.makeText(group.delim, group.mode);
const middleNode = new MathNode("mo", [textNode]);
middleNode.setAttribute("fence", "true");
// MathML gives 5/18em spacing to each <mo> element.
// \middle should get delimiter spacing instead.
middleNode.setAttribute("lspace", "0.05em");
middleNode.setAttribute("rspace", "0.05em");
return middleNode;
},
});
+345
View File
@@ -0,0 +1,345 @@
import defineFunction from "../defineFunction";
import {makeSpan, makeSvgSpan, makeVList, wrapFragment} from "../buildCommon";
import {isCharacterBox} from "../utils";
import {MathNode} from "../mathMLTree";
import {stretchyEnclose} from "../stretchy";
import {phasePath} from "../svgGeometry";
import {PathNode, SvgNode} from "../domTree";
import {calculateSize, makeEm} from "../units";
import {assertNodeType} from "../parseNode";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {HtmlBuilder, MathMLBuilder} from "../defineFunction";
const htmlBuilder: HtmlBuilder<"enclose"> = (group, options) => {
// \cancel, \bcancel, \xcancel, \sout, \fbox, \colorbox, \fcolorbox, \phase
// Some groups can return document fragments. Handle those by wrapping
// them in a span.
const inner = wrapFragment(
html.buildGroup(group.body, options), options);
const label = group.label.slice(1);
let scale = options.sizeMultiplier;
let img;
let imgShift = 0;
// In the LaTeX cancel package, line geometry is slightly different
// depending on whether the subject is wider than it is tall, or vice versa.
// We don't know the width of a group, so as a proxy, we test if
// the subject is a single character. This captures most of the
// subjects that should get the "tall" treatment.
const isSingleChar = isCharacterBox(group.body);
if (label === "sout") {
img = makeSpan(["stretchy", "sout"]);
img.height = options.fontMetrics().defaultRuleThickness / scale;
imgShift = -0.5 * options.fontMetrics().xHeight;
} else if (label === "phase") {
// Set a couple of dimensions from the steinmetz package.
const lineWeight = calculateSize({number: 0.6, unit: "pt"}, options);
const clearance = calculateSize({number: 0.35, unit: "ex"}, options);
// Prevent size changes like \Huge from affecting line thickness
const newOptions = options.havingBaseSizing();
scale = scale / newOptions.sizeMultiplier;
const angleHeight = inner.height + inner.depth + lineWeight + clearance;
// Reserve a left pad for the angle.
inner.style.paddingLeft = makeEm(angleHeight / 2 + lineWeight);
// Create an SVG
const viewBoxHeight = Math.floor(1000 * angleHeight * scale);
const path = phasePath(viewBoxHeight);
const svgNode = new SvgNode([new PathNode("phase", path)], {
"width": "400em",
"height": makeEm(viewBoxHeight / 1000),
"viewBox": `0 0 400000 ${viewBoxHeight}`,
"preserveAspectRatio": "xMinYMin slice",
});
// Wrap it in a span with overflow: hidden.
img = makeSvgSpan(["hide-tail"], [svgNode], options);
img.style.height = makeEm(angleHeight);
imgShift = inner.depth + lineWeight + clearance;
} else {
// Add horizontal padding
if (/cancel/.test(label)) {
if (!isSingleChar) {
inner.classes.push("cancel-pad");
}
} else if (label === "angl") {
inner.classes.push("anglpad");
} else {
inner.classes.push("boxpad");
}
// Add vertical padding
let topPad = 0;
let bottomPad = 0;
let ruleThickness = 0;
// ref: cancel package: \advance\totalheight2\p@ % "+2"
if (/box/.test(label)) {
ruleThickness = Math.max(
options.fontMetrics().fboxrule, // default
options.minRuleThickness, // User override.
);
topPad = options.fontMetrics().fboxsep +
(label === "colorbox" ? 0 : ruleThickness);
bottomPad = topPad;
} else if (label === "angl") {
ruleThickness = Math.max(
options.fontMetrics().defaultRuleThickness,
options.minRuleThickness
);
topPad = 4 * ruleThickness; // gap = 3 × line, plus the line itself.
bottomPad = Math.max(0, 0.25 - inner.depth);
} else {
topPad = isSingleChar ? 0.2 : 0;
bottomPad = topPad;
}
img = stretchyEnclose(inner, label, topPad, bottomPad, options);
if (/fbox|boxed|fcolorbox/.test(label)) {
img.style.borderStyle = "solid";
img.style.borderWidth = makeEm(ruleThickness);
} else if (label === "angl" && ruleThickness !== 0.049) {
img.style.borderTopWidth = makeEm(ruleThickness);
img.style.borderRightWidth = makeEm(ruleThickness);
}
imgShift = inner.depth + bottomPad;
if (group.backgroundColor) {
img.style.backgroundColor = group.backgroundColor;
if (group.borderColor) {
img.style.borderColor = group.borderColor;
}
}
}
let vlist;
if (group.backgroundColor) {
vlist = makeVList({
positionType: "individualShift",
children: [
// Put the color background behind inner;
{type: "elem", elem: img, shift: imgShift},
{type: "elem", elem: inner, shift: 0},
],
}, options);
} else {
const classes = /cancel|phase/.test(label) ? ["svg-align"] : [];
vlist = makeVList({
positionType: "individualShift",
children: [
// Write the \cancel stroke on top of inner.
{
type: "elem",
elem: inner,
shift: 0,
},
{
type: "elem",
elem: img,
shift: imgShift,
wrapperClasses: classes,
},
],
}, options);
}
if (/cancel/.test(label)) {
// The cancel package documentation says that cancel lines add their height
// to the expression, but tests show that isn't how it actually works.
vlist.height = inner.height;
vlist.depth = inner.depth;
}
if (/cancel/.test(label) && !isSingleChar) {
// cancel does not create horiz space for its line extension.
return makeSpan(["mord", "cancel-lap"], [vlist], options);
} else {
return makeSpan(["mord"], [vlist], options);
}
};
const mathmlBuilder: MathMLBuilder<"enclose"> = (group, options) => {
let fboxsep = 0;
const node = new MathNode(
group.label.includes("colorbox") ? "mpadded" : "menclose",
[mml.buildGroup(group.body, options)]
);
switch (group.label) {
case "\\cancel":
node.setAttribute("notation", "updiagonalstrike");
break;
case "\\bcancel":
node.setAttribute("notation", "downdiagonalstrike");
break;
case "\\phase":
node.setAttribute("notation", "phasorangle");
break;
case "\\sout":
node.setAttribute("notation", "horizontalstrike");
break;
case "\\fbox":
node.setAttribute("notation", "box");
break;
case "\\angl":
node.setAttribute("notation", "actuarial");
break;
case "\\fcolorbox":
case "\\colorbox":
// <menclose> doesn't have a good notation option. So use <mpadded>
// instead. Set some attributes that come included with <menclose>.
fboxsep = options.fontMetrics().fboxsep *
options.fontMetrics().ptPerEm;
node.setAttribute("width", `+${2 * fboxsep}pt`);
node.setAttribute("height", `+${2 * fboxsep}pt`);
node.setAttribute("lspace", `${fboxsep}pt`); //
node.setAttribute("voffset", `${fboxsep}pt`);
if (group.label === "\\fcolorbox") {
const thk = Math.max(
options.fontMetrics().fboxrule, // default
options.minRuleThickness, // user override
);
node.setAttribute("style", `border: ${makeEm(thk)} solid ${group.borderColor}`);
}
break;
case "\\xcancel":
node.setAttribute("notation", "updiagonalstrike downdiagonalstrike");
break;
}
if (group.backgroundColor) {
node.setAttribute("mathbackground", group.backgroundColor);
}
return node;
};
defineFunction({
type: "enclose",
names: ["\\colorbox"],
props: {
numArgs: 2,
allowedInText: true,
argTypes: ["color", "text"],
},
handler({parser, funcName}, args, optArgs) {
const color = assertNodeType(args[0], "color-token").color;
const body = args[1];
return {
type: "enclose",
mode: parser.mode,
label: funcName,
backgroundColor: color,
body,
};
},
htmlBuilder,
mathmlBuilder,
});
defineFunction({
type: "enclose",
names: ["\\fcolorbox"],
props: {
numArgs: 3,
allowedInText: true,
argTypes: ["color", "color", "text"],
},
handler({parser, funcName}, args, optArgs) {
const borderColor = assertNodeType(args[0], "color-token").color;
const backgroundColor = assertNodeType(args[1], "color-token").color;
const body = args[2];
return {
type: "enclose",
mode: parser.mode,
label: funcName,
backgroundColor,
borderColor,
body,
};
},
htmlBuilder,
mathmlBuilder,
});
defineFunction({
type: "enclose",
names: ["\\fbox"],
props: {
numArgs: 1,
argTypes: ["hbox"],
allowedInText: true,
},
handler({parser}, args) {
return {
type: "enclose",
mode: parser.mode,
label: "\\fbox",
body: args[0],
};
},
});
defineFunction({
type: "enclose",
names: ["\\cancel", "\\bcancel", "\\xcancel", "\\phase"],
props: {
numArgs: 1,
},
handler({parser, funcName}, args) {
const body = args[0];
return {
type: "enclose",
mode: parser.mode,
label: funcName,
body,
};
},
htmlBuilder,
mathmlBuilder,
});
defineFunction({
type: "enclose",
names: ["\\sout"],
props: {
numArgs: 1,
allowedInText: true,
},
handler({parser, funcName}, args) {
if (parser.mode === "math") {
parser.settings.reportNonstrict("mathVsSout",
`LaTeX's \\sout works only in text mode`);
}
const body = args[0];
return {
type: "enclose",
mode: parser.mode,
label: funcName,
body,
};
},
htmlBuilder,
mathmlBuilder,
});
defineFunction({
type: "enclose",
names: ["\\angl"],
props: {
numArgs: 1,
argTypes: ["hbox"],
allowedInText: false,
},
handler({parser}, args) {
return {
type: "enclose",
mode: parser.mode,
label: "\\angl",
body: args[0],
};
},
});
+61
View File
@@ -0,0 +1,61 @@
import defineFunction from "../defineFunction";
import ParseError from "../ParseError";
import {assertNodeType} from "../parseNode";
import environments from "../environments";
// Environment delimiters. HTML/MathML rendering is defined in the corresponding
// defineEnvironment definitions.
defineFunction({
type: "environment",
names: ["\\begin", "\\end"],
props: {
numArgs: 1,
argTypes: ["text"],
},
handler({parser, funcName}, args) {
const nameGroup = args[0];
if (nameGroup.type !== "ordgroup") {
throw new ParseError("Invalid environment name", nameGroup);
}
let envName = "";
for (let i = 0; i < nameGroup.body.length; ++i) {
envName += assertNodeType(nameGroup.body[i], "textord").text;
}
if (funcName === "\\begin") {
// begin...end is similar to left...right
if (!environments.hasOwnProperty(envName)) {
throw new ParseError(
"No such environment: " + envName, nameGroup);
}
// Build the environment object. Arguments and other information will
// be made available to the begin and end methods using properties.
const env = environments[envName];
const {args, optArgs} =
parser.parseArguments("\\begin{" + envName + "}", env);
const context = {
mode: parser.mode,
envName,
parser,
};
const result = env.handler(context, args, optArgs);
parser.expect("\\end", false);
const endNameToken = parser.nextToken;
const end = assertNodeType(parser.parseFunction(), "environment");
if (end.name !== envName) {
throw new ParseError(
`Mismatch: \\begin{${envName}} matched by \\end{${end.name}}`,
endNameToken);
}
// TODO(ts), "environment" handler returns an environment ParseNode
return result as any;
}
return {
type: "environment",
mode: parser.mode,
name: envName,
nameGroup,
};
},
});
+119
View File
@@ -0,0 +1,119 @@
// TODO(kevinb): implement \\sl and \\sc
import {binrelClass} from "./mclass";
import defineFunction, {normalizeArgument} from "../defineFunction";
import {isCharacterBox} from "../utils";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type Options from "../Options";
import type {ParseNode} from "../parseNode";
const htmlBuilder = (group: ParseNode<"font">, options: Options) => {
const font = group.font;
const newOptions = options.withFont(font);
return html.buildGroup(group.body, newOptions);
};
const mathmlBuilder = (group: ParseNode<"font">, options: Options) => {
const font = group.font;
const newOptions = options.withFont(font);
return mml.buildGroup(group.body, newOptions);
};
const fontAliases: Record<string, string> = {
"\\Bbb": "\\mathbb",
"\\bold": "\\mathbf",
"\\frak": "\\mathfrak",
"\\bm": "\\boldsymbol",
};
defineFunction({
type: "font",
names: [
// styles, except \boldsymbol defined below
"\\mathrm", "\\mathit", "\\mathbf", "\\mathnormal", "\\mathsfit",
// families
"\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf",
"\\mathtt",
// aliases, except \bm defined below
"\\Bbb", "\\bold", "\\frak",
],
props: {
numArgs: 1,
allowedInArgument: true,
},
handler: ({parser, funcName}, args) => {
const body = normalizeArgument(args[0]);
let func = funcName;
if (func in fontAliases) {
func = fontAliases[func];
}
return {
type: "font",
mode: parser.mode,
font: func.slice(1),
body,
};
},
htmlBuilder,
mathmlBuilder,
});
defineFunction({
type: "mclass",
names: ["\\boldsymbol", "\\bm"],
props: {
numArgs: 1,
},
handler: ({parser}, args) => {
const body = args[0];
// amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the
// argument's bin|rel|ord status
return {
type: "mclass",
mode: parser.mode,
mclass: binrelClass(body),
body: [
{
type: "font",
mode: parser.mode,
font: "boldsymbol",
body,
},
],
isCharacterBox: isCharacterBox(body),
};
},
});
// Old font changing functions
defineFunction({
type: "font",
names: ["\\rm", "\\sf", "\\tt", "\\bf", "\\it", "\\cal"],
props: {
numArgs: 0,
allowedInText: true,
},
handler: ({parser, funcName, breakOnTokenText}, args) => {
const {mode} = parser;
const body = parser.parseExpression(true, breakOnTokenText);
const style = `math${funcName.slice(1)}`;
return {
type: "font",
mode: mode,
font: style,
body: {
type: "ordgroup",
mode: parser.mode,
body,
},
};
},
htmlBuilder,
mathmlBuilder,
});
+470
View File
@@ -0,0 +1,470 @@
import defineFunction, {normalizeArgument} from "../defineFunction";
import {makeLineSpan, makeSpan, makeVList} from "../buildCommon";
import {makeCustomSizedDelim} from "../delimiter";
import {MathNode, TextNode} from "../mathMLTree";
import type {ParseNode} from "../parseNode";
import Style from "../Style";
import {assertNodeType} from "../parseNode";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import {calculateSize, makeEm} from "../units";
import type {StyleStr} from "../types";
import type {HtmlBuilder, MathMLBuilder} from "../defineFunction";
const htmlBuilder: HtmlBuilder<"genfrac"> = (group, options) => {
// Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
const style = options.style;
const nstyle = style.fracNum();
const dstyle = style.fracDen();
let newOptions;
newOptions = options.havingStyle(nstyle);
const numerm = html.buildGroup(group.numer, newOptions, options);
if (group.continued) {
// \cfrac inserts a \strut into the numerator.
// Get \strut dimensions from TeXbook page 353.
const hStrut = 8.5 / options.fontMetrics().ptPerEm;
const dStrut = 3.5 / options.fontMetrics().ptPerEm;
numerm.height = numerm.height < hStrut ? hStrut : numerm.height;
numerm.depth = numerm.depth < dStrut ? dStrut : numerm.depth;
}
newOptions = options.havingStyle(dstyle);
const denomm = html.buildGroup(group.denom, newOptions, options);
let rule;
let ruleWidth;
let ruleSpacing;
if (group.hasBarLine) {
if (group.barSize) {
ruleWidth = calculateSize(group.barSize, options);
rule = makeLineSpan("frac-line", options, ruleWidth);
} else {
rule = makeLineSpan("frac-line", options);
}
ruleWidth = rule.height;
ruleSpacing = rule.height;
} else {
rule = null;
ruleWidth = 0;
ruleSpacing = options.fontMetrics().defaultRuleThickness;
}
// Rule 15b
let numShift;
let clearance;
let denomShift;
if (style.size === Style.DISPLAY.size) {
numShift = options.fontMetrics().num1;
if (ruleWidth > 0) {
clearance = 3 * ruleSpacing;
} else {
clearance = 7 * ruleSpacing;
}
denomShift = options.fontMetrics().denom1;
} else {
if (ruleWidth > 0) {
numShift = options.fontMetrics().num2;
clearance = ruleSpacing;
} else {
numShift = options.fontMetrics().num3;
clearance = 3 * ruleSpacing;
}
denomShift = options.fontMetrics().denom2;
}
let frac;
if (!rule) {
// Rule 15c
const candidateClearance =
(numShift - numerm.depth) - (denomm.height - denomShift);
if (candidateClearance < clearance) {
numShift += 0.5 * (clearance - candidateClearance);
denomShift += 0.5 * (clearance - candidateClearance);
}
frac = makeVList({
positionType: "individualShift",
children: [
{type: "elem", elem: denomm, shift: denomShift},
{type: "elem", elem: numerm, shift: -numShift},
],
}, options);
} else {
// Rule 15d
const axisHeight = options.fontMetrics().axisHeight;
if ((numShift - numerm.depth) - (axisHeight + 0.5 * ruleWidth) <
clearance) {
numShift +=
clearance - ((numShift - numerm.depth) -
(axisHeight + 0.5 * ruleWidth));
}
if ((axisHeight - 0.5 * ruleWidth) - (denomm.height - denomShift) <
clearance) {
denomShift +=
clearance - ((axisHeight - 0.5 * ruleWidth) -
(denomm.height - denomShift));
}
const midShift = -(axisHeight - 0.5 * ruleWidth);
frac = makeVList({
positionType: "individualShift",
children: [
{type: "elem", elem: denomm, shift: denomShift},
{type: "elem", elem: rule, shift: midShift},
{type: "elem", elem: numerm, shift: -numShift},
],
}, options);
}
// Since we manually change the style sometimes (with \dfrac or \tfrac),
// account for the possible size change here.
newOptions = options.havingStyle(style);
frac.height *= newOptions.sizeMultiplier / options.sizeMultiplier;
frac.depth *= newOptions.sizeMultiplier / options.sizeMultiplier;
// Rule 15e
let delimSize;
if (style.size === Style.DISPLAY.size) {
delimSize = options.fontMetrics().delim1;
} else if (style.size === Style.SCRIPTSCRIPT.size) {
delimSize = options.havingStyle(Style.SCRIPT).fontMetrics().delim2;
} else {
delimSize = options.fontMetrics().delim2;
}
let leftDelim;
let rightDelim;
if (group.leftDelim == null) {
leftDelim = html.makeNullDelimiter(options, ["mopen"]);
} else {
leftDelim = makeCustomSizedDelim(
group.leftDelim, delimSize, true,
options.havingStyle(style), group.mode, ["mopen"]);
}
if (group.continued) {
rightDelim = makeSpan([]); // zero width for \cfrac
} else if (group.rightDelim == null) {
rightDelim = html.makeNullDelimiter(options, ["mclose"]);
} else {
rightDelim = makeCustomSizedDelim(
group.rightDelim, delimSize, true,
options.havingStyle(style), group.mode, ["mclose"]);
}
return makeSpan(
["mord"].concat(newOptions.sizingClasses(options)),
[leftDelim, makeSpan(["mfrac"], [frac]), rightDelim],
options);
};
const mathmlBuilder: MathMLBuilder<"genfrac"> = (group, options) => {
const node = new MathNode(
"mfrac",
[
mml.buildGroup(group.numer, options),
mml.buildGroup(group.denom, options),
]);
if (!group.hasBarLine) {
node.setAttribute("linethickness", "0px");
} else if (group.barSize) {
const ruleWidth = calculateSize(group.barSize, options);
node.setAttribute("linethickness", makeEm(ruleWidth));
}
if (group.leftDelim != null || group.rightDelim != null) {
const withDelims = [];
if (group.leftDelim != null) {
const leftOp = new MathNode(
"mo",
[new TextNode(group.leftDelim.replace("\\", ""))]
);
leftOp.setAttribute("fence", "true");
withDelims.push(leftOp);
}
withDelims.push(node);
if (group.rightDelim != null) {
const rightOp = new MathNode(
"mo",
[new TextNode(group.rightDelim.replace("\\", ""))]
);
rightOp.setAttribute("fence", "true");
withDelims.push(rightOp);
}
return mml.makeRow(withDelims);
}
return node;
};
const wrapWithStyle = (
frac: ParseNode<"genfrac">,
style?: StyleStr | null,
): ParseNode<"genfrac"> => {
if (!style) {
return frac;
}
const wrapper: ParseNode<"styling"> = {
type: "styling",
mode: frac.mode,
style,
body: [frac],
};
// @ts-ignore defineFunction handler needs to return ParseNode<"genfrac">
return wrapper;
};
defineFunction({
type: "genfrac",
names: [
"\\cfrac", "\\dfrac", "\\frac", "\\tfrac",
"\\dbinom", "\\binom", "\\tbinom",
"\\\\atopfrac", // cant be entered directly
"\\\\bracefrac", "\\\\brackfrac", // ditto
],
props: {
numArgs: 2,
allowedInArgument: true,
},
handler: ({parser, funcName}, args) => {
const numer = args[0];
const denom = args[1];
let hasBarLine: boolean;
let leftDelim: string | null = null;
let rightDelim: string | null = null;
switch (funcName) {
case "\\cfrac":
case "\\dfrac":
case "\\frac":
case "\\tfrac":
hasBarLine = true;
break;
case "\\\\atopfrac":
hasBarLine = false;
break;
case "\\dbinom":
case "\\binom":
case "\\tbinom":
hasBarLine = false;
leftDelim = "(";
rightDelim = ")";
break;
case "\\\\bracefrac":
hasBarLine = false;
leftDelim = "\\{";
rightDelim = "\\}";
break;
case "\\\\brackfrac":
hasBarLine = false;
leftDelim = "[";
rightDelim = "]";
break;
default:
throw new Error("Unrecognized genfrac command");
}
const continued = funcName === "\\cfrac";
let style = null;
if (continued || funcName.startsWith("\\d")) {
style = "display" as StyleStr;
} else if (funcName.startsWith("\\t")) {
style = "text" as StyleStr;
}
return wrapWithStyle({
type: "genfrac",
mode: parser.mode,
numer,
denom,
continued,
hasBarLine,
leftDelim,
rightDelim,
barSize: null,
}, style);
},
htmlBuilder,
mathmlBuilder,
});
// Infix generalized fractions -- these are not rendered directly, but replaced
// immediately by one of the variants above.
defineFunction({
type: "infix",
names: ["\\over", "\\choose", "\\atop", "\\brace", "\\brack"],
props: {
numArgs: 0,
infix: true,
},
handler({parser, funcName, token}) {
let replaceWith;
switch (funcName) {
case "\\over":
replaceWith = "\\frac";
break;
case "\\choose":
replaceWith = "\\binom";
break;
case "\\atop":
replaceWith = "\\\\atopfrac";
break;
case "\\brace":
replaceWith = "\\\\bracefrac";
break;
case "\\brack":
replaceWith = "\\\\brackfrac";
break;
default:
throw new Error("Unrecognized infix genfrac command");
}
return {
type: "infix",
mode: parser.mode,
replaceWith,
token,
};
},
});
const stylArray: StyleStr[] = ["display", "text", "script", "scriptscript"];
const delimFromValue = function(delimString: string): string | null {
let delim = null;
if (delimString.length > 0) {
delim = delimString;
delim = delim === "." ? null : delim;
}
return delim;
};
defineFunction({
type: "genfrac",
names: ["\\genfrac"],
props: {
numArgs: 6,
allowedInArgument: true,
argTypes: ["math", "math", "size", "text", "math", "math"],
},
handler({parser}, args) {
const numer = args[4];
const denom = args[5];
// Look into the parse nodes to get the desired delimiters.
const leftNode = normalizeArgument(args[0]);
const leftDelim = leftNode.type === "atom" && leftNode.family === "open"
? delimFromValue(leftNode.text) : null;
const rightNode = normalizeArgument(args[1]);
const rightDelim = rightNode.type === "atom" && rightNode.family === "close"
? delimFromValue(rightNode.text) : null;
const barNode = assertNodeType(args[2], "size");
let hasBarLine: boolean;
let barSize = null;
if (barNode.isBlank) {
// \genfrac acts differently than \above.
// \genfrac treats an empty size group as a signal to use a
// standard bar size. \above would see size = 0 and omit the bar.
hasBarLine = true;
} else {
barSize = barNode.value;
hasBarLine = barSize.number > 0;
}
// Find out if we want displaystyle, textstyle, etc.
let size = null;
let styl = args[3];
if (styl.type === "ordgroup") {
if (styl.body.length > 0) {
const textOrd = assertNodeType(styl.body[0], "textord");
size = stylArray[Number(textOrd.text)] as StyleStr;
}
} else {
styl = assertNodeType(styl, "textord");
size = stylArray[Number(styl.text)] as StyleStr;
}
return wrapWithStyle({
type: "genfrac",
mode: parser.mode,
numer,
denom,
continued: false,
hasBarLine,
barSize,
leftDelim,
rightDelim,
}, size);
},
});
// \above is an infix fraction that also defines a fraction bar size.
defineFunction({
type: "infix",
names: ["\\above"],
props: {
numArgs: 1,
argTypes: ["size"],
infix: true,
},
handler({parser, funcName, token}, args) {
return {
type: "infix",
mode: parser.mode,
replaceWith: "\\\\abovefrac",
size: assertNodeType(args[0], "size").value,
token,
};
},
});
defineFunction({
type: "genfrac",
names: ["\\\\abovefrac"],
props: {
numArgs: 3,
argTypes: ["math", "size", "math"],
},
handler: ({parser, funcName}, args) => {
const numer = args[0];
const barSize = assertNodeType(args[1], "infix").size;
if (!barSize) {
throw new Error(
`\\\\abovefrac expected size, but got ${String(barSize)}`);
}
const denom = args[2];
const hasBarLine = barSize.number > 0;
return {
type: "genfrac",
mode: parser.mode,
numer,
denom,
continued: false,
hasBarLine,
barSize,
leftDelim: null,
rightDelim: null,
};
},
});
+38
View File
@@ -0,0 +1,38 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeFragment} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
// \hbox is provided for compatibility with LaTeX \vcenter.
// In LaTeX, \vcenter can act only on a box, as in
// \vcenter{\hbox{$\frac{a+b}{\dfrac{c}{d}}$}}
// This function by itself doesn't do anything but prevent a soft line break.
defineFunction({
type: "hbox",
names: ["\\hbox"],
props: {
numArgs: 1,
argTypes: ["text"],
allowedInText: true,
primitive: true,
},
handler({parser}, args) {
return {
type: "hbox",
mode: parser.mode,
body: ordargument(args[0]),
};
},
htmlBuilder(group, options) {
const elements = html.buildExpression(group.body, options, false);
return makeFragment(elements);
},
mathmlBuilder(group, options) {
return new MathNode(
"mrow", mml.buildExpression(group.body, options)
);
},
});
+136
View File
@@ -0,0 +1,136 @@
import defineFunction from "../defineFunction";
import {makeSpan, makeVList} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {stretchyMathML, stretchySvg} from "../stretchy";
import Style from "../Style";
import {assertNodeType} from "../parseNode";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {HtmlBuilderSupSub, MathMLBuilder} from "../defineFunction";
import type {ParseNode} from "../parseNode";
// NOTE: Unlike most `htmlBuilder`s, this one handles not only "horizBrace", but
// also "supsub" since an over/underbrace can affect super/subscripting.
export const htmlBuilder: HtmlBuilderSupSub<"horizBrace"> = (grp, options) => {
const style = options.style;
// Pull out the `ParseNode<"horizBrace">` if `grp` is a "supsub" node.
let supSubGroup;
let group: ParseNode<"horizBrace">;
if (grp.type === "supsub") {
// Ref: LaTeX source2e: }}}}\limits}
// i.e. LaTeX treats the brace similar to an op and passes it
// with \limits, so we need to assign supsub style.
supSubGroup = grp.sup ?
html.buildGroup(grp.sup, options.havingStyle(style.sup()), options) :
html.buildGroup(grp.sub, options.havingStyle(style.sub()), options);
group = assertNodeType(grp.base, "horizBrace");
} else {
group = assertNodeType(grp, "horizBrace");
}
// Build the base group
const body = html.buildGroup(
group.base, options.havingBaseStyle(Style.DISPLAY));
// Create the stretchy element
const braceBody = stretchySvg(group, options);
// Generate the vlist, with the appropriate kerns ┏━━━━━━━━┓
// This first vlist contains the content and the brace: equation
let vlist;
if (group.isOver) {
vlist = makeVList({
positionType: "firstBaseline",
children: [
{type: "elem", elem: body},
{type: "kern", size: 0.1},
{type: "elem", elem: braceBody},
],
}, options);
// TODO(ts): Replace this with passing "svg-align" into makeVList.
(vlist as any).children[0].children[0].children[1].classes.push("svg-align");
} else {
vlist = makeVList({
positionType: "bottom",
positionData: body.depth + 0.1 + braceBody.height,
children: [
{type: "elem", elem: braceBody},
{type: "kern", size: 0.1},
{type: "elem", elem: body},
],
}, options);
// TODO(ts): Replace this with passing "svg-align" into makeVList.
(vlist as any).children[0].children[0].children[0].classes.push("svg-align");
}
if (supSubGroup) {
// To write the supsub, wrap the first vlist in another vlist:
// They can't all go in the same vlist, because the note might be
// wider than the equation. We want the equation to control the
// brace width.
// note long note long note
// ┏━━━━━━━━┓ or ┏━━━┓ not ┏━━━━━━━━━┓
// equation eqn eqn
const vSpan = makeSpan(
["minner", (group.isOver ? "mover" : "munder")],
[vlist], options);
if (group.isOver) {
vlist = makeVList({
positionType: "firstBaseline",
children: [
{type: "elem", elem: vSpan},
{type: "kern", size: 0.2},
{type: "elem", elem: supSubGroup},
],
}, options);
} else {
vlist = makeVList({
positionType: "bottom",
positionData: vSpan.depth + 0.2 + supSubGroup.height +
supSubGroup.depth,
children: [
{type: "elem", elem: supSubGroup},
{type: "kern", size: 0.2},
{type: "elem", elem: vSpan},
],
}, options);
}
}
return makeSpan(
["minner", (group.isOver ? "mover" : "munder")], [vlist], options);
};
const mathmlBuilder: MathMLBuilder<"horizBrace"> = (group, options) => {
const accentNode = stretchyMathML(group.label);
return new MathNode(
(group.isOver ? "mover" : "munder"),
[mml.buildGroup(group.base, options), accentNode]
);
};
// Horizontal stretchy braces
defineFunction({
type: "horizBrace",
names: ["\\overbrace", "\\underbrace", "\\overbracket", "\\underbracket"],
props: {
numArgs: 1,
},
handler({parser, funcName}, args) {
return {
type: "horizBrace",
mode: parser.mode,
label: funcName,
isOver: funcName.includes("\\over"),
base: args[0],
};
},
htmlBuilder,
mathmlBuilder,
});
+93
View File
@@ -0,0 +1,93 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeAnchor} from "../buildCommon";
import {assertNodeType} from "../parseNode";
import {MathNode} from "../mathMLTree";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {ParseNode} from "../parseNode";
defineFunction({
type: "href",
names: ["\\href"],
props: {
numArgs: 2,
argTypes: ["url", "original"],
allowedInText: true,
},
handler: ({parser}, args) => {
const body = args[1];
const href = assertNodeType(args[0], "url").url;
if (!parser.settings.isTrusted({
command: "\\href",
url: href,
})) {
return parser.formatUnsupportedCmd("\\href");
}
return {
type: "href",
mode: parser.mode,
href,
body: ordargument(body),
};
},
htmlBuilder: (group, options) => {
const elements = html.buildExpression(group.body, options, false);
return makeAnchor(group.href, [], elements, options);
},
mathmlBuilder: (group, options) => {
let math = mml.buildExpressionRow(group.body, options);
if (!(math instanceof MathNode)) {
math = new MathNode("mrow", [math]);
}
(math as MathNode).setAttribute("href", group.href);
return math;
},
});
defineFunction({
type: "href",
names: ["\\url"],
props: {
numArgs: 1,
argTypes: ["url"],
allowedInText: true,
},
handler: ({parser}, args) => {
const href = assertNodeType(args[0], "url").url;
if (!parser.settings.isTrusted({
command: "\\url",
url: href,
})) {
return parser.formatUnsupportedCmd("\\url");
}
const chars: ParseNode<"textord">[] = [];
for (let i = 0; i < href.length; i++) {
let c = href[i];
if (c === "~") {
c = "\\textasciitilde";
}
chars.push({
type: "textord",
mode: "text",
text: c,
});
}
const body: ParseNode<"text"> = {
type: "text",
mode: parser.mode,
font: "\\texttt",
body: chars,
};
return {
type: "href",
mode: parser.mode,
href,
body: ordargument(body),
};
},
});
+105
View File
@@ -0,0 +1,105 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeSpan} from "../buildCommon";
import {assertNodeType} from "../parseNode";
import ParseError from "../ParseError";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {AnyTrustContext} from "../Settings";
defineFunction({
type: "html",
names: ["\\htmlClass", "\\htmlId", "\\htmlStyle", "\\htmlData"],
props: {
numArgs: 2,
argTypes: ["raw", "original"],
allowedInText: true,
},
handler: ({parser, funcName, token}, args) => {
const value = assertNodeType(args[0], "raw").string;
const body = args[1];
if (parser.settings.strict) {
parser.settings.reportNonstrict("htmlExtension",
"HTML extension is disabled on strict mode");
}
let trustContext: AnyTrustContext;
const attributes: Record<string, string> = {};
switch (funcName) {
case "\\htmlClass":
attributes.class = value;
trustContext = {
command: "\\htmlClass",
class: value,
};
break;
case "\\htmlId":
attributes.id = value;
trustContext = {
command: "\\htmlId",
id: value,
};
break;
case "\\htmlStyle":
attributes.style = value;
trustContext = {
command: "\\htmlStyle",
style: value,
};
break;
case "\\htmlData": {
const data = value.split(",");
for (let i = 0; i < data.length; i++) {
const item = data[i];
const firstEquals = item.indexOf("=");
if (firstEquals < 0) {
throw new ParseError(`\\htmlData key/value '${item}'` +
` missing equals sign`);
}
const key = item.slice(0, firstEquals);
const value = item.slice(firstEquals + 1);
attributes["data-" + key.trim()] = value;
}
trustContext = {
command: "\\htmlData",
attributes,
};
break;
}
default:
throw new Error("Unrecognized html command");
}
if (!parser.settings.isTrusted(trustContext)) {
return parser.formatUnsupportedCmd(funcName);
}
return {
type: "html",
mode: parser.mode,
attributes,
body: ordargument(body),
};
},
htmlBuilder: (group, options) => {
const elements = html.buildExpression(group.body, options, false);
const classes = ["enclosing"];
if (group.attributes.class) {
classes.push(...group.attributes.class.trim().split(/\s+/));
}
const span = makeSpan(classes, elements, options);
for (const attr in group.attributes) {
if (attr !== "class" && group.attributes.hasOwnProperty(attr)) {
span.setAttribute(attr, group.attributes[attr]);
}
}
return span;
},
mathmlBuilder: (group, options) => {
return mml.buildExpressionRow(group.body, options);
},
});
+34
View File
@@ -0,0 +1,34 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeFragment} from "../buildCommon";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
defineFunction({
type: "htmlmathml",
names: ["\\html@mathml"],
props: {
numArgs: 2,
allowedInArgument: true,
allowedInText: true,
},
handler: ({parser}, args) => {
return {
type: "htmlmathml",
mode: parser.mode,
html: ordargument(args[0]),
mathml: ordargument(args[1]),
};
},
htmlBuilder: (group, options) => {
const elements = html.buildExpression(
group.html,
options,
false
);
return makeFragment(elements);
},
mathmlBuilder: (group, options) => {
return mml.buildExpressionRow(group.mathml, options);
},
});
+150
View File
@@ -0,0 +1,150 @@
import defineFunction from "../defineFunction";
import type {Measurement} from "../units";
import {calculateSize, validUnit, makeEm} from "../units";
import ParseError from "../ParseError";
import {Img} from "../domTree";
import {MathNode} from "../mathMLTree";
import {assertNodeType} from "../parseNode";
import type {CssStyle} from "../domTree";
const sizeData = function(str: string): Measurement {
if (/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(str)) {
// str is a number with no unit specified.
// default unit is bp, per graphix package.
return {number: +str, unit: "bp"};
} else {
const match = (/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(str);
if (!match) {
throw new ParseError("Invalid size: '" + str
+ "' in \\includegraphics");
}
const data = {
number: +(match[1] + match[2]), // sign + magnitude, cast to number
unit: match[3],
};
if (!validUnit(data)) {
throw new ParseError("Invalid unit: '" + data.unit
+ "' in \\includegraphics.");
}
return data;
}
};
defineFunction({
type: "includegraphics",
names: ["\\includegraphics"],
props: {
numArgs: 1,
numOptionalArgs: 1,
argTypes: ["raw", "url"],
allowedInText: false,
},
handler: ({parser}, args, optArgs) => {
let width = {number: 0, unit: "em"};
let height = {number: 0.9, unit: "em"}; // sorta character sized.
let totalheight = {number: 0, unit: "em"};
let alt = "";
if (optArgs[0]) {
const attributeStr = assertNodeType(optArgs[0], "raw").string;
// Parser.js does not parse key/value pairs. We get a string.
const attributes = attributeStr.split(",");
for (let i = 0; i < attributes.length; i++) {
const keyVal = attributes[i].split("=");
if (keyVal.length === 2) {
const str = keyVal[1].trim();
switch (keyVal[0].trim()) {
case "alt":
alt = str;
break;
case "width":
width = sizeData(str);
break;
case "height":
height = sizeData(str);
break;
case "totalheight":
totalheight = sizeData(str);
break;
default:
throw new ParseError("Invalid key: '" + keyVal[0] +
"' in \\includegraphics.");
}
}
}
}
const src = assertNodeType(args[0], "url").url;
if (alt === "") {
// No alt given. Use the file name. Strip away the path.
alt = src;
alt = alt.replace(/^.*[\\/]/, '');
alt = alt.substring(0, alt.lastIndexOf('.'));
}
if (!parser.settings.isTrusted({
command: "\\includegraphics",
url: src,
})) {
return parser.formatUnsupportedCmd("\\includegraphics");
}
return {
type: "includegraphics",
mode: parser.mode,
alt: alt,
width: width,
height: height,
totalheight: totalheight,
src: src,
};
},
htmlBuilder: (group, options) => {
const height = calculateSize(group.height, options);
let depth = 0;
if (group.totalheight.number > 0) {
depth = calculateSize(group.totalheight, options) - height;
}
let width = 0;
if (group.width.number > 0) {
width = calculateSize(group.width, options);
}
const style: CssStyle = {height: makeEm(height + depth)};
if (width > 0) {
style.width = makeEm(width);
}
if (depth > 0) {
style.verticalAlign = makeEm(-depth);
}
const node = new Img(group.src, group.alt, style);
node.height = height;
node.depth = depth;
return node;
},
mathmlBuilder: (group, options) => {
const node = new MathNode("mglyph", []);
node.setAttribute("alt", group.alt);
const height = calculateSize(group.height, options);
let depth = 0;
if (group.totalheight.number > 0) {
depth = calculateSize(group.totalheight, options) - height;
node.setAttribute("valign", makeEm(-depth));
}
node.setAttribute("height", makeEm(height + depth));
if (group.width.number > 0) {
const width = calculateSize(group.width, options);
node.setAttribute("width", makeEm(width));
}
node.setAttribute("src", group.src);
return node;
},
});
+55
View File
@@ -0,0 +1,55 @@
// Horizontal spacing commands
import defineFunction from "../defineFunction";
import {makeGlue} from "../buildCommon";
import {SpaceNode} from "../mathMLTree";
import {calculateSize} from "../units";
import {assertNodeType} from "../parseNode";
// TODO: \hskip and \mskip should support plus and minus in lengths
defineFunction({
type: "kern",
names: ["\\kern", "\\mkern", "\\hskip", "\\mskip"],
props: {
numArgs: 1,
argTypes: ["size"],
primitive: true,
allowedInText: true,
},
handler({parser, funcName}, args) {
const size = assertNodeType(args[0], "size");
if (parser.settings.strict) {
const mathFunction = (funcName[1] === 'm'); // \mkern, \mskip
const muUnit = (size.value.unit === 'mu');
if (mathFunction) {
if (!muUnit) {
parser.settings.reportNonstrict("mathVsTextUnits",
`LaTeX's ${funcName} supports only mu units, ` +
`not ${size.value.unit} units`);
}
if (parser.mode !== "math") {
parser.settings.reportNonstrict("mathVsTextUnits",
`LaTeX's ${funcName} works only in math mode`);
}
} else { // !mathFunction
if (muUnit) {
parser.settings.reportNonstrict("mathVsTextUnits",
`LaTeX's ${funcName} doesn't support mu units`);
}
}
}
return {
type: "kern",
mode: parser.mode,
dimension: size.value,
};
},
htmlBuilder(group, options) {
return makeGlue(group.dimension, options);
},
mathmlBuilder(group, options) {
const dimension = calculateSize(group.dimension, options);
return new SpaceNode(dimension);
},
});
+73
View File
@@ -0,0 +1,73 @@
// Horizontal overlap functions
import defineFunction from "../defineFunction";
import {makeSpan} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {makeEm} from "../units";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
defineFunction({
type: "lap",
names: ["\\mathllap", "\\mathrlap", "\\mathclap"],
props: {
numArgs: 1,
allowedInText: true,
},
handler: ({parser, funcName}, args) => {
const body = args[0];
return {
type: "lap",
mode: parser.mode,
alignment: funcName.slice(5),
body,
};
},
htmlBuilder: (group, options) => {
// mathllap, mathrlap, mathclap
let inner;
if (group.alignment === "clap") {
// ref: https://www.math.lsu.edu/~aperlis/publications/mathclap/
inner = makeSpan(
[], [html.buildGroup(group.body, options)]);
// wrap, since CSS will center a .clap > .inner > span
inner = makeSpan(["inner"], [inner], options);
} else {
inner = makeSpan(
["inner"], [html.buildGroup(group.body, options)]);
}
const fix = makeSpan(["fix"], []);
let node = makeSpan(
[group.alignment], [inner, fix], options);
// At this point, we have correctly set horizontal alignment of the
// two items involved in the lap.
// Next, use a strut to set the height of the HTML bounding box.
// Otherwise, a tall argument may be misplaced.
// This code resolved issue #1153
const strut = makeSpan(["strut"]);
strut.style.height = makeEm(node.height + node.depth);
if (node.depth) {
strut.style.verticalAlign = makeEm(-node.depth);
}
node.children.unshift(strut);
// Next, prevent vertical misplacement when next to something tall.
// This code resolves issue #1234
node = makeSpan(["thinbox"], [node], options);
return makeSpan(["mord", "vbox"], [node], options);
},
mathmlBuilder: (group, options) => {
// mathllap, mathrlap, mathclap
const node = new MathNode(
"mpadded", [mml.buildGroup(group.body, options)]);
if (group.alignment !== "rlap") {
const offset = (group.alignment === "llap" ? "-1" : "-0.5");
node.setAttribute("lspace", offset + "width");
}
node.setAttribute("width", "0px");
return node;
},
});
+41
View File
@@ -0,0 +1,41 @@
import defineFunction from "../defineFunction";
import ParseError from "../ParseError";
// Switching from text mode back to math mode
defineFunction({
type: "styling",
names: ["\\(", "$"],
props: {
numArgs: 0,
allowedInText: true,
allowedInMath: false,
},
handler({funcName, parser}, args) {
const outerMode = parser.mode;
parser.switchMode("math");
const close = (funcName === "\\(" ? "\\)" : "$");
const body = parser.parseExpression(false, close);
parser.expect(close);
parser.switchMode(outerMode);
return {
type: "styling",
mode: parser.mode,
style: "text",
body,
};
},
});
// Check for extra closing math delimiters
defineFunction({
type: "text", // Doesn't matter what this is.
names: ["\\)", "\\]"],
props: {
numArgs: 0,
allowedInText: true,
allowedInMath: false,
},
handler(context, args) {
throw new ParseError(`Mismatched ${context.funcName}`);
},
});
+51
View File
@@ -0,0 +1,51 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeFragment} from "../buildCommon";
import Style from "../Style";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type Options from "../Options";
import type {ParseNode} from "../parseNode";
const chooseMathStyle = (group: ParseNode<"mathchoice">, options: Options) => {
switch (options.style.size) {
case Style.DISPLAY.size: return group.display;
case Style.TEXT.size: return group.text;
case Style.SCRIPT.size: return group.script;
case Style.SCRIPTSCRIPT.size: return group.scriptscript;
default: return group.text;
}
};
defineFunction({
type: "mathchoice",
names: ["\\mathchoice"],
props: {
numArgs: 4,
primitive: true,
},
handler: ({parser}, args) => {
return {
type: "mathchoice",
mode: parser.mode,
display: ordargument(args[0]),
text: ordargument(args[1]),
script: ordargument(args[2]),
scriptscript: ordargument(args[3]),
};
},
htmlBuilder: (group, options) => {
const body = chooseMathStyle(group, options);
const elements = html.buildExpression(
body,
options,
false
);
return makeFragment(elements);
},
mathmlBuilder: (group, options) => {
const body = chooseMathStyle(group, options);
return mml.buildExpressionRow(body, options);
},
});
+165
View File
@@ -0,0 +1,165 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeSpan} from "../buildCommon";
import {isCharacterBox} from "../utils";
import {MathNode} from "../mathMLTree";
import type {AnyParseNode} from "../parseNode";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type Options from "../Options";
import type {ParseNode} from "../parseNode";
function htmlBuilder(group: ParseNode<"mclass">, options: Options) {
const elements = html.buildExpression(group.body, options, true);
return makeSpan([group.mclass], elements, options);
}
function mathmlBuilder(group: ParseNode<"mclass">, options: Options) {
let node: MathNode;
const inner = mml.buildExpression(group.body, options);
if (group.mclass === "minner") {
node = new MathNode("mpadded", inner);
} else if (group.mclass === "mord") {
if (group.isCharacterBox) {
node = inner[0];
node.type = "mi";
} else {
node = new MathNode("mi", inner);
}
} else {
if (group.isCharacterBox) {
node = inner[0];
node.type = "mo";
} else {
node = new MathNode("mo", inner);
}
// Set spacing based on what is the most likely adjacent atom type.
// See TeXbook p170.
if (group.mclass === "mbin") {
node.attributes.lspace = "0.22em"; // medium space
node.attributes.rspace = "0.22em";
} else if (group.mclass === "mpunct") {
node.attributes.lspace = "0em";
node.attributes.rspace = "0.17em"; // thinspace
} else if (group.mclass === "mopen" || group.mclass === "mclose") {
node.attributes.lspace = "0em";
node.attributes.rspace = "0em";
} else if (group.mclass === "minner") {
node.attributes.lspace = "0.0556em"; // 1 mu is the most likely option
node.attributes.width = "+0.1111em";
}
// MathML <mo> default space is 5/18 em, so <mrel> needs no action.
// Ref: https://developer.mozilla.org/en-US/docs/Web/MathML/Element/mo
}
return node;
}
// Math class commands except \mathop
defineFunction({
type: "mclass",
names: [
"\\mathord", "\\mathbin", "\\mathrel", "\\mathopen",
"\\mathclose", "\\mathpunct", "\\mathinner",
],
props: {
numArgs: 1,
primitive: true,
},
handler({parser, funcName}, args) {
const body = args[0];
return {
type: "mclass",
mode: parser.mode,
mclass: "m" + funcName.slice(5), // TODO(kevinb): don't prefix with 'm'
body: ordargument(body),
isCharacterBox: isCharacterBox(body),
};
},
htmlBuilder,
mathmlBuilder,
});
export const binrelClass = (arg: AnyParseNode): string => {
// \binrel@ spacing varies with (bin|rel|ord) of the atom in the argument.
// (by rendering separately and with {}s before and after, and measuring
// the change in spacing). We'll do roughly the same by detecting the
// atom type directly.
const atom = (arg.type === "ordgroup" && arg.body.length ? arg.body[0] : arg);
if (atom.type === "atom" && (atom.family === "bin" || atom.family === "rel")) {
return "m" + atom.family;
} else {
return "mord";
}
};
// \@binrel{x}{y} renders like y but as mbin/mrel/mord if x is mbin/mrel/mord.
// This is equivalent to \binrel@{x}\binrel@@{y} in AMSTeX.
defineFunction({
type: "mclass",
names: ["\\@binrel"],
props: {
numArgs: 2,
},
handler({parser}, args) {
return {
type: "mclass",
mode: parser.mode,
mclass: binrelClass(args[0]),
body: ordargument(args[1]),
isCharacterBox: isCharacterBox(args[1]),
};
},
});
// Build a relation or stacked op by placing one symbol on top of another
defineFunction({
type: "mclass",
names: ["\\stackrel", "\\overset", "\\underset"],
props: {
numArgs: 2,
},
handler({parser, funcName}, args) {
const baseArg = args[1];
const shiftedArg = args[0];
let mclass;
if (funcName !== "\\stackrel") {
// LaTeX applies \binrel spacing to \overset and \underset.
mclass = binrelClass(baseArg);
} else {
mclass = "mrel"; // for \stackrel
}
const baseOp: ParseNode<"op"> = {
type: "op",
mode: baseArg.mode,
limits: true,
alwaysHandleSupSub: true,
parentIsSupSub: false,
symbol: false,
suppressBaseShift: funcName !== "\\stackrel",
body: ordargument(baseArg),
};
const supsub: ParseNode<"supsub"> = {
type: "supsub",
mode: shiftedArg.mode,
base: baseOp,
sup: funcName === "\\underset" ? null : shiftedArg,
sub: funcName === "\\underset" ? shiftedArg : null,
};
return {
type: "mclass",
mode: parser.mode,
mclass,
body: [supsub],
isCharacterBox: isCharacterBox(supsub),
};
},
htmlBuilder,
mathmlBuilder,
});
+332
View File
@@ -0,0 +1,332 @@
// Limits, symbols
import defineFunction, {ordargument} from "../defineFunction";
import {mathsym, makeSpan, makeSymbol, makeVList, staticSvg} from "../buildCommon";
import {SymbolNode} from "../domTree";
import {MathNode, newDocumentFragment, TextNode} from "../mathMLTree";
import Style from "../Style";
import {assembleSupSub} from "./utils/assembleSupSub";
import {assertNodeType} from "../parseNode";
import {makeEm} from "../units";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {HtmlBuilderSupSub, MathMLBuilder} from "../defineFunction";
import type {ParseNode} from "../parseNode";
// Most operators have a large successor symbol, but these don't.
const noSuccessor = new Set([
"\\smallint",
]);
// NOTE: Unlike most `htmlBuilder`s, this one handles not only "op", but also
// "supsub" since some of them (like \int) can affect super/subscripting.
export const htmlBuilder: HtmlBuilderSupSub<"op"> = (grp, options) => {
// Operators are handled in the TeXbook pg. 443-444, rule 13(a).
let supGroup;
let subGroup;
let hasLimits = false;
let group: ParseNode<"op">;
if (grp.type === "supsub") {
// If we have limits, supsub will pass us its group to handle. Pull
// out the superscript and subscript and set the group to the op in
// its base.
supGroup = grp.sup;
subGroup = grp.sub;
group = assertNodeType(grp.base, "op");
hasLimits = true;
} else {
group = assertNodeType(grp, "op");
}
const style = options.style;
let large = false;
if (style.size === Style.DISPLAY.size &&
group.symbol &&
!noSuccessor.has(group.name)) {
// Most symbol operators get larger in displaystyle (rule 13)
large = true;
}
let base;
if (group.symbol) {
// If this is a symbol, create the symbol.
const fontName = large ? "Size2-Regular" : "Size1-Regular";
let stash = "";
if (group.name === "\\oiint" || group.name === "\\oiiint") {
// No font glyphs yet, so use a glyph w/o the oval.
// TODO: When font glyphs are available, delete this code.
stash = group.name.slice(1);
group.name = stash === "oiint" ? "\\iint" : "\\iiint";
}
base = makeSymbol(
group.name, fontName, "math", options,
["mop", "op-symbol", large ? "large-op" : "small-op"]);
if (stash.length > 0) {
// We're in \oiint or \oiiint. Overlay the oval.
// TODO: When font glyphs are available, delete this code.
const italic = base.italic;
const oval = staticSvg(stash + "Size"
+ (large ? "2" : "1"), options);
base = makeVList({
positionType: "individualShift",
children: [
{type: "elem", elem: base, shift: 0},
{type: "elem", elem: oval, shift: large ? 0.08 : 0},
],
}, options);
group.name = "\\" + stash;
base.classes.unshift("mop");
// TODO(ts)
(base as any).italic = italic;
}
} else if (group.body) {
// If this is a list, compose that list.
const inner = html.buildExpression(group.body, options, true);
if (inner.length === 1 && inner[0] instanceof SymbolNode) {
base = inner[0];
base.classes[0] = "mop"; // replace old mclass
} else {
base = makeSpan(["mop"], inner, options);
}
} else {
// Otherwise, this is a text operator. Build the text from the
// operator's name.
const output = [];
for (let i = 1; i < group.name!.length; i++) {
output.push(mathsym(group.name![i], group.mode, options));
}
base = makeSpan(["mop"], output, options);
}
// If content of op is a single symbol, shift it vertically.
let baseShift = 0;
let slant = 0;
if ((base instanceof SymbolNode
|| group.name === "\\oiint" || group.name === "\\oiiint")
&& !group.suppressBaseShift) {
// We suppress the shift of the base of \overset and \underset. Otherwise,
// shift the symbol so its center lies on the axis (rule 13). It
// appears that our fonts have the centers of the symbols already
// almost on the axis, so these numbers are very small. Note we
// don't actually apply this here, but instead it is used either in
// the vlist creation or separately when there are no limits.
baseShift = (base.height - base.depth) / 2 -
options.fontMetrics().axisHeight;
// The slant of the symbol is just its italic correction.
// TODO(ts)
slant = (base as SymbolNode & {italic?: number}).italic || 0;
}
if (hasLimits) {
return assembleSupSub(base, supGroup, subGroup, options,
style, slant, baseShift);
} else {
if (baseShift) {
base.style.position = "relative";
base.style.top = makeEm(baseShift);
}
return base;
}
};
const mathmlBuilder: MathMLBuilder<"op"> = (group, options) => {
let node;
if (group.symbol) {
// This is a symbol. Just add the symbol.
node = new MathNode(
"mo", [mml.makeText(group.name, group.mode)]);
if (noSuccessor.has(group.name)) {
node.setAttribute("largeop", "false");
}
} else if (group.body) {
// This is an operator with children. Add them.
node = new MathNode(
"mo", mml.buildExpression(group.body, options));
} else {
// This is a text operator. Add all the characters from the
// operator's name.
node = new MathNode(
"mi", [new TextNode(group.name!.slice(1))]);
// Append an <mo>&ApplyFunction;</mo>.
// ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.4
const operator = new MathNode("mo",
[mml.makeText("\u2061", "text")]);
if (group.parentIsSupSub) {
node = new MathNode("mrow", [node, operator]);
} else {
node = newDocumentFragment([node, operator]);
}
}
return node;
};
const singleCharBigOps: Record<string, string> = {
"\u220F": "\\prod",
"\u2210": "\\coprod",
"\u2211": "\\sum",
"\u22c0": "\\bigwedge",
"\u22c1": "\\bigvee",
"\u22c2": "\\bigcap",
"\u22c3": "\\bigcup",
"\u2a00": "\\bigodot",
"\u2a01": "\\bigoplus",
"\u2a02": "\\bigotimes",
"\u2a04": "\\biguplus",
"\u2a06": "\\bigsqcup",
};
defineFunction({
type: "op",
names: [
"\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
"\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
"\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint", "\u220F",
"\u2210", "\u2211", "\u22c0", "\u22c1", "\u22c2", "\u22c3", "\u2a00",
"\u2a01", "\u2a02", "\u2a04", "\u2a06",
],
props: {
numArgs: 0,
},
handler: ({parser, funcName}, args) => {
let fName = funcName;
if (fName.length === 1) {
fName = singleCharBigOps[fName];
}
return {
type: "op",
mode: parser.mode,
limits: true,
parentIsSupSub: false,
symbol: true,
name: fName,
};
},
htmlBuilder,
mathmlBuilder,
});
// Note: calling defineFunction with a type that's already been defined only
// works because the same htmlBuilder and mathmlBuilder are being used.
defineFunction({
type: "op",
names: ["\\mathop"],
props: {
numArgs: 1,
primitive: true,
},
handler: ({parser}, args) => {
const body = args[0];
return {
type: "op",
mode: parser.mode,
limits: false,
parentIsSupSub: false,
symbol: false,
body: ordargument(body),
};
},
htmlBuilder,
mathmlBuilder,
});
// There are 2 flags for operators; whether they produce limits in
// displaystyle, and whether they are symbols and should grow in
// displaystyle. These four groups cover the four possible choices.
const singleCharIntegrals: Record<string, string> = {
"\u222b": "\\int",
"\u222c": "\\iint",
"\u222d": "\\iiint",
"\u222e": "\\oint",
"\u222f": "\\oiint",
"\u2230": "\\oiiint",
};
// No limits, not symbols
defineFunction({
type: "op",
names: [
"\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg",
"\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg",
"\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp",
"\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin",
"\\sinh", "\\sh", "\\tan", "\\tanh", "\\tg", "\\th",
],
props: {
numArgs: 0,
},
handler({parser, funcName}) {
return {
type: "op",
mode: parser.mode,
limits: false,
parentIsSupSub: false,
symbol: false,
name: funcName,
};
},
htmlBuilder,
mathmlBuilder,
});
// Limits, not symbols
defineFunction({
type: "op",
names: [
"\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup",
],
props: {
numArgs: 0,
},
handler({parser, funcName}) {
return {
type: "op",
mode: parser.mode,
limits: true,
parentIsSupSub: false,
symbol: false,
name: funcName,
};
},
htmlBuilder,
mathmlBuilder,
});
// No limits, symbols
defineFunction({
type: "op",
names: [
"\\int", "\\iint", "\\iiint", "\\oint", "\\oiint", "\\oiiint",
"\u222b", "\u222c", "\u222d", "\u222e", "\u222f", "\u2230",
],
props: {
numArgs: 0,
allowedInArgument: true,
},
handler({parser, funcName}) {
let fName = funcName;
if (fName.length === 1) {
fName = singleCharIntegrals[fName];
}
return {
type: "op",
mode: parser.mode,
limits: false,
parentIsSupSub: false,
symbol: true,
name: fName,
};
},
htmlBuilder,
mathmlBuilder,
});
+161
View File
@@ -0,0 +1,161 @@
import defineFunction, {ordargument} from "../defineFunction";
import defineMacro from "../defineMacro";
import {makeSpan} from "../buildCommon";
import {MathNode, newDocumentFragment, SpaceNode, TextNode} from "../mathMLTree";
import {SymbolNode} from "../domTree";
import {assembleSupSub} from "./utils/assembleSupSub";
import {assertNodeType} from "../parseNode";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type {HtmlBuilderSupSub, MathMLBuilder} from "../defineFunction";
import type {AnyParseNode, ParseNode} from "../parseNode";
// NOTE: Unlike most `htmlBuilder`s, this one handles not only
// "operatorname", but also "supsub" since \operatorname* can
// affect super/subscripting.
export const htmlBuilder: HtmlBuilderSupSub<"operatorname"> = (grp, options) => {
// Operators are handled in the TeXbook pg. 443-444, rule 13(a).
let supGroup;
let subGroup;
let hasLimits = false;
let group: ParseNode<"operatorname">;
if (grp.type === "supsub") {
// If we have limits, supsub will pass us its group to handle. Pull
// out the superscript and subscript and set the group to the op in
// its base.
supGroup = grp.sup;
subGroup = grp.sub;
group = assertNodeType(grp.base, "operatorname");
hasLimits = true;
} else {
group = assertNodeType(grp, "operatorname");
}
let base;
if (group.body.length > 0) {
const body = group.body.map((child): AnyParseNode => {
const childText = "text" in child ? child.text : undefined;
if (typeof childText === "string") {
return {
type: "textord",
mode: child.mode,
text: childText,
};
} else {
return child;
}
});
// Consolidate function names into symbol characters.
const expression = html.buildExpression(
body, options.withFont("mathrm"), true);
for (let i = 0; i < expression.length; i++) {
const child = expression[i];
if (child instanceof SymbolNode) {
// Per amsopn package,
// change minus to hyphen and \ast to asterisk
child.text = child.text.replace(/\u2212/, "-")
.replace(/\u2217/, "*");
}
}
base = makeSpan(["mop"], expression, options);
} else {
base = makeSpan(["mop"], [], options);
}
if (hasLimits) {
return assembleSupSub(base, supGroup, subGroup, options,
options.style, 0, 0);
} else {
return base;
}
};
const mathmlBuilder: MathMLBuilder<"operatorname"> = (group, options) => {
// The steps taken here are similar to the html version.
let expression: Array<MathNode | TextNode> = mml.buildExpression(
group.body, options.withFont("mathrm"));
// Is expression a string or has it something like a fraction?
let isAllString = true; // default
for (let i = 0; i < expression.length; i++) {
const node = expression[i];
if (node instanceof SpaceNode) {
// Do nothing
} else if (node instanceof MathNode) {
switch (node.type) {
case "mi":
case "mn":
case "mspace":
case "mtext":
break; // Do nothing yet.
case "mo": {
const child = node.children[0];
if (node.children.length === 1 &&
child instanceof TextNode) {
child.text =
child.text.replace(/\u2212/, "-")
.replace(/\u2217/, "*");
} else {
isAllString = false;
}
break;
}
default:
isAllString = false;
}
} else {
isAllString = false;
}
}
if (isAllString) {
// Write a single TextNode instead of multiple nested tags.
const word = expression.map(node => node.toText()).join("");
expression = [new TextNode(word)];
}
const identifier = new MathNode("mi", expression);
identifier.setAttribute("mathvariant", "normal");
// \u2061 is the same as &ApplyFunction;
// ref: https://www.w3schools.com/charsets/ref_html_entities_a.asp
const operator = new MathNode("mo",
[mml.makeText("\u2061", "text")]);
if (group.parentIsSupSub) {
return new MathNode("mrow", [identifier, operator]);
} else {
return newDocumentFragment([identifier, operator]);
}
};
// \operatorname
// amsopn.dtx: \mathop{#1\kern\z@\operator@font#3}\newmcodes@
defineFunction({
type: "operatorname",
names: ["\\operatorname@", "\\operatornamewithlimits"],
props: {
numArgs: 1,
},
handler: ({parser, funcName}, args) => {
const body = args[0];
return {
type: "operatorname",
mode: parser.mode,
body: ordargument(body),
alwaysHandleSupSub: (funcName === "\\operatornamewithlimits"),
limits: false,
parentIsSupSub: false,
};
},
htmlBuilder,
mathmlBuilder,
});
defineMacro("\\operatorname",
"\\@ifstar\\operatornamewithlimits\\operatorname@");
+20
View File
@@ -0,0 +1,20 @@
import {defineFunctionBuilders} from "../defineFunction";
import {makeFragment, makeSpan} from "../buildCommon";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
defineFunctionBuilders({
type: "ordgroup",
htmlBuilder(group, options) {
if (group.semisimple) {
return makeFragment(
html.buildExpression(group.body, options, false));
}
return makeSpan(
["mord"], html.buildExpression(group.body, options, true), options);
},
mathmlBuilder(group, options) {
return mml.buildExpressionRow(group.body, options, true);
},
});
+58
View File
@@ -0,0 +1,58 @@
import defineFunction from "../defineFunction";
import {makeLineSpan, makeSpan, makeVList} from "../buildCommon";
import {MathNode, TextNode} from "../mathMLTree";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
defineFunction({
type: "overline",
names: ["\\overline"],
props: {
numArgs: 1,
},
handler({parser}, args) {
const body = args[0];
return {
type: "overline",
mode: parser.mode,
body,
};
},
htmlBuilder(group, options) {
// Overlines are handled in the TeXbook pg 443, Rule 9.
// Build the inner group in the cramped style.
const innerGroup = html.buildGroup(group.body,
options.havingCrampedStyle());
// Create the line above the body
const line = makeLineSpan("overline-line", options);
// Generate the vlist, with the appropriate kerns
const defaultRuleThickness = options.fontMetrics().defaultRuleThickness;
const vlist = makeVList({
positionType: "firstBaseline",
children: [
{type: "elem", elem: innerGroup},
{type: "kern", size: 3 * defaultRuleThickness},
{type: "elem", elem: line},
{type: "kern", size: defaultRuleThickness},
],
}, options);
return makeSpan(["mord", "overline"], [vlist], options);
},
mathmlBuilder(group, options) {
const operator = new MathNode(
"mo", [new TextNode("\u203e")]);
operator.setAttribute("stretchy", "true");
const node = new MathNode(
"mover",
[mml.buildGroup(group.body, options), operator]);
node.setAttribute("accent", "true");
return node;
},
});
+73
View File
@@ -0,0 +1,73 @@
import defineFunction, {ordargument} from "../defineFunction";
import defineMacro from "../defineMacro";
import {makeFragment, makeSpan} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
defineFunction({
type: "phantom",
names: ["\\phantom"],
props: {
numArgs: 1,
allowedInText: true,
},
handler: ({parser}, args) => {
const body = args[0];
return {
type: "phantom",
mode: parser.mode,
body: ordargument(body),
};
},
htmlBuilder: (group, options) => {
const elements = html.buildExpression(
group.body,
options.withPhantom(),
false
);
// \phantom isn't supposed to affect the elements it contains.
// See "color" for more details.
return makeFragment(elements);
},
mathmlBuilder: (group, options) => {
const inner = mml.buildExpression(group.body, options);
return new MathNode("mphantom", inner);
},
});
defineMacro("\\hphantom", "\\smash{\\phantom{#1}}");
defineFunction({
type: "vphantom",
names: ["\\vphantom"],
props: {
numArgs: 1,
allowedInText: true,
},
handler: ({parser}, args) => {
const body = args[0];
return {
type: "vphantom",
mode: parser.mode,
body,
};
},
htmlBuilder: (group, options) => {
const inner = makeSpan(
["inner"],
[html.buildGroup(group.body, options.withPhantom())]);
const fix = makeSpan(["fix"], []);
return makeSpan(
["mord", "rlap"], [inner, fix], options);
},
mathmlBuilder: (group, options) => {
const inner = mml.buildExpression(ordargument(group.body), options);
const phantom = new MathNode("mphantom", inner);
const node = new MathNode("mpadded", [phantom]);
node.setAttribute("width", "0px");
return node;
},
});
+43
View File
@@ -0,0 +1,43 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeSpan} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import {binrelClass} from "./mclass";
import type {ParseNode} from "../parseNode";
// \pmb is a simulation of bold font.
// The version of \pmb in ambsy.sty works by typesetting three copies
// with small offsets. We use CSS text-shadow.
// It's a hack. Not as good as a real bold font. Better than nothing.
defineFunction({
type: "pmb",
names: ["\\pmb"],
props: {
numArgs: 1,
allowedInText: true,
},
handler({parser}, args) {
return {
type: "pmb",
mode: parser.mode,
mclass: binrelClass(args[0]),
body: ordargument(args[0]),
};
},
htmlBuilder(group: ParseNode<"pmb">, options) {
const elements = html.buildExpression(group.body, options, true);
const node = makeSpan([group.mclass], elements, options);
node.style.textShadow = "0.02em 0.01em 0.04px";
return node;
},
mathmlBuilder(group: ParseNode<"pmb">, style) {
const inner = mml.buildExpression(group.body, style);
// Wrap with an <mstyle> element.
const node = new MathNode("mstyle", inner);
node.setAttribute("style", "text-shadow: 0.02em 0.01em 0.04px");
return node;
},
});
+45
View File
@@ -0,0 +1,45 @@
import defineFunction from "../defineFunction";
import {makeVList} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {assertNodeType} from "../parseNode";
import {calculateSize} from "../units";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
// Box manipulation
defineFunction({
type: "raisebox",
names: ["\\raisebox"],
props: {
numArgs: 2,
argTypes: ["size", "hbox"],
allowedInText: true,
},
handler({parser}, args) {
const amount = assertNodeType(args[0], "size").value;
const body = args[1];
return {
type: "raisebox",
mode: parser.mode,
dy: amount,
body,
};
},
htmlBuilder(group, options) {
const body = html.buildGroup(group.body, options);
const dy = calculateSize(group.dy, options);
return makeVList({
positionType: "shift",
positionData: -dy,
children: [{type: "elem", elem: body}],
}, options);
},
mathmlBuilder(group, options) {
const node = new MathNode(
"mpadded", [mml.buildGroup(group.body, options)]);
const dy = group.dy.number + group.dy.unit;
node.setAttribute("voffset", dy);
return node;
},
});
+17
View File
@@ -0,0 +1,17 @@
import defineFunction from "../defineFunction";
defineFunction({
type: "internal",
names: ["\\relax"],
props: {
numArgs: 0,
allowedInText: true,
allowedInArgument: true,
},
handler({parser}) {
return {
type: "internal",
mode: parser.mode,
};
},
});
+76
View File
@@ -0,0 +1,76 @@
import {makeSpan} from "../buildCommon";
import defineFunction from "../defineFunction";
import {MathNode} from "../mathMLTree";
import {assertNodeType} from "../parseNode";
import {calculateSize, makeEm} from "../units";
defineFunction({
type: "rule",
names: ["\\rule"],
props: {
numArgs: 2,
numOptionalArgs: 1,
allowedInText: true,
allowedInMath: true,
argTypes: ["size", "size", "size"],
},
handler({parser}, args, optArgs) {
const shift = optArgs[0];
const width = assertNodeType(args[0], "size");
const height = assertNodeType(args[1], "size");
return {
type: "rule",
mode: parser.mode,
shift: shift && assertNodeType(shift, "size").value,
width: width.value,
height: height.value,
};
},
htmlBuilder(group, options) {
// Make an empty span for the rule
const rule = makeSpan(["mord", "rule"], [], options);
// Calculate the shift, width, and height of the rule, and account for units
const width = calculateSize(group.width, options);
const height = calculateSize(group.height, options);
const shift = (group.shift) ? calculateSize(group.shift, options) : 0;
// Style the rule to the right size
rule.style.borderRightWidth = makeEm(width);
rule.style.borderTopWidth = makeEm(height);
rule.style.bottom = makeEm(shift);
// Record the height and width
rule.width = width;
rule.height = height + shift;
rule.depth = -shift;
// Font size is the number large enough that the browser will
// reserve at least `absHeight` space above the baseline.
// The 1.125 factor was empirically determined
rule.maxFontSize = height * 1.125 * options.sizeMultiplier;
return rule;
},
mathmlBuilder(group, options) {
const width = calculateSize(group.width, options);
const height = calculateSize(group.height, options);
const shift = (group.shift) ? calculateSize(group.shift, options) : 0;
const color = options.color && options.getColor() || "black";
const rule = new MathNode("mspace");
rule.setAttribute("mathbackground", color);
rule.setAttribute("width", makeEm(width));
rule.setAttribute("height", makeEm(height));
const wrapper = new MathNode("mpadded", [rule]);
if (shift >= 0) {
wrapper.setAttribute("height", makeEm(shift));
} else {
wrapper.setAttribute("height", makeEm(shift));
wrapper.setAttribute("depth", makeEm(-shift));
}
wrapper.setAttribute("voffset", makeEm(shift));
return wrapper;
},
});
+90
View File
@@ -0,0 +1,90 @@
import {makeFragment} from "../buildCommon";
import defineFunction from "../defineFunction";
import {MathNode} from "../mathMLTree";
import {makeEm} from "../units";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type Options from "../Options";
import type {AnyParseNode} from "../parseNode";
import type {HtmlBuilder} from "../defineFunction";
import type {documentFragment as HtmlDocumentFragment} from "../domTree";
export function sizingGroup(
value: AnyParseNode[],
options: Options,
baseOptions: Options,
): HtmlDocumentFragment {
const inner = html.buildExpression(value, options, false);
const multiplier = options.sizeMultiplier / baseOptions.sizeMultiplier;
// Add size-resetting classes to the inner list and set maxFontSize
// manually. Handle nested size changes.
for (let i = 0; i < inner.length; i++) {
const pos = inner[i].classes.indexOf("sizing");
if (pos < 0) {
Array.prototype.push.apply(inner[i].classes,
options.sizingClasses(baseOptions));
} else if (inner[i].classes[pos + 1] === "reset-size" + options.size) {
// This is a nested size change: e.g., inner[i] is the "b" in
// `\Huge a \small b`. Override the old size (the `reset-` class)
// but not the new size.
inner[i].classes[pos + 1] = "reset-size" + baseOptions.size;
}
inner[i].height *= multiplier;
inner[i].depth *= multiplier;
}
return makeFragment(inner);
}
const sizeFuncs = [
"\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small",
"\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
];
export const htmlBuilder: HtmlBuilder<"sizing"> = (group, options) => {
// Handle sizing operators like \Huge. Real TeX doesn't actually allow
// these functions inside of math expressions, so we do some special
// handling.
const newOptions = options.havingSize(group.size);
return sizingGroup(group.body, newOptions, options);
};
defineFunction({
type: "sizing",
names: sizeFuncs,
props: {
numArgs: 0,
allowedInText: true,
},
handler: ({breakOnTokenText, funcName, parser}, args) => {
const body = parser.parseExpression(false, breakOnTokenText);
return {
type: "sizing",
mode: parser.mode,
// Figure out what size to use based on the list of functions above
size: sizeFuncs.indexOf(funcName) + 1,
body,
};
},
htmlBuilder,
mathmlBuilder: (group, options) => {
const newOptions = options.havingSize(group.size);
const inner = mml.buildExpression(group.body, newOptions);
const node = new MathNode("mstyle", inner);
// TODO(emily): This doesn't produce the correct size for nested size
// changes, because we don't keep state of what style we're currently
// in, so we can't reset the size to normal before changing it. Now
// that we're passing an options parameter we should be able to fix
// this.
node.setAttribute("mathsize", makeEm(newOptions.sizeMultiplier));
return node;
},
});
+115
View File
@@ -0,0 +1,115 @@
// smash, with optional [tb], as in AMS
import defineFunction from "../defineFunction";
import {makeSpan, makeVList} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {assertNodeType, assertSymbolNodeType} from "../parseNode";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
defineFunction({
type: "smash",
names: ["\\smash"],
props: {
numArgs: 1,
numOptionalArgs: 1,
allowedInText: true,
},
handler: ({parser}, args, optArgs) => {
let smashHeight = false;
let smashDepth = false;
const tbArg = optArgs[0] && assertNodeType(optArgs[0], "ordgroup");
if (tbArg) {
// Optional [tb] argument is engaged.
// ref: amsmath: \renewcommand{\smash}[1][tb]{%
// def\mb@t{\ht}\def\mb@b{\dp}\def\mb@tb{\ht\z@\z@\dp}%
let letter = "";
for (let i = 0; i < tbArg.body.length; ++i) {
const node = tbArg.body[i];
letter = assertSymbolNodeType(node).text;
if (letter === "t") {
smashHeight = true;
} else if (letter === "b") {
smashDepth = true;
} else {
smashHeight = false;
smashDepth = false;
break;
}
}
} else {
smashHeight = true;
smashDepth = true;
}
const body = args[0];
return {
type: "smash",
mode: parser.mode,
body,
smashHeight,
smashDepth,
};
},
htmlBuilder: (group, options) => {
const node = makeSpan(
[], [html.buildGroup(group.body, options)]);
if (!group.smashHeight && !group.smashDepth) {
return node;
}
if (group.smashHeight) {
node.height = 0;
}
if (group.smashDepth) {
node.depth = 0;
}
if (group.smashHeight && group.smashDepth) {
// Symmetric \smash can stay in inline layout.
return makeSpan(["mord", "smash"], [node], options);
}
// In order to influence makeVList for asymmetric smashing, we have to
// reset the children.
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
if (group.smashHeight) {
node.children[i].height = 0;
}
if (group.smashDepth) {
node.children[i].depth = 0;
}
}
}
// At this point, we've reset the TeX-like height and depth values.
// But the span still has an HTML line height.
// makeVList applies "display: table-cell", which prevents the browser
// from acting on that line height. So we'll call makeVList now.
const smashedNode = makeVList({
positionType: "firstBaseline",
children: [{type: "elem", elem: node}],
}, options);
// For spacing, TeX treats \smash as a math group (same spacing as ord).
return makeSpan(["mord"], [smashedNode], options);
},
mathmlBuilder: (group, options) => {
const node = new MathNode(
"mpadded", [mml.buildGroup(group.body, options)]);
if (group.smashHeight) {
node.setAttribute("height", "0px");
}
if (group.smashDepth) {
node.setAttribute("depth", "0px");
}
return node;
},
});
+124
View File
@@ -0,0 +1,124 @@
import defineFunction from "../defineFunction";
import {makeSpan, makeVList, wrapFragment} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import {makeSqrtImage} from "../delimiter";
import Style from "../Style";
import {makeEm} from "../units";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
defineFunction({
type: "sqrt",
names: ["\\sqrt"],
props: {
numArgs: 1,
numOptionalArgs: 1,
},
handler({parser}, args, optArgs) {
const index = optArgs[0];
const body = args[0];
return {
type: "sqrt",
mode: parser.mode,
body,
index,
};
},
htmlBuilder(group, options) {
// Square roots are handled in the TeXbook pg. 443, Rule 11.
// First, we do the same steps as in overline to build the inner group
// and line
let inner = html.buildGroup(group.body, options.havingCrampedStyle());
if (inner.height === 0) {
// Render a small surd.
inner.height = options.fontMetrics().xHeight;
}
// Some groups can return document fragments. Handle those by wrapping
// them in a span.
inner = wrapFragment(inner, options);
// Calculate the minimum size for the \surd delimiter
const metrics = options.fontMetrics();
const theta = metrics.defaultRuleThickness;
let phi = theta;
if (options.style.id < Style.TEXT.id) {
phi = options.fontMetrics().xHeight;
}
// Calculate the clearance between the body and line
let lineClearance = theta + phi / 4;
const minDelimiterHeight = (inner.height + inner.depth +
lineClearance + theta);
// Create a sqrt SVG of the required minimum size
const {span: img, ruleWidth, advanceWidth} =
makeSqrtImage(minDelimiterHeight, options);
const delimDepth = img.height - ruleWidth;
// Adjust the clearance based on the delimiter size
if (delimDepth > inner.height + inner.depth + lineClearance) {
lineClearance =
(lineClearance + delimDepth - inner.height - inner.depth) / 2;
}
// Shift the sqrt image
const imgShift = img.height - inner.height - lineClearance - ruleWidth;
inner.style.paddingLeft = makeEm(advanceWidth);
// Overlay the image and the argument.
const body = makeVList({
positionType: "firstBaseline",
children: [
{type: "elem", elem: inner, wrapperClasses: ["svg-align"]},
{type: "kern", size: -(inner.height + imgShift)},
{type: "elem", elem: img},
{type: "kern", size: ruleWidth},
],
}, options);
if (!group.index) {
return makeSpan(["mord", "sqrt"], [body], options);
} else {
// Handle the optional root index
// The index is always in scriptscript style
const newOptions = options.havingStyle(Style.SCRIPTSCRIPT);
const rootm = html.buildGroup(group.index, newOptions, options);
// The amount the index is shifted by. This is taken from the TeX
// source, in the definition of `\r@@t`.
const toShift = 0.6 * (body.height - body.depth);
// Build a VList with the superscript shifted up correctly
const rootVList = makeVList({
positionType: "shift",
positionData: -toShift,
children: [{type: "elem", elem: rootm}],
}, options);
// Add a class surrounding it so we can add on the appropriate
// kerning
const rootVListWrap = makeSpan(["root"], [rootVList]);
return makeSpan(["mord", "sqrt"],
[rootVListWrap, body], options);
}
},
mathmlBuilder(group, options) {
const {body, index} = group;
return index ?
new MathNode(
"mroot", [
mml.buildGroup(body, options),
mml.buildGroup(index, options),
]) :
new MathNode(
"msqrt", [mml.buildGroup(body, options)]);
},
});
+73
View File
@@ -0,0 +1,73 @@
import defineFunction from "../defineFunction";
import {MathNode} from "../mathMLTree";
import Style from "../Style";
import {sizingGroup} from "./sizing";
import * as mml from "../buildMathML";
import type {StyleStr} from "../types";
const styleMap = {
"display": Style.DISPLAY,
"text": Style.TEXT,
"script": Style.SCRIPT,
"scriptscript": Style.SCRIPTSCRIPT,
};
defineFunction({
type: "styling",
names: [
"\\displaystyle", "\\textstyle", "\\scriptstyle",
"\\scriptscriptstyle",
],
props: {
numArgs: 0,
allowedInText: true,
primitive: true,
},
handler({breakOnTokenText, funcName, parser}, args) {
// parse out the implicit body
const body = parser.parseExpression(true, breakOnTokenText);
// TODO: Refactor to avoid duplicating styleMap in multiple places (e.g.
// here and in buildHTML and de-dupe the enumeration of all the styles).
// TODO(ts): The names above exactly match the styles.
const style = funcName.slice(1, funcName.length - 5) as StyleStr;
return {
type: "styling",
mode: parser.mode,
// Figure out what style to use by pulling out the style from
// the function name
style,
body,
};
},
htmlBuilder(group, options) {
// Style changes are handled in the TeXbook on pg. 442, Rule 3.
const newStyle = styleMap[group.style];
const newOptions = options.havingStyle(newStyle).withFont('');
return sizingGroup(group.body, newOptions, options);
},
mathmlBuilder(group, options) {
// Figure out what style we're changing to.
const newStyle = styleMap[group.style];
const newOptions = options.havingStyle(newStyle);
const inner = mml.buildExpression(group.body, newOptions);
const node = new MathNode("mstyle", inner);
const styleAttributes = {
"display": ["0", "true"],
"text": ["0", "false"],
"script": ["1", "false"],
"scriptscript": ["2", "false"],
};
const attr = styleAttributes[group.style];
node.setAttribute("scriptlevel", attr[0]);
node.setAttribute("displaystyle", attr[1]);
return node;
},
});
+265
View File
@@ -0,0 +1,265 @@
import {defineFunctionBuilders} from "../defineFunction";
import {makeSpan, makeVList} from "../buildCommon";
import {SymbolNode} from "../domTree";
import {isCharacterBox} from "../utils";
import {MathNode} from "../mathMLTree";
import {makeEm} from "../units";
import Style from "../Style";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import * as accent from "./accent";
import * as horizBrace from "./horizBrace";
import * as op from "./op";
import * as operatorname from "./operatorname";
import type Options from "../Options";
import type {ParseNode} from "../parseNode";
import type {HtmlBuilder} from "../defineFunction";
import type {MathNodeType} from "../mathMLTree";
/**
* Sometimes, groups perform special rules when they have superscripts or
* subscripts attached to them. This function lets the `supsub` group know that
* Sometimes, groups perform special rules when they have superscripts or
* its inner element should handle the superscripts and subscripts instead of
* handling them itself.
*/
const htmlBuilderDelegate = function(
group: ParseNode<"supsub">,
options: Options,
): HtmlBuilder<any> | null | undefined {
const base = group.base;
if (!base) {
return null;
} else if (base.type === "op") {
// Operators handle supsubs differently when they have limits
// (e.g. `\displaystyle\sum_2^3`)
const delegate = base.limits &&
(options.style.size === Style.DISPLAY.size ||
base.alwaysHandleSupSub);
return delegate ? op.htmlBuilder : null;
} else if (base.type === "operatorname") {
const delegate = base.alwaysHandleSupSub &&
(options.style.size === Style.DISPLAY.size || base.limits);
return delegate ? operatorname.htmlBuilder : null;
} else if (base.type === "accent") {
return isCharacterBox(base.base) ? accent.htmlBuilder : null;
} else if (base.type === "horizBrace") {
const isSup = !group.sub;
return isSup === base.isOver ? horizBrace.htmlBuilder : null;
} else {
return null;
}
};
// Super scripts and subscripts, whose precise placement can depend on other
// functions that precede them.
defineFunctionBuilders({
type: "supsub",
htmlBuilder(group, options) {
// Superscript and subscripts are handled in the TeXbook on page
// 445-446, rules 18(a-f).
// Here is where we defer to the inner group if it should handle
// superscripts and subscripts itself.
const builderDelegate = htmlBuilderDelegate(group, options);
if (builderDelegate) {
return builderDelegate(group, options);
}
const {base: valueBase, sup: valueSup, sub: valueSub} = group;
const base = html.buildGroup(valueBase, options);
let supm;
let subm;
const metrics = options.fontMetrics();
// Rule 18a
let supShift = 0;
let subShift = 0;
const isCharBox = valueBase && isCharacterBox(valueBase);
if (valueSup) {
const newOptions = options.havingStyle(options.style.sup());
supm = html.buildGroup(valueSup, newOptions, options);
if (!isCharBox) {
supShift = base.height - newOptions.fontMetrics().supDrop
* newOptions.sizeMultiplier / options.sizeMultiplier;
}
}
if (valueSub) {
const newOptions = options.havingStyle(options.style.sub());
subm = html.buildGroup(valueSub, newOptions, options);
if (!isCharBox) {
subShift = base.depth + newOptions.fontMetrics().subDrop
* newOptions.sizeMultiplier / options.sizeMultiplier;
}
}
// Rule 18c
let minSupShift;
if (options.style === Style.DISPLAY) {
minSupShift = metrics.sup1;
} else if (options.style.cramped) {
minSupShift = metrics.sup3;
} else {
minSupShift = metrics.sup2;
}
// scriptspace is a font-size-independent size, so scale it
// appropriately for use as the marginRight.
const multiplier = options.sizeMultiplier;
const marginRight = makeEm((0.5 / metrics.ptPerEm) / multiplier);
let marginLeft = null;
if (subm) {
// Subscripts shouldn't be shifted by the base's italic correction.
// Account for that by shifting the subscript back the appropriate
// amount. Note we only do this when the base is a single symbol.
const isOiint =
group.base && group.base.type === "op" && group.base.name &&
(group.base.name === "\\oiint" || group.base.name === "\\oiiint");
if (base instanceof SymbolNode || isOiint) {
// @ts-ignore
marginLeft = makeEm(-base.italic);
}
}
let supsub;
if (supm && subm) {
supShift = Math.max(
supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight);
subShift = Math.max(subShift, metrics.sub2);
const ruleWidth = metrics.defaultRuleThickness;
// Rule 18e
const maxWidth = 4 * ruleWidth;
if ((supShift - supm.depth) - (subm.height - subShift) < maxWidth) {
subShift = maxWidth - (supShift - supm.depth) + subm.height;
const psi = 0.8 * metrics.xHeight - (supShift - supm.depth);
if (psi > 0) {
supShift += psi;
subShift -= psi;
}
}
const vlistElem = [
{type: "elem" as const, elem: subm, shift: subShift, marginRight,
marginLeft},
{type: "elem" as const, elem: supm, shift: -supShift, marginRight},
];
supsub = makeVList({
positionType: "individualShift",
children: vlistElem,
}, options);
} else if (subm) {
// Rule 18b
subShift = Math.max(
subShift, metrics.sub1,
subm.height - 0.8 * metrics.xHeight);
const vlistElem =
[{type: "elem" as const, elem: subm, marginLeft, marginRight}];
supsub = makeVList({
positionType: "shift",
positionData: subShift,
children: vlistElem,
}, options);
} else if (supm) {
// Rule 18c, d
supShift = Math.max(supShift, minSupShift,
supm.depth + 0.25 * metrics.xHeight);
supsub = makeVList({
positionType: "shift",
positionData: -supShift,
children: [{type: "elem" as const, elem: supm, marginRight}],
}, options);
} else {
throw new Error("supsub must have either sup or sub.");
}
// Wrap the supsub vlist in a span.msupsub to reset text-align.
const mclass = html.getTypeOfDomTree(base, "right") || "mord";
return makeSpan([mclass],
[base, makeSpan(["msupsub"], [supsub])],
options);
},
mathmlBuilder(group, options) {
// Is the inner group a relevant horizontal brace?
let isBrace = false;
let isOver;
let isSup;
if (group.base && group.base.type === "horizBrace") {
isSup = !!group.sup;
if (isSup === group.base.isOver) {
isBrace = true;
isOver = group.base.isOver;
}
}
if (group.base &&
(group.base.type === "op" || group.base.type === "operatorname")) {
group.base.parentIsSupSub = true;
}
const children = [mml.buildGroup(group.base, options)];
if (group.sub) {
children.push(mml.buildGroup(group.sub, options));
}
if (group.sup) {
children.push(mml.buildGroup(group.sup, options));
}
let nodeType: MathNodeType;
if (isBrace) {
nodeType = (isOver ? "mover" : "munder");
} else if (!group.sub) {
const base = group.base;
if (base && base.type === "op" && base.limits &&
(options.style === Style.DISPLAY || base.alwaysHandleSupSub)) {
nodeType = "mover";
} else if (base && base.type === "operatorname" &&
base.alwaysHandleSupSub &&
(base.limits || options.style === Style.DISPLAY)) {
nodeType = "mover";
} else {
nodeType = "msup";
}
} else if (!group.sup) {
const base = group.base;
if (base && base.type === "op" && base.limits &&
(options.style === Style.DISPLAY || base.alwaysHandleSupSub)) {
nodeType = "munder";
} else if (base && base.type === "operatorname" &&
base.alwaysHandleSupSub &&
(base.limits || options.style === Style.DISPLAY)) {
nodeType = "munder";
} else {
nodeType = "msub";
}
} else {
const base = group.base;
if (base && base.type === "op" && base.limits &&
options.style === Style.DISPLAY) {
nodeType = "munderover";
} else if (base && base.type === "operatorname" &&
base.alwaysHandleSupSub &&
(options.style === Style.DISPLAY || base.limits)) {
nodeType = "munderover";
} else {
nodeType = "msubsup";
}
}
return new MathNode(nodeType, children);
},
});
+32
View File
@@ -0,0 +1,32 @@
import {defineFunctionBuilders} from "../defineFunction";
import {mathsym} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import * as mml from "../buildMathML";
// Operator ParseNodes created in Parser.js from symbol Groups in src/symbols.js.
defineFunctionBuilders({
type: "atom",
htmlBuilder(group, options) {
return mathsym(
group.text, group.mode, options, ["m" + group.family]);
},
mathmlBuilder(group, options) {
const node = new MathNode(
"mo", [mml.makeText(group.text, group.mode)]);
if (group.family === "bin") {
const variant = mml.getVariant(group, options);
if (variant === "bold-italic") {
node.setAttribute("mathvariant", variant);
}
} else if (group.family === "punct") {
node.setAttribute("separator", "true");
} else if (group.family === "open" || group.family === "close") {
// Delims built here should not stretch vertically.
// See delimsizing.js for stretchy delims.
node.setAttribute("stretchy", "false");
}
return node;
},
});
+61
View File
@@ -0,0 +1,61 @@
import {defineFunctionBuilders} from "../defineFunction";
import {makeOrd} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import * as mml from "../buildMathML";
import type {ParseNode} from "../parseNode";
// "mathord" and "textord" ParseNodes created in Parser.js from symbol Groups in
// src/symbols.js.
const defaultVariant: Record<string, string> = {
"mi": "italic",
"mn": "normal",
"mtext": "normal",
};
defineFunctionBuilders({
type: "mathord",
htmlBuilder(group, options) {
return makeOrd(group, options, "mathord");
},
mathmlBuilder(group: ParseNode<"mathord">, options) {
const node = new MathNode(
"mi",
[mml.makeText(group.text, group.mode, options)]);
const variant = mml.getVariant(group, options) || "italic";
if (variant !== defaultVariant[node.type]) {
node.setAttribute("mathvariant", variant);
}
return node;
},
});
defineFunctionBuilders({
type: "textord",
htmlBuilder(group, options) {
return makeOrd(group, options, "textord");
},
mathmlBuilder(group: ParseNode<"textord">, options) {
const text = mml.makeText(group.text, group.mode, options);
const variant = mml.getVariant(group, options) || "normal";
let node;
if (group.mode === 'text') {
node = new MathNode("mtext", [text]);
} else if (/[0-9]/.test(group.text)) {
node = new MathNode("mn", [text]);
} else if (group.text === "\\prime") {
node = new MathNode("mo", [text]);
} else {
node = new MathNode("mi", [text]);
}
if (variant !== defaultVariant[node.type]) {
node.setAttribute("mathvariant", variant);
}
return node;
},
});
+72
View File
@@ -0,0 +1,72 @@
import {defineFunctionBuilders} from "../defineFunction";
import {mathsym, makeOrd, makeSpan} from "../buildCommon";
import {MathNode, TextNode} from "../mathMLTree";
import ParseError from "../ParseError";
// A map of CSS-based spacing functions to their CSS class.
const cssSpace: Record<string, string> = {
"\\nobreak": "nobreak",
"\\allowbreak": "allowbreak",
};
// A lookup table to determine whether a spacing function/symbol should be
// treated like a regular space character. If a symbol or command is a key
// in this table, then it should be a regular space character. Furthermore,
// the associated value may have a `className` specifying an extra CSS class
// to add to the created `span`.
const regularSpace: Record<string, {className?: string}> = {
" ": {},
"\\ ": {},
"~": {
className: "nobreak",
},
"\\space": {},
"\\nobreakspace": {
className: "nobreak",
},
};
// ParseNode<"spacing"> created in Parser.js from the "spacing" symbol Groups in
// src/symbols.js.
defineFunctionBuilders({
type: "spacing",
htmlBuilder(group, options) {
if (regularSpace.hasOwnProperty(group.text)) {
const className = regularSpace[group.text].className || "";
// Spaces are generated by adding an actual space. Each of these
// things has an entry in the symbols table, so these will be turned
// into appropriate outputs.
if (group.mode === "text") {
const ord = makeOrd(group, options, "textord");
ord.classes.push(className);
return ord;
} else {
return makeSpan(["mspace", className],
[mathsym(group.text, group.mode, options)],
options);
}
} else if (cssSpace.hasOwnProperty(group.text)) {
// Spaces based on just a CSS class.
return makeSpan(
["mspace", cssSpace[group.text]],
[], options);
} else {
throw new ParseError(`Unknown type of space "${group.text}"`);
}
},
mathmlBuilder(group, options) {
let node;
if (regularSpace.hasOwnProperty(group.text)) {
node = new MathNode(
"mtext", [new TextNode("\u00a0")]);
} else if (cssSpace.hasOwnProperty(group.text)) {
// CSS-based MathML spaces (\nobreak, \allowbreak) are ignored
return new MathNode("mspace");
} else {
throw new ParseError(`Unknown type of space "${group.text}"`);
}
return node;
},
});
+38
View File
@@ -0,0 +1,38 @@
import {defineFunctionBuilders} from "../defineFunction";
import {MathNode} from "../mathMLTree";
import * as mml from "../buildMathML";
const pad = () => {
const padNode = new MathNode("mtd", []);
padNode.setAttribute("width", "50%");
return padNode;
};
defineFunctionBuilders({
type: "tag",
mathmlBuilder(group, options) {
const table = new MathNode("mtable", [
new MathNode("mtr", [
pad(),
new MathNode("mtd", [
mml.buildExpressionRow(group.body, options),
]),
pad(),
new MathNode("mtd", [
mml.buildExpressionRow(group.tag, options),
]),
]),
]);
table.setAttribute("width", "100%");
return table;
// TODO: Left-aligned tags.
// Currently, the group and options passed here do not contain
// enough info to set tag alignment. `leqno` is in Settings but it is
// not passed to Options. On the HTML side, leqno is
// set by a CSS class applied in buildTree.js. That would have worked
// in MathML if browsers supported <mlabeledtr>. Since they don't, we
// need to rewrite the way this function is called.
},
});
+77
View File
@@ -0,0 +1,77 @@
import defineFunction, {ordargument} from "../defineFunction";
import {makeSpan} from "../buildCommon";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
import type Options from "../Options";
import type {ParseNode} from "../parseNode";
// Non-mathy text, possibly in a font
const textFontFamilies: Record<string, string | undefined> = {
"\\text": undefined, "\\textrm": "textrm", "\\textsf": "textsf",
"\\texttt": "texttt", "\\textnormal": "textrm",
};
const textFontWeights: Record<string, "textbf" | "textmd"> = {
"\\textbf": "textbf",
"\\textmd": "textmd",
};
const textFontShapes: Record<string, "textit" | "textup"> = {
"\\textit": "textit",
"\\textup": "textup",
};
const optionsWithFont = (group: ParseNode<"text">, options: Options): Options => {
const font = group.font;
// Checks if the argument is a font family or a font style.
if (!font) {
return options;
} else if (textFontFamilies[font]) {
return options.withTextFontFamily(textFontFamilies[font]);
} else if (textFontWeights[font]) {
return options.withTextFontWeight(textFontWeights[font]);
} else if (font === "\\emph") {
return options.fontShape === "textit" ?
options.withTextFontShape("textup") :
options.withTextFontShape("textit");
}
return options.withTextFontShape(textFontShapes[font] as "textit" | "textup");
};
defineFunction({
type: "text",
names: [
// Font families
"\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal",
// Font weights
"\\textbf", "\\textmd",
// Font Shapes
"\\textit", "\\textup", "\\emph",
],
props: {
numArgs: 1,
argTypes: ["text"],
allowedInArgument: true,
allowedInText: true,
},
handler({parser, funcName}, args) {
const body = args[0];
return {
type: "text",
mode: parser.mode,
body: ordargument(body),
font: funcName,
};
},
htmlBuilder(group, options) {
const newOptions = optionsWithFont(group, options);
const inner = html.buildExpression(group.body, newOptions, true);
return makeSpan(["mord", "text"], inner, newOptions);
},
mathmlBuilder(group, options) {
const newOptions = optionsWithFont(group, options);
return mml.buildExpressionRow(group.body, newOptions);
},
});
+57
View File
@@ -0,0 +1,57 @@
import defineFunction from "../defineFunction";
import {makeLineSpan, makeSpan, makeVList} from "../buildCommon";
import {MathNode, TextNode} from "../mathMLTree";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
defineFunction({
type: "underline",
names: ["\\underline"],
props: {
numArgs: 1,
allowedInText: true,
},
handler({parser}, args) {
return {
type: "underline",
mode: parser.mode,
body: args[0],
};
},
htmlBuilder(group, options) {
// Underlines are handled in the TeXbook pg 443, Rule 10.
// Build the inner group.
const innerGroup = html.buildGroup(group.body, options);
// Create the line to go below the body
const line = makeLineSpan("underline-line", options);
// Generate the vlist, with the appropriate kerns
const defaultRuleThickness = options.fontMetrics().defaultRuleThickness;
const vlist = makeVList({
positionType: "top",
positionData: innerGroup.height,
children: [
{type: "kern", size: defaultRuleThickness},
{type: "elem", elem: line},
{type: "kern", size: 3 * defaultRuleThickness},
{type: "elem", elem: innerGroup},
],
}, options);
return makeSpan(["mord", "underline"], [vlist], options);
},
mathmlBuilder(group, options) {
const operator = new MathNode(
"mo", [new TextNode("\u203e")]);
operator.setAttribute("stretchy", "true");
const node = new MathNode(
"munder",
[mml.buildGroup(group.body, options), operator]);
node.setAttribute("accentunder", "true");
return node;
},
});
+119
View File
@@ -0,0 +1,119 @@
import {makeSpan, makeVList} from "../../buildCommon";
import * as html from "../../buildHTML";
import {isCharacterBox} from "../../utils";
import type {StyleInterface} from "../../Style";
import type Options from "../../Options";
import type {DomSpan, SymbolNode} from "../../domTree";
import type {AnyParseNode} from "../../parseNode";
import {makeEm} from "../../units";
// For an operator with limits, assemble the base, sup, and sub into a span.
export const assembleSupSub = (
base: DomSpan | SymbolNode,
supGroup: AnyParseNode | null | undefined,
subGroup: AnyParseNode | null | undefined,
options: Options,
style: StyleInterface,
slant: number,
baseShift: number,
): DomSpan => {
base = makeSpan([], [base]);
const subIsSingleCharacter = subGroup && isCharacterBox(subGroup);
let sub;
let sup;
// We manually have to handle the superscripts and subscripts. This,
// aside from the kern calculations, is copied from supsub.
if (supGroup) {
const elem = html.buildGroup(
supGroup, options.havingStyle(style.sup()), options);
sup = {
elem,
kern: Math.max(
options.fontMetrics().bigOpSpacing1,
options.fontMetrics().bigOpSpacing3 - elem.depth),
};
}
if (subGroup) {
const elem = html.buildGroup(
subGroup, options.havingStyle(style.sub()), options);
sub = {
elem,
kern: Math.max(
options.fontMetrics().bigOpSpacing2,
options.fontMetrics().bigOpSpacing4 - elem.height),
};
}
// Build the final group as a vlist of the possible subscript, base,
// and possible superscript.
let finalGroup;
if (sup && sub) {
const bottom = options.fontMetrics().bigOpSpacing5 +
sub.elem.height + sub.elem.depth +
sub.kern +
base.depth + baseShift;
finalGroup = makeVList({
positionType: "bottom",
positionData: bottom,
children: [
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
{type: "elem", elem: sub.elem, marginLeft: makeEm(-slant)},
{type: "kern", size: sub.kern},
{type: "elem", elem: base},
{type: "kern", size: sup.kern},
{type: "elem", elem: sup.elem, marginLeft: makeEm(slant)},
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
],
}, options);
} else if (sub) {
const top = base.height - baseShift;
// Shift the limits by the slant of the symbol. Note
// that we are supposed to shift the limits by 1/2 of the slant,
// but since we are centering the limits adding a full slant of
// margin will shift by 1/2 that.
finalGroup = makeVList({
positionType: "top",
positionData: top,
children: [
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
{type: "elem", elem: sub.elem, marginLeft: makeEm(-slant)},
{type: "kern", size: sub.kern},
{type: "elem", elem: base},
],
}, options);
} else if (sup) {
const bottom = base.depth + baseShift;
finalGroup = makeVList({
positionType: "bottom",
positionData: bottom,
children: [
{type: "elem", elem: base},
{type: "kern", size: sup.kern},
{type: "elem", elem: sup.elem, marginLeft: makeEm(slant)},
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
],
}, options);
} else {
// This case probably shouldn't occur (this would mean the
// supsub was sending us a group with no superscript or
// subscript) but be safe.
return base;
}
const parts = [finalGroup];
if (sub && slant !== 0 && !subIsSingleCharacter) {
// A negative margin-left was applied to the lower limit.
// Avoid an overlap by placing a spacer on the left on the group.
const spacer = makeSpan(["mspace"], [], options);
spacer.style.marginRight = makeEm(slant);
parts.unshift(spacer);
}
return makeSpan(["mop", "op-limits"], parts, options);
};
+44
View File
@@ -0,0 +1,44 @@
import defineFunction from "../defineFunction";
import {makeVList} from "../buildCommon";
import {MathNode} from "../mathMLTree";
import * as html from "../buildHTML";
import * as mml from "../buildMathML";
// \vcenter: Vertically center the argument group on the math axis.
defineFunction({
type: "vcenter",
names: ["\\vcenter"],
props: {
numArgs: 1,
argTypes: ["original"], // In LaTeX, \vcenter can act only on a box.
allowedInText: false,
},
handler({parser}, args) {
return {
type: "vcenter",
mode: parser.mode,
body: args[0],
};
},
htmlBuilder(group, options) {
const body = html.buildGroup(group.body, options);
const axisHeight = options.fontMetrics().axisHeight;
const dy = 0.5 * ((body.height - axisHeight) - (body.depth + axisHeight));
return makeVList({
positionType: "shift",
positionData: dy,
children: [{type: "elem", elem: body}],
}, options);
},
mathmlBuilder(group, options) {
// There is no way to do this in MathML.
// Write a class as a breadcrumb in case some post-processor wants
// to perform a vcenter adjustment.
// Wrap in mrow to ensure valid MathML when placed inside mo (e.g., \mathrel)
const mpadded = new MathNode(
"mpadded", [mml.buildGroup(group.body, options)], ["vcenter"]);
return new MathNode("mrow", [mpadded]);
},
});
+57
View File
@@ -0,0 +1,57 @@
import defineFunction from "../defineFunction";
import {makeSpan, makeSymbol, tryCombineChars} from "../buildCommon";
import {MathNode, TextNode} from "../mathMLTree";
import ParseError from "../ParseError";
import type {ParseNode} from "../parseNode";
defineFunction({
type: "verb",
names: ["\\verb"],
props: {
numArgs: 0,
allowedInText: true,
},
handler(context, args, optArgs) {
// \verb and \verb* are dealt with directly in Parser.js.
// If we end up here, it's because of a failure to match the two delimiters
// in the regex in Lexer.js. LaTeX raises the following error when \verb is
// terminated by end of line (or file).
throw new ParseError(
"\\verb ended by end of line instead of matching delimiter");
},
htmlBuilder(group, options) {
const text = makeVerb(group);
const body = [];
// \verb enters text mode and therefore is sized like \textstyle
const newOptions = options.havingStyle(options.style.text());
for (let i = 0; i < text.length; i++) {
let c = text[i];
if (c === '~') {
c = '\\textasciitilde';
}
body.push(makeSymbol(c, "Typewriter-Regular",
group.mode, newOptions, ["mord", "texttt"]));
}
return makeSpan(
["mord", "text"].concat(newOptions.sizingClasses(options)),
tryCombineChars(body),
newOptions,
);
},
mathmlBuilder(group, options) {
const text = new TextNode(makeVerb(group));
const node = new MathNode("mtext", [text]);
node.setAttribute("mathvariant", "monospace");
return node;
},
});
/**
* Converts verb group into body string.
*
* \verb* replaces each space with an open box \u2423
* \verb replaces each space with a no-break space \xA0
*/
const makeVerb = (group: ParseNode<"verb">): string =>
group.body.replace(/ /g, group.star ? '\u2423' : '\xA0');