UNPKG

14.8 kBJavaScriptView Raw
1// @flow
2import defineFunction from "../defineFunction";
3import buildCommon from "../buildCommon";
4import delimiter from "../delimiter";
5import mathMLTree from "../mathMLTree";
6import Style from "../Style";
7import {assertNodeType, assertAtomFamily, checkNodeType} from "../parseNode";
8import {assert} from "../utils";
9
10import * as html from "../buildHTML";
11import * as mml from "../buildMathML";
12import {calculateSize} from "../units";
13
14const adjustStyle = (size, originalStyle) => {
15 // Figure out what style this fraction should be in based on the
16 // function used
17 let style = originalStyle;
18 if (size === "display") {
19 // Get display style as a default.
20 // If incoming style is sub/sup, use style.text() to get correct size.
21 style = style.id >= Style.SCRIPT.id ? style.text() : Style.DISPLAY;
22 } else if (size === "text" &&
23 style.size === Style.DISPLAY.size) {
24 // We're in a \tfrac but incoming style is displaystyle, so:
25 style = Style.TEXT;
26 } else if (size === "script") {
27 style = Style.SCRIPT;
28 } else if (size === "scriptscript") {
29 style = Style.SCRIPTSCRIPT;
30 }
31 return style;
32};
33
34const htmlBuilder = (group, options) => {
35 // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
36 const style = adjustStyle(group.size, options.style);
37
38 const nstyle = style.fracNum();
39 const dstyle = style.fracDen();
40 let newOptions;
41
42 newOptions = options.havingStyle(nstyle);
43 const numerm = html.buildGroup(group.numer, newOptions, options);
44
45 if (group.continued) {
46 // \cfrac inserts a \strut into the numerator.
47 // Get \strut dimensions from TeXbook page 353.
48 const hStrut = 8.5 / options.fontMetrics().ptPerEm;
49 const dStrut = 3.5 / options.fontMetrics().ptPerEm;
50 numerm.height = numerm.height < hStrut ? hStrut : numerm.height;
51 numerm.depth = numerm.depth < dStrut ? dStrut : numerm.depth;
52 }
53
54 newOptions = options.havingStyle(dstyle);
55 const denomm = html.buildGroup(group.denom, newOptions, options);
56
57 let rule;
58 let ruleWidth;
59 let ruleSpacing;
60 if (group.hasBarLine) {
61 if (group.barSize) {
62 ruleWidth = calculateSize(group.barSize, options);
63 rule = buildCommon.makeLineSpan("frac-line", options, ruleWidth);
64 } else {
65 rule = buildCommon.makeLineSpan("frac-line", options);
66 }
67 ruleWidth = rule.height;
68 ruleSpacing = rule.height;
69 } else {
70 rule = null;
71 ruleWidth = 0;
72 ruleSpacing = options.fontMetrics().defaultRuleThickness;
73 }
74
75 // Rule 15b
76 let numShift;
77 let clearance;
78 let denomShift;
79 if (style.size === Style.DISPLAY.size || group.size === "display") {
80 numShift = options.fontMetrics().num1;
81 if (ruleWidth > 0) {
82 clearance = 3 * ruleSpacing;
83 } else {
84 clearance = 7 * ruleSpacing;
85 }
86 denomShift = options.fontMetrics().denom1;
87 } else {
88 if (ruleWidth > 0) {
89 numShift = options.fontMetrics().num2;
90 clearance = ruleSpacing;
91 } else {
92 numShift = options.fontMetrics().num3;
93 clearance = 3 * ruleSpacing;
94 }
95 denomShift = options.fontMetrics().denom2;
96 }
97
98 let frac;
99 if (!rule) {
100 // Rule 15c
101 const candidateClearance =
102 (numShift - numerm.depth) - (denomm.height - denomShift);
103 if (candidateClearance < clearance) {
104 numShift += 0.5 * (clearance - candidateClearance);
105 denomShift += 0.5 * (clearance - candidateClearance);
106 }
107
108 frac = buildCommon.makeVList({
109 positionType: "individualShift",
110 children: [
111 {type: "elem", elem: denomm, shift: denomShift},
112 {type: "elem", elem: numerm, shift: -numShift},
113 ],
114 }, options);
115 } else {
116 // Rule 15d
117 const axisHeight = options.fontMetrics().axisHeight;
118
119 if ((numShift - numerm.depth) - (axisHeight + 0.5 * ruleWidth) <
120 clearance) {
121 numShift +=
122 clearance - ((numShift - numerm.depth) -
123 (axisHeight + 0.5 * ruleWidth));
124 }
125
126 if ((axisHeight - 0.5 * ruleWidth) - (denomm.height - denomShift) <
127 clearance) {
128 denomShift +=
129 clearance - ((axisHeight - 0.5 * ruleWidth) -
130 (denomm.height - denomShift));
131 }
132
133 const midShift = -(axisHeight - 0.5 * ruleWidth);
134
135 frac = buildCommon.makeVList({
136 positionType: "individualShift",
137 children: [
138 {type: "elem", elem: denomm, shift: denomShift},
139 {type: "elem", elem: rule, shift: midShift},
140 {type: "elem", elem: numerm, shift: -numShift},
141 ],
142 }, options);
143 }
144
145 // Since we manually change the style sometimes (with \dfrac or \tfrac),
146 // account for the possible size change here.
147 newOptions = options.havingStyle(style);
148 frac.height *= newOptions.sizeMultiplier / options.sizeMultiplier;
149 frac.depth *= newOptions.sizeMultiplier / options.sizeMultiplier;
150
151 // Rule 15e
152 let delimSize;
153 if (style.size === Style.DISPLAY.size) {
154 delimSize = options.fontMetrics().delim1;
155 } else {
156 delimSize = options.fontMetrics().delim2;
157 }
158
159 let leftDelim;
160 let rightDelim;
161 if (group.leftDelim == null) {
162 leftDelim = html.makeNullDelimiter(options, ["mopen"]);
163 } else {
164 leftDelim = delimiter.customSizedDelim(
165 group.leftDelim, delimSize, true,
166 options.havingStyle(style), group.mode, ["mopen"]);
167 }
168
169 if (group.continued) {
170 rightDelim = buildCommon.makeSpan([]); // zero width for \cfrac
171 } else if (group.rightDelim == null) {
172 rightDelim = html.makeNullDelimiter(options, ["mclose"]);
173 } else {
174 rightDelim = delimiter.customSizedDelim(
175 group.rightDelim, delimSize, true,
176 options.havingStyle(style), group.mode, ["mclose"]);
177 }
178
179 return buildCommon.makeSpan(
180 ["mord"].concat(newOptions.sizingClasses(options)),
181 [leftDelim, buildCommon.makeSpan(["mfrac"], [frac]), rightDelim],
182 options);
183};
184
185const mathmlBuilder = (group, options) => {
186 let node = new mathMLTree.MathNode(
187 "mfrac",
188 [
189 mml.buildGroup(group.numer, options),
190 mml.buildGroup(group.denom, options),
191 ]);
192
193 if (!group.hasBarLine) {
194 node.setAttribute("linethickness", "0px");
195 } else if (group.barSize) {
196 const ruleWidth = calculateSize(group.barSize, options);
197 node.setAttribute("linethickness", ruleWidth + "em");
198 }
199
200 const style = adjustStyle(group.size, options.style);
201 if (style.size !== options.style.size) {
202 node = new mathMLTree.MathNode("mstyle", [node]);
203 const isDisplay = (style.size === Style.DISPLAY.size) ? "true" : "false";
204 node.setAttribute("displaystyle", isDisplay);
205 node.setAttribute("scriptlevel", "0");
206 }
207
208 if (group.leftDelim != null || group.rightDelim != null) {
209 const withDelims = [];
210
211 if (group.leftDelim != null) {
212 const leftOp = new mathMLTree.MathNode(
213 "mo",
214 [new mathMLTree.TextNode(group.leftDelim.replace("\\", ""))]
215 );
216
217 leftOp.setAttribute("fence", "true");
218
219 withDelims.push(leftOp);
220 }
221
222 withDelims.push(node);
223
224 if (group.rightDelim != null) {
225 const rightOp = new mathMLTree.MathNode(
226 "mo",
227 [new mathMLTree.TextNode(group.rightDelim.replace("\\", ""))]
228 );
229
230 rightOp.setAttribute("fence", "true");
231
232 withDelims.push(rightOp);
233 }
234
235 return mml.makeRow(withDelims);
236 }
237
238 return node;
239};
240
241defineFunction({
242 type: "genfrac",
243 names: [
244 "\\cfrac", "\\dfrac", "\\frac", "\\tfrac",
245 "\\dbinom", "\\binom", "\\tbinom",
246 "\\\\atopfrac", // can’t be entered directly
247 "\\\\bracefrac", "\\\\brackfrac", // ditto
248 ],
249 props: {
250 numArgs: 2,
251 greediness: 2,
252 },
253 handler: ({parser, funcName}, args) => {
254 const numer = args[0];
255 const denom = args[1];
256 let hasBarLine;
257 let leftDelim = null;
258 let rightDelim = null;
259 let size = "auto";
260
261 switch (funcName) {
262 case "\\cfrac":
263 case "\\dfrac":
264 case "\\frac":
265 case "\\tfrac":
266 hasBarLine = true;
267 break;
268 case "\\\\atopfrac":
269 hasBarLine = false;
270 break;
271 case "\\dbinom":
272 case "\\binom":
273 case "\\tbinom":
274 hasBarLine = false;
275 leftDelim = "(";
276 rightDelim = ")";
277 break;
278 case "\\\\bracefrac":
279 hasBarLine = false;
280 leftDelim = "\\{";
281 rightDelim = "\\}";
282 break;
283 case "\\\\brackfrac":
284 hasBarLine = false;
285 leftDelim = "[";
286 rightDelim = "]";
287 break;
288 default:
289 throw new Error("Unrecognized genfrac command");
290 }
291
292 switch (funcName) {
293 case "\\cfrac":
294 case "\\dfrac":
295 case "\\dbinom":
296 size = "display";
297 break;
298 case "\\tfrac":
299 case "\\tbinom":
300 size = "text";
301 break;
302 }
303
304 return {
305 type: "genfrac",
306 mode: parser.mode,
307 continued: funcName === "\\cfrac",
308 numer,
309 denom,
310 hasBarLine,
311 leftDelim,
312 rightDelim,
313 size,
314 barSize: null,
315 };
316 },
317
318 htmlBuilder,
319 mathmlBuilder,
320});
321
322// Infix generalized fractions -- these are not rendered directly, but replaced
323// immediately by one of the variants above.
324defineFunction({
325 type: "infix",
326 names: ["\\over", "\\choose", "\\atop", "\\brace", "\\brack"],
327 props: {
328 numArgs: 0,
329 infix: true,
330 },
331 handler({parser, funcName, token}) {
332 let replaceWith;
333 switch (funcName) {
334 case "\\over":
335 replaceWith = "\\frac";
336 break;
337 case "\\choose":
338 replaceWith = "\\binom";
339 break;
340 case "\\atop":
341 replaceWith = "\\\\atopfrac";
342 break;
343 case "\\brace":
344 replaceWith = "\\\\bracefrac";
345 break;
346 case "\\brack":
347 replaceWith = "\\\\brackfrac";
348 break;
349 default:
350 throw new Error("Unrecognized infix genfrac command");
351 }
352 return {
353 type: "infix",
354 mode: parser.mode,
355 replaceWith,
356 token,
357 };
358 },
359});
360
361const stylArray = ["display", "text", "script", "scriptscript"];
362
363const delimFromValue = function(delimString: string): string | null {
364 let delim = null;
365 if (delimString.length > 0) {
366 delim = delimString;
367 delim = delim === "." ? null : delim;
368 }
369 return delim;
370};
371
372defineFunction({
373 type: "genfrac",
374 names: ["\\genfrac"],
375 props: {
376 numArgs: 6,
377 greediness: 6,
378 argTypes: ["math", "math", "size", "text", "math", "math"],
379 },
380 handler({parser}, args) {
381 const numer = args[4];
382 const denom = args[5];
383
384 // Look into the parse nodes to get the desired delimiters.
385 let leftNode = checkNodeType(args[0], "atom");
386 if (leftNode) {
387 leftNode = assertAtomFamily(args[0], "open");
388 }
389 const leftDelim = leftNode ? delimFromValue(leftNode.text) : null;
390
391 let rightNode = checkNodeType(args[1], "atom");
392 if (rightNode) {
393 rightNode = assertAtomFamily(args[1], "close");
394 }
395 const rightDelim = rightNode ? delimFromValue(rightNode.text) : null;
396
397 const barNode = assertNodeType(args[2], "size");
398 let hasBarLine;
399 let barSize = null;
400 if (barNode.isBlank) {
401 // \genfrac acts differently than \above.
402 // \genfrac treats an empty size group as a signal to use a
403 // standard bar size. \above would see size = 0 and omit the bar.
404 hasBarLine = true;
405 } else {
406 barSize = barNode.value;
407 hasBarLine = barSize.number > 0;
408 }
409
410 // Find out if we want displaystyle, textstyle, etc.
411 let size = "auto";
412 let styl = checkNodeType(args[3], "ordgroup");
413 if (styl) {
414 if (styl.body.length > 0) {
415 const textOrd = assertNodeType(styl.body[0], "textord");
416 size = stylArray[Number(textOrd.text)];
417 }
418 } else {
419 styl = assertNodeType(args[3], "textord");
420 size = stylArray[Number(styl.text)];
421 }
422
423 return {
424 type: "genfrac",
425 mode: parser.mode,
426 numer,
427 denom,
428 continued: false,
429 hasBarLine,
430 barSize,
431 leftDelim,
432 rightDelim,
433 size,
434 };
435 },
436
437 htmlBuilder,
438 mathmlBuilder,
439});
440
441// \above is an infix fraction that also defines a fraction bar size.
442defineFunction({
443 type: "infix",
444 names: ["\\above"],
445 props: {
446 numArgs: 1,
447 argTypes: ["size"],
448 infix: true,
449 },
450 handler({parser, funcName, token}, args) {
451 return {
452 type: "infix",
453 mode: parser.mode,
454 replaceWith: "\\\\abovefrac",
455 size: assertNodeType(args[0], "size").value,
456 token,
457 };
458 },
459});
460
461defineFunction({
462 type: "genfrac",
463 names: ["\\\\abovefrac"],
464 props: {
465 numArgs: 3,
466 argTypes: ["math", "size", "math"],
467 },
468 handler: ({parser, funcName}, args) => {
469 const numer = args[0];
470 const barSize = assert(assertNodeType(args[1], "infix").size);
471 const denom = args[2];
472
473 const hasBarLine = barSize.number > 0;
474 return {
475 type: "genfrac",
476 mode: parser.mode,
477 numer,
478 denom,
479 continued: false,
480 hasBarLine,
481 barSize,
482 leftDelim: null,
483 rightDelim: null,
484 size: "auto",
485 };
486 },
487
488 htmlBuilder,
489 mathmlBuilder,
490});