UNPKG

14.4 kBJavaScriptView Raw
1// @flow
2// Limits, symbols
3import defineFunction, {ordargument} from "../defineFunction";
4import buildCommon from "../buildCommon";
5import {SymbolNode} from "../domTree";
6import * as mathMLTree from "../mathMLTree";
7import utils from "../utils";
8import Style from "../Style";
9import {assertNodeType, checkNodeType} from "../parseNode";
10
11import * as html from "../buildHTML";
12import * as mml from "../buildMathML";
13
14import type {HtmlBuilderSupSub, MathMLBuilder} from "../defineFunction";
15import type {ParseNode} from "../parseNode";
16
17// Most operators have a large successor symbol, but these don't.
18const noSuccessor = [
19 "\\smallint",
20];
21
22// NOTE: Unlike most `htmlBuilder`s, this one handles not only "op", but also
23// "supsub" since some of them (like \int) can affect super/subscripting.
24export const htmlBuilder: HtmlBuilderSupSub<"op"> = (grp, options) => {
25 // Operators are handled in the TeXbook pg. 443-444, rule 13(a).
26 let supGroup;
27 let subGroup;
28 let hasLimits = false;
29 let group: ParseNode<"op">;
30 const supSub = checkNodeType(grp, "supsub");
31 if (supSub) {
32 // If we have limits, supsub will pass us its group to handle. Pull
33 // out the superscript and subscript and set the group to the op in
34 // its base.
35 supGroup = supSub.sup;
36 subGroup = supSub.sub;
37 group = assertNodeType(supSub.base, "op");
38 hasLimits = true;
39 } else {
40 group = assertNodeType(grp, "op");
41 }
42
43 const style = options.style;
44
45 let large = false;
46 if (style.size === Style.DISPLAY.size &&
47 group.symbol &&
48 !utils.contains(noSuccessor, group.name)) {
49
50 // Most symbol operators get larger in displaystyle (rule 13)
51 large = true;
52 }
53
54 let base;
55 if (group.symbol) {
56 // If this is a symbol, create the symbol.
57 const fontName = large ? "Size2-Regular" : "Size1-Regular";
58
59 let stash = "";
60 if (group.name === "\\oiint" || group.name === "\\oiiint") {
61 // No font glyphs yet, so use a glyph w/o the oval.
62 // TODO: When font glyphs are available, delete this code.
63 stash = group.name.substr(1);
64 // $FlowFixMe
65 group.name = stash === "oiint" ? "\\iint" : "\\iiint";
66 }
67
68 base = buildCommon.makeSymbol(
69 group.name, fontName, "math", options,
70 ["mop", "op-symbol", large ? "large-op" : "small-op"]);
71
72 if (stash.length > 0) {
73 // We're in \oiint or \oiiint. Overlay the oval.
74 // TODO: When font glyphs are available, delete this code.
75 const italic = base.italic;
76 const oval = buildCommon.staticSvg(stash + "Size"
77 + (large ? "2" : "1"), options);
78 base = buildCommon.makeVList({
79 positionType: "individualShift",
80 children: [
81 {type: "elem", elem: base, shift: 0},
82 {type: "elem", elem: oval, shift: large ? 0.08 : 0},
83 ],
84 }, options);
85 // $FlowFixMe
86 group.name = "\\" + stash;
87 base.classes.unshift("mop");
88 // $FlowFixMe
89 base.italic = italic;
90 }
91 } else if (group.body) {
92 // If this is a list, compose that list.
93 const inner = html.buildExpression(group.body, options, true);
94 if (inner.length === 1 && inner[0] instanceof SymbolNode) {
95 base = inner[0];
96 base.classes[0] = "mop"; // replace old mclass
97 } else {
98 base = buildCommon.makeSpan(
99 ["mop"], buildCommon.tryCombineChars(inner), options);
100 }
101 } else {
102 // Otherwise, this is a text operator. Build the text from the
103 // operator's name.
104 // TODO(emily): Add a space in the middle of some of these
105 // operators, like \limsup
106 const output = [];
107 for (let i = 1; i < group.name.length; i++) {
108 output.push(buildCommon.mathsym(group.name[i], group.mode));
109 }
110 base = buildCommon.makeSpan(["mop"], output, options);
111 }
112
113 // If content of op is a single symbol, shift it vertically.
114 let baseShift = 0;
115 let slant = 0;
116 if ((base instanceof SymbolNode
117 || group.name === "\\oiint" || group.name === "\\oiiint")
118 && !group.suppressBaseShift) {
119 // We suppress the shift of the base of \overset and \underset. Otherwise,
120 // shift the symbol so its center lies on the axis (rule 13). It
121 // appears that our fonts have the centers of the symbols already
122 // almost on the axis, so these numbers are very small. Note we
123 // don't actually apply this here, but instead it is used either in
124 // the vlist creation or separately when there are no limits.
125 baseShift = (base.height - base.depth) / 2 -
126 options.fontMetrics().axisHeight;
127
128 // The slant of the symbol is just its italic correction.
129 // $FlowFixMe
130 slant = base.italic;
131 }
132
133 if (hasLimits) {
134 // IE 8 clips \int if it is in a display: inline-block. We wrap it
135 // in a new span so it is an inline, and works.
136 base = buildCommon.makeSpan([], [base]);
137
138 let sub;
139 let sup;
140 // We manually have to handle the superscripts and subscripts. This,
141 // aside from the kern calculations, is copied from supsub.
142 if (supGroup) {
143 const elem = html.buildGroup(
144 supGroup, options.havingStyle(style.sup()), options);
145
146 sup = {
147 elem,
148 kern: Math.max(
149 options.fontMetrics().bigOpSpacing1,
150 options.fontMetrics().bigOpSpacing3 - elem.depth),
151 };
152 }
153
154 if (subGroup) {
155 const elem = html.buildGroup(
156 subGroup, options.havingStyle(style.sub()), options);
157
158 sub = {
159 elem,
160 kern: Math.max(
161 options.fontMetrics().bigOpSpacing2,
162 options.fontMetrics().bigOpSpacing4 - elem.height),
163 };
164 }
165
166 // Build the final group as a vlist of the possible subscript, base,
167 // and possible superscript.
168 let finalGroup;
169 if (sup && sub) {
170 const bottom = options.fontMetrics().bigOpSpacing5 +
171 sub.elem.height + sub.elem.depth +
172 sub.kern +
173 base.depth + baseShift;
174
175 finalGroup = buildCommon.makeVList({
176 positionType: "bottom",
177 positionData: bottom,
178 children: [
179 {type: "kern", size: options.fontMetrics().bigOpSpacing5},
180 {type: "elem", elem: sub.elem, marginLeft: -slant + "em"},
181 {type: "kern", size: sub.kern},
182 {type: "elem", elem: base},
183 {type: "kern", size: sup.kern},
184 {type: "elem", elem: sup.elem, marginLeft: slant + "em"},
185 {type: "kern", size: options.fontMetrics().bigOpSpacing5},
186 ],
187 }, options);
188 } else if (sub) {
189 const top = base.height - baseShift;
190
191 // Shift the limits by the slant of the symbol. Note
192 // that we are supposed to shift the limits by 1/2 of the slant,
193 // but since we are centering the limits adding a full slant of
194 // margin will shift by 1/2 that.
195 finalGroup = buildCommon.makeVList({
196 positionType: "top",
197 positionData: top,
198 children: [
199 {type: "kern", size: options.fontMetrics().bigOpSpacing5},
200 {type: "elem", elem: sub.elem, marginLeft: -slant + "em"},
201 {type: "kern", size: sub.kern},
202 {type: "elem", elem: base},
203 ],
204 }, options);
205 } else if (sup) {
206 const bottom = base.depth + baseShift;
207
208 finalGroup = buildCommon.makeVList({
209 positionType: "bottom",
210 positionData: bottom,
211 children: [
212 {type: "elem", elem: base},
213 {type: "kern", size: sup.kern},
214 {type: "elem", elem: sup.elem, marginLeft: slant + "em"},
215 {type: "kern", size: options.fontMetrics().bigOpSpacing5},
216 ],
217 }, options);
218 } else {
219 // This case probably shouldn't occur (this would mean the
220 // supsub was sending us a group with no superscript or
221 // subscript) but be safe.
222 return base;
223 }
224
225 return buildCommon.makeSpan(
226 ["mop", "op-limits"], [finalGroup], options);
227 } else {
228 if (baseShift) {
229 base.style.position = "relative";
230 base.style.top = baseShift + "em";
231 }
232
233 return base;
234 }
235};
236
237const mathmlBuilder: MathMLBuilder<"op"> = (group, options) => {
238 let node;
239
240 if (group.symbol) {
241 // This is a symbol. Just add the symbol.
242 node = new mathMLTree.MathNode(
243 "mo", [mml.makeText(group.name, group.mode)]);
244 if (utils.contains(noSuccessor, group.name)) {
245 node.setAttribute("largeop", "false");
246 }
247 } else if (group.body) {
248 // This is an operator with children. Add them.
249 node = new mathMLTree.MathNode(
250 "mo", mml.buildExpression(group.body, options));
251 } else {
252 // This is a text operator. Add all of the characters from the
253 // operator's name.
254 // TODO(emily): Add a space in the middle of some of these
255 // operators, like \limsup.
256 node = new mathMLTree.MathNode(
257 "mi", [new mathMLTree.TextNode(group.name.slice(1))]);
258 // Append an <mo>&ApplyFunction;</mo>.
259 // ref: https://www.w3.org/TR/REC-MathML/chap3_2.html#sec3.2.4
260 const operator = new mathMLTree.MathNode("mo",
261 [mml.makeText("\u2061", "text")]);
262 if (group.parentIsSupSub) {
263 node = new mathMLTree.MathNode("mo", [node, operator]);
264 } else {
265 node = mathMLTree.newDocumentFragment([node, operator]);
266 }
267 }
268
269 return node;
270};
271
272const singleCharBigOps: {[string]: string} = {
273 "\u220F": "\\prod",
274 "\u2210": "\\coprod",
275 "\u2211": "\\sum",
276 "\u22c0": "\\bigwedge",
277 "\u22c1": "\\bigvee",
278 "\u22c2": "\\bigcap",
279 "\u22c3": "\\bigcup",
280 "\u2a00": "\\bigodot",
281 "\u2a01": "\\bigoplus",
282 "\u2a02": "\\bigotimes",
283 "\u2a04": "\\biguplus",
284 "\u2a06": "\\bigsqcup",
285};
286
287defineFunction({
288 type: "op",
289 names: [
290 "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
291 "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
292 "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint", "\u220F",
293 "\u2210", "\u2211", "\u22c0", "\u22c1", "\u22c2", "\u22c3", "\u2a00",
294 "\u2a01", "\u2a02", "\u2a04", "\u2a06",
295 ],
296 props: {
297 numArgs: 0,
298 },
299 handler: ({parser, funcName}, args) => {
300 let fName = funcName;
301 if (fName.length === 1) {
302 fName = singleCharBigOps[fName];
303 }
304 return {
305 type: "op",
306 mode: parser.mode,
307 limits: true,
308 parentIsSupSub: false,
309 symbol: true,
310 name: fName,
311 };
312 },
313 htmlBuilder,
314 mathmlBuilder,
315});
316
317// Note: calling defineFunction with a type that's already been defined only
318// works because the same htmlBuilder and mathmlBuilder are being used.
319defineFunction({
320 type: "op",
321 names: ["\\mathop"],
322 props: {
323 numArgs: 1,
324 },
325 handler: ({parser}, args) => {
326 const body = args[0];
327 return {
328 type: "op",
329 mode: parser.mode,
330 limits: false,
331 parentIsSupSub: false,
332 symbol: false,
333 body: ordargument(body),
334 };
335 },
336 htmlBuilder,
337 mathmlBuilder,
338});
339
340// There are 2 flags for operators; whether they produce limits in
341// displaystyle, and whether they are symbols and should grow in
342// displaystyle. These four groups cover the four possible choices.
343
344const singleCharIntegrals: {[string]: string} = {
345 "\u222b": "\\int",
346 "\u222c": "\\iint",
347 "\u222d": "\\iiint",
348 "\u222e": "\\oint",
349 "\u222f": "\\oiint",
350 "\u2230": "\\oiiint",
351};
352
353// No limits, not symbols
354defineFunction({
355 type: "op",
356 names: [
357 "\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg",
358 "\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg",
359 "\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp",
360 "\\hom", "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin",
361 "\\sinh", "\\sh", "\\tan", "\\tanh", "\\tg", "\\th",
362 ],
363 props: {
364 numArgs: 0,
365 },
366 handler({parser, funcName}) {
367 return {
368 type: "op",
369 mode: parser.mode,
370 limits: false,
371 parentIsSupSub: false,
372 symbol: false,
373 name: funcName,
374 };
375 },
376 htmlBuilder,
377 mathmlBuilder,
378});
379
380// Limits, not symbols
381defineFunction({
382 type: "op",
383 names: [
384 "\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup",
385 ],
386 props: {
387 numArgs: 0,
388 },
389 handler({parser, funcName}) {
390 return {
391 type: "op",
392 mode: parser.mode,
393 limits: true,
394 parentIsSupSub: false,
395 symbol: false,
396 name: funcName,
397 };
398 },
399 htmlBuilder,
400 mathmlBuilder,
401});
402
403// No limits, symbols
404defineFunction({
405 type: "op",
406 names: [
407 "\\int", "\\iint", "\\iiint", "\\oint", "\\oiint", "\\oiiint",
408 "\u222b", "\u222c", "\u222d", "\u222e", "\u222f", "\u2230",
409 ],
410 props: {
411 numArgs: 0,
412 },
413 handler({parser, funcName}) {
414 let fName = funcName;
415 if (fName.length === 1) {
416 fName = singleCharIntegrals[fName];
417 }
418 return {
419 type: "op",
420 mode: parser.mode,
421 limits: false,
422 parentIsSupSub: false,
423 symbol: true,
424 name: fName,
425 };
426 },
427 htmlBuilder,
428 mathmlBuilder,
429});