Spaces:
Sleeping
Sleeping
| 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, | |
| }; | |
| }, | |
| }); | |