+285
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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", // can’t 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", // can’t 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", // can’t 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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", // can’t 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>⁡</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
@@ -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 ⁡
|
||||
// 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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');
|
||||
Reference in New Issue
Block a user