Support \operatorname* (#1899)
* Support \operatorname* * Fix lint errors * Fix YAML * Update screenshots * Break out a function to avoid code duplication * Fix lint errors * Restore wrapper span * Update docs * Reinstall color macros lost in merge * Update screenshots * Add type annotations, Move to utils file, add \limits to screenshots * Fix lint errors * Rearrange screen shot to fit onto page * Update screenshots * tweak location of utils.js and assembleSupSup.js
@@ -760,7 +760,7 @@ use `\ce` instead|
|
||||
|\omicron|$\omicron$||
|
||||
|\ominus|$\ominus$||
|
||||
|\operatorname|$\operatorname{asin} x$|\operatorname{asin} x|
|
||||
|\operatorname\*|<span style="color:firebrick;">Not supported</span>|[Issue #1242](https://github.com/KaTeX/KaTeX/issues/1242)|
|
||||
|\operatorname\*|$\operatorname{asin}\limits_y x$|`\operatorname{asin}\limits_y x`|
|
||||
|\oplus|$\oplus$||
|
||||
|\or|<span style="color:firebrick;">Not supported</span>||
|
||||
|\oslash|$\oslash$||
|
||||
|
@@ -364,8 +364,8 @@ Direct Input: $+ - / * ⋅ ± × ÷ ∓ ∔ ∧ ∨ ∩ ∪ ≀ ⊎ ⊓ ⊔ ⊕
|
||||
| $\cos$ `\cos` | $\exp$ `\exp` | $\tanh$ `\tanh`| $\min$ `\min` |
|
||||
| $\cosec$ `\cosec` | $\hom$ `\hom` | $\tg$ `\tg` | $\Pr$ `\Pr` |
|
||||
| $\cosh$ `\cosh`| $\ker$ `\ker` | $\th$ `\th` | $\sup$ `\sup` |
|
||||
| $\cot$ `\cot` | $\lg$ `\lg`| $\operatorname{f}$ `\operatorname{f}` | $\argmax$ `\argmax` |
|
||||
| $\argmin$ `\argmin`| $\plim$ `\plim` |
|
||||
| $\cot$ `\cot` | $\lg$ `\lg`| $\argmax$ `\argmax` | $\argmin$ `\argmin` |
|
||||
| $\plim$ `\plim` | $\operatorname{f}$ `\operatorname{f}`| $\operatorname*{f}$ `\operatorname*{f}`| |
|
||||
|
||||
Functions on the right column of this table can take `\limits`.
|
||||
|
||||
|
@@ -51,6 +51,7 @@ const tokenRegexString = `(${spaceRegexString}+)|` + // whitespace
|
||||
`${combiningDiacriticalMarkString}*` + // ...plus accents
|
||||
"|\\\\verb\\*([^]).*?\\3" + // \verb*
|
||||
"|\\\\verb([^*a-zA-Z]).*?\\4" + // \verb unstarred
|
||||
"|\\\\operatorname\\*" + // \operatorname*
|
||||
`|${controlWordWhitespaceRegexString}` + // \macroName + spaces
|
||||
`|${controlSymbolRegexString})`; // \\, \', etc.
|
||||
|
||||
|
@@ -315,15 +315,21 @@ export default class Parser {
|
||||
|
||||
if (lex.text === "\\limits" || lex.text === "\\nolimits") {
|
||||
// We got a limit control
|
||||
const opNode = checkNodeType(base, "op");
|
||||
let opNode = checkNodeType(base, "op");
|
||||
if (opNode) {
|
||||
const limits = lex.text === "\\limits";
|
||||
opNode.limits = limits;
|
||||
opNode.alwaysHandleSupSub = true;
|
||||
} else {
|
||||
throw new ParseError(
|
||||
"Limit controls must follow a math operator",
|
||||
lex);
|
||||
opNode = checkNodeType(base, "operatorname");
|
||||
if (opNode && opNode.alwaysHandleSupSub) {
|
||||
const limits = lex.text === "\\limits";
|
||||
opNode.limits = limits;
|
||||
} else {
|
||||
throw new ParseError(
|
||||
"Limit controls must follow a math operator",
|
||||
lex);
|
||||
}
|
||||
}
|
||||
this.consume();
|
||||
} else if (lex.text === "^") {
|
||||
|
@@ -6,6 +6,7 @@ import {SymbolNode} from "../domTree";
|
||||
import * as mathMLTree from "../mathMLTree";
|
||||
import utils from "../utils";
|
||||
import Style from "../Style";
|
||||
import {assembleSupSub} from "./utils/assembleSupSub";
|
||||
import {assertNodeType, checkNodeType} from "../parseNode";
|
||||
|
||||
import * as html from "../buildHTML";
|
||||
@@ -131,99 +132,9 @@ export const htmlBuilder: HtmlBuilderSupSub<"op"> = (grp, options) => {
|
||||
}
|
||||
|
||||
if (hasLimits) {
|
||||
// IE 8 clips \int if it is in a display: inline-block. We wrap it
|
||||
// in a new span so it is an inline, and works.
|
||||
base = buildCommon.makeSpan([], [base]);
|
||||
return assembleSupSub(base, supGroup, subGroup, options,
|
||||
style, slant, baseShift);
|
||||
|
||||
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 = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: sub.elem, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: sub.kern},
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: sup.kern},
|
||||
{type: "elem", elem: sup.elem, marginLeft: slant + "em"},
|
||||
{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 = buildCommon.makeVList({
|
||||
positionType: "top",
|
||||
positionData: top,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: sub.elem, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: sub.kern},
|
||||
{type: "elem", elem: base},
|
||||
],
|
||||
}, options);
|
||||
} else if (sup) {
|
||||
const bottom = base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: sup.kern},
|
||||
{type: "elem", elem: sup.elem, marginLeft: slant + "em"},
|
||||
{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;
|
||||
}
|
||||
|
||||
return buildCommon.makeSpan(
|
||||
["mop", "op-limits"], [finalGroup], options);
|
||||
} else {
|
||||
if (baseShift) {
|
||||
base.style.position = "relative";
|
||||
@@ -251,8 +162,6 @@ const mathmlBuilder: MathMLBuilder<"op"> = (group, options) => {
|
||||
} else {
|
||||
// This is a text operator. Add all of the characters from the
|
||||
// operator's name.
|
||||
// TODO(emily): Add a space in the middle of some of these
|
||||
// operators, like \limsup.
|
||||
node = new mathMLTree.MathNode(
|
||||
"mi", [new mathMLTree.TextNode(group.name.slice(1))]);
|
||||
// Append an <mo>⁡</mo>.
|
||||
|
@@ -3,115 +3,160 @@ import defineFunction, {ordargument} from "../defineFunction";
|
||||
import buildCommon from "../buildCommon";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
import {SymbolNode} from "../domTree";
|
||||
import {assembleSupSub} from "./utils/assembleSupSub";
|
||||
import {assertNodeType, checkNodeType} 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
|
||||
// "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">;
|
||||
const supSub = checkNodeType(grp, "supsub");
|
||||
if (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 = supSub.sup;
|
||||
subGroup = supSub.sub;
|
||||
group = assertNodeType(supSub.base, "operatorname");
|
||||
hasLimits = true;
|
||||
} else {
|
||||
group = assertNodeType(grp, "operatorname");
|
||||
}
|
||||
|
||||
let base;
|
||||
if (group.body.length > 0) {
|
||||
const body = group.body.map(child => {
|
||||
// $FlowFixMe: Check if the node has a string `text` property.
|
||||
const childText = child.text;
|
||||
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 = buildCommon.makeSpan(["mop"], expression, options);
|
||||
} else {
|
||||
base = buildCommon.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 = 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 mathMLTree.SpaceNode) {
|
||||
// Do nothing
|
||||
} else if (node instanceof mathMLTree.MathNode) {
|
||||
switch (node.type) {
|
||||
case "mi":
|
||||
case "mn":
|
||||
case "ms":
|
||||
case "mspace":
|
||||
case "mtext":
|
||||
break; // Do nothing yet.
|
||||
case "mo": {
|
||||
const child = node.children[0];
|
||||
if (node.children.length === 1 &&
|
||||
child instanceof mathMLTree.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 mathMLTree.TextNode(word)];
|
||||
}
|
||||
|
||||
const identifier = new mathMLTree.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 mathMLTree.MathNode("mo",
|
||||
[mml.makeText("\u2061", "text")]);
|
||||
|
||||
if (group.parentIsSupSub) {
|
||||
return new mathMLTree.MathNode("mo", [identifier, operator]);
|
||||
} else {
|
||||
return mathMLTree.newDocumentFragment([identifier, operator]);
|
||||
}
|
||||
};
|
||||
|
||||
// \operatorname
|
||||
// amsopn.dtx: \mathop{#1\kern\z@\operator@font#3}\newmcodes@
|
||||
defineFunction({
|
||||
type: "operatorname",
|
||||
names: ["\\operatorname"],
|
||||
names: ["\\operatorname", "\\operatorname*"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
},
|
||||
handler: ({parser}, args) => {
|
||||
handler: ({parser, funcName}, args) => {
|
||||
const body = args[0];
|
||||
return {
|
||||
type: "operatorname",
|
||||
mode: parser.mode,
|
||||
body: ordargument(body),
|
||||
alwaysHandleSupSub: (funcName === "\\operatorname*"),
|
||||
limits: false,
|
||||
parentIsSupSub: false,
|
||||
};
|
||||
},
|
||||
|
||||
htmlBuilder: (group, options) => {
|
||||
if (group.body.length > 0) {
|
||||
const body = group.body.map(child => {
|
||||
// $FlowFixMe: Check if the node has a string `text` property.
|
||||
const childText = child.text;
|
||||
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/, "*");
|
||||
}
|
||||
}
|
||||
return buildCommon.makeSpan(["mop"], expression, options);
|
||||
} else {
|
||||
return buildCommon.makeSpan(["mop"], [], options);
|
||||
}
|
||||
},
|
||||
|
||||
mathmlBuilder: (group, options) => {
|
||||
// The steps taken here are similar to the html version.
|
||||
let expression = 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 mathMLTree.SpaceNode) {
|
||||
// Do nothing
|
||||
} else if (node instanceof mathMLTree.MathNode) {
|
||||
switch (node.type) {
|
||||
case "mi":
|
||||
case "mn":
|
||||
case "ms":
|
||||
case "mspace":
|
||||
case "mtext":
|
||||
break; // Do nothing yet.
|
||||
case "mo": {
|
||||
const child = node.children[0];
|
||||
if (node.children.length === 1 &&
|
||||
child instanceof mathMLTree.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 mathMLTree.TextNode(word)];
|
||||
}
|
||||
|
||||
const identifier = new mathMLTree.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 mathMLTree.MathNode("mo",
|
||||
[mml.makeText("\u2061", "text")]);
|
||||
|
||||
return mathMLTree.newDocumentFragment([identifier, operator]);
|
||||
},
|
||||
htmlBuilder,
|
||||
mathmlBuilder,
|
||||
});
|
||||
|
||||
|
@@ -12,6 +12,7 @@ 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";
|
||||
@@ -39,6 +40,10 @@ const htmlBuilderDelegate = function(
|
||||
(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 utils.isCharacterBox(base.base) ? accent.htmlBuilder : null;
|
||||
} else if (base.type === "horizBrace") {
|
||||
@@ -201,7 +206,8 @@ defineFunctionBuilders({
|
||||
}
|
||||
}
|
||||
|
||||
if (group.base && group.base.type === "op") {
|
||||
if (group.base &&
|
||||
(group.base.type === "op" || group.base.type === "operatorname")) {
|
||||
group.base.parentIsSupSub = true;
|
||||
}
|
||||
|
||||
@@ -223,6 +229,10 @@ defineFunctionBuilders({
|
||||
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";
|
||||
}
|
||||
@@ -231,6 +241,10 @@ defineFunctionBuilders({
|
||||
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";
|
||||
}
|
||||
@@ -239,6 +253,10 @@ defineFunctionBuilders({
|
||||
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";
|
||||
}
|
||||
|
112
src/functions/utils/assembleSupSub.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// @flow
|
||||
import buildCommon from "../../buildCommon";
|
||||
import * as html from "../../buildHTML";
|
||||
import type {StyleInterface} from "../../Style";
|
||||
import type Options from "../../Options";
|
||||
import type {DomSpan, SymbolNode} from "../../domTree";
|
||||
import type {AnyParseNode} from "../../parseNode";
|
||||
|
||||
// For an operator with limits, assemble the base, sup, and sub into a span.
|
||||
|
||||
export const assembleSupSub = (
|
||||
base: DomSpan | SymbolNode,
|
||||
supGroup: ?AnyParseNode,
|
||||
subGroup: ?AnyParseNode,
|
||||
options: Options,
|
||||
style: StyleInterface,
|
||||
slant: number,
|
||||
baseShift: number,
|
||||
): DomSpan => {
|
||||
// IE 8 clips \int if it is in a display: inline-block. We wrap it
|
||||
// in a new span so it is an inline, and works.
|
||||
base = buildCommon.makeSpan([], [base]);
|
||||
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 = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: sub.elem, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: sub.kern},
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: sup.kern},
|
||||
{type: "elem", elem: sup.elem, marginLeft: slant + "em"},
|
||||
{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 = buildCommon.makeVList({
|
||||
positionType: "top",
|
||||
positionData: top,
|
||||
children: [
|
||||
{type: "kern", size: options.fontMetrics().bigOpSpacing5},
|
||||
{type: "elem", elem: sub.elem, marginLeft: -slant + "em"},
|
||||
{type: "kern", size: sub.kern},
|
||||
{type: "elem", elem: base},
|
||||
],
|
||||
}, options);
|
||||
} else if (sup) {
|
||||
const bottom = base.depth + baseShift;
|
||||
|
||||
finalGroup = buildCommon.makeVList({
|
||||
positionType: "bottom",
|
||||
positionData: bottom,
|
||||
children: [
|
||||
{type: "elem", elem: base},
|
||||
{type: "kern", size: sup.kern},
|
||||
{type: "elem", elem: sup.elem, marginLeft: slant + "em"},
|
||||
{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;
|
||||
}
|
||||
|
||||
return buildCommon.makeSpan(
|
||||
["mop", "op-limits"], [finalGroup], options);
|
||||
};
|
@@ -815,8 +815,8 @@ defineMacro("\\approxcoloncolon",
|
||||
|
||||
// Present in newtxmath, pxfonts and txfonts
|
||||
defineMacro("\\notni", "\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220C}}");
|
||||
defineMacro("\\limsup", "\\DOTSB\\mathop{\\operatorname{lim\\,sup}}\\limits");
|
||||
defineMacro("\\liminf", "\\DOTSB\\mathop{\\operatorname{lim\\,inf}}\\limits");
|
||||
defineMacro("\\limsup", "\\DOTSB\\operatorname*{lim\\,sup}");
|
||||
defineMacro("\\liminf", "\\DOTSB\\operatorname*{lim\\,inf}");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// MathML alternates for KaTeX glyphs in the Unicode private area
|
||||
@@ -942,8 +942,8 @@ defineMacro("\\Zeta", "\\mathrm{Z}");
|
||||
// statmath.sty
|
||||
// https://ctan.math.illinois.edu/macros/latex/contrib/statmath/statmath.pdf
|
||||
|
||||
defineMacro("\\argmin", "\\DOTSB\\mathop{\\operatorname{arg\\,min}}\\limits");
|
||||
defineMacro("\\argmax", "\\DOTSB\\mathop{\\operatorname{arg\\,max}}\\limits");
|
||||
defineMacro("\\argmin", "\\DOTSB\\operatorname*{arg\\,min}");
|
||||
defineMacro("\\argmax", "\\DOTSB\\operatorname*{arg\\,max}");
|
||||
defineMacro("\\plim", "\\DOTSB\\mathop{\\operatorname{plim}}\\limits");
|
||||
|
||||
// Custom Khan Academy colors, should be moved to an optional package
|
||||
|
@@ -358,6 +358,9 @@ type ParseNodeTypes = {
|
||||
mode: Mode,
|
||||
loc?: ?SourceLocation,
|
||||
body: AnyParseNode[],
|
||||
alwaysHandleSupSub: boolean,
|
||||
limits: boolean,
|
||||
parentIsSupSub: boolean,
|
||||
|},
|
||||
"overline": {|
|
||||
type: "overline",
|
||||
|
@@ -327,7 +327,7 @@ exports[`A MathML builder should output \\limsup_{x \\rightarrow \\infty} correc
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<semantics>
|
||||
<mrow>
|
||||
<munder>
|
||||
<msub>
|
||||
<mo>
|
||||
<mi mathvariant="normal">
|
||||
lim sup
|
||||
@@ -347,7 +347,7 @@ exports[`A MathML builder should output \\limsup_{x \\rightarrow \\infty} correc
|
||||
∞
|
||||
</mi>
|
||||
</mrow>
|
||||
</munder>
|
||||
</msub>
|
||||
</mrow>
|
||||
<annotation encoding="application/x-tex">
|
||||
\\limsup_{x \\rightarrow \\infty}
|
||||
|
@@ -2651,6 +2651,9 @@ describe("An aligned environment", function() {
|
||||
describe("operatorname support", function() {
|
||||
it("should not fail", function() {
|
||||
expect("\\operatorname{x*Π∑\\Pi\\sum\\frac a b}").toBuild();
|
||||
expect("\\operatorname*{x*Π∑\\Pi\\sum\\frac a b}").toBuild();
|
||||
expect("\\operatorname*{x*Π∑\\Pi\\sum\\frac a b}_y x").toBuild();
|
||||
expect("\\operatorname*{x*Π∑\\Pi\\sum\\frac a b}\\limits_y x").toBuild();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3218,11 +3221,11 @@ describe("A macro expander", function() {
|
||||
});
|
||||
|
||||
it("should expand \\limsup as expected", () => {
|
||||
expect`\limsup`.toParseLike`\mathop{\operatorname{lim\,sup}}\limits`;
|
||||
expect`\limsup`.toParseLike`\operatorname*{lim\,sup}`;
|
||||
});
|
||||
|
||||
it("should expand \\liminf as expected", () => {
|
||||
expect`\liminf`.toParseLike`\mathop{\operatorname{lim\,inf}}\limits`;
|
||||
expect`\liminf`.toParseLike`\operatorname*{lim\,inf}`;
|
||||
});
|
||||
|
||||
it("should expand \\plim as expected", () => {
|
||||
@@ -3230,11 +3233,11 @@ describe("A macro expander", function() {
|
||||
});
|
||||
|
||||
it("should expand \\argmin as expected", () => {
|
||||
expect`\argmin`.toParseLike`\mathop{\operatorname{arg\,min}}\limits`;
|
||||
expect`\argmin`.toParseLike`\operatorname*{arg\,min}`;
|
||||
});
|
||||
|
||||
it("should expand \\argmax as expected", () => {
|
||||
expect`\argmax`.toParseLike`\mathop{\operatorname{arg\,max}}\limits`;
|
||||
expect`\argmax`.toParseLike`\operatorname*{arg\,max}`;
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 21 KiB |
@@ -271,14 +271,16 @@ OldFont: |
|
||||
OperatorName: |
|
||||
\begin{matrix}
|
||||
\operatorname g (z) + 5\operatorname{g}z + \operatorname{Gam-ma}(z) \\
|
||||
\operatorname{Gam ma}(z) + \operatorname{\Gamma}(z) + \operatorname{}x
|
||||
\operatorname{Gam ma}(z) + \operatorname{\Gamma}(z) + \operatorname{}x \\
|
||||
\operatorname*{asin} x + \operatorname*{asin}_y x + \operatorname*{asin}\limits_y x \\[0.3em]
|
||||
{\displaystyle \operatorname*{asin}_y x}
|
||||
\end{matrix}
|
||||
OpLimits: |
|
||||
\begin{matrix}
|
||||
{\sin_2^2 \lim_2^2 \int_2^2 \sum_2^2}
|
||||
{\displaystyle \lim_2^2 \int_2^2 \intop_2^2 \sum_2^2} \\
|
||||
\limsup_{x \rightarrow \infty} x \stackrel{?}= \liminf_{x \rightarrow \infty} x \\
|
||||
\displaystyle \sum_{\substack{0<i<m\\0<j<n}}
|
||||
{\displaystyle \limsup_{x \rightarrow \infty} x\:\: \sum_{\substack{0<i<m\\0<j<n}}}
|
||||
\end{matrix}
|
||||
OverUnderline: x\underline{x}\underline{\underline{x}}\underline{x_{x_{x_x}}}\underline{x^{x^{x^x}}}\overline{x}\overline{x}\overline{x^{x^{x^x}}} \blue{\overline{\underline{x}}\underline{\overline{x}}}
|
||||
OverUnderset: |
|
||||
|