1 |
|
2 | import defineFunction from "../defineFunction";
|
3 | import buildCommon from "../buildCommon";
|
4 | import delimiter from "../delimiter";
|
5 | import mathMLTree from "../mathMLTree";
|
6 | import Style from "../Style";
|
7 | import {assertNodeType, assertAtomFamily, checkNodeType} from "../parseNode";
|
8 | import {assert} from "../utils";
|
9 |
|
10 | import * as html from "../buildHTML";
|
11 | import * as mml from "../buildMathML";
|
12 | import {calculateSize} from "../units";
|
13 |
|
14 | const adjustStyle = (size, originalStyle) => {
|
15 |
|
16 |
|
17 | let style = originalStyle;
|
18 | if (size === "display") {
|
19 |
|
20 |
|
21 | style = style.id >= Style.SCRIPT.id ? style.text() : Style.DISPLAY;
|
22 | } else if (size === "text" &&
|
23 | style.size === Style.DISPLAY.size) {
|
24 |
|
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 |
|
34 | const htmlBuilder = (group, options) => {
|
35 |
|
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 |
|
47 |
|
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 |
|
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 |
|
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 |
|
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 |
|
146 |
|
147 | newOptions = options.havingStyle(style);
|
148 | frac.height *= newOptions.sizeMultiplier / options.sizeMultiplier;
|
149 | frac.depth *= newOptions.sizeMultiplier / options.sizeMultiplier;
|
150 |
|
151 |
|
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([]);
|
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 |
|
185 | const 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 |
|
241 | defineFunction({
|
242 | type: "genfrac",
|
243 | names: [
|
244 | "\\cfrac", "\\dfrac", "\\frac", "\\tfrac",
|
245 | "\\dbinom", "\\binom", "\\tbinom",
|
246 | "\\\\atopfrac",
|
247 | "\\\\bracefrac", "\\\\brackfrac",
|
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 |
|
323 |
|
324 | defineFunction({
|
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 |
|
361 | const stylArray = ["display", "text", "script", "scriptscript"];
|
362 |
|
363 | const 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 |
|
372 | defineFunction({
|
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 |
|
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 |
|
402 |
|
403 |
|
404 | hasBarLine = true;
|
405 | } else {
|
406 | barSize = barNode.value;
|
407 | hasBarLine = barSize.number > 0;
|
408 | }
|
409 |
|
410 |
|
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 |
|
442 | defineFunction({
|
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 |
|
461 | defineFunction({
|
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 | });
|