// @flow import defineFunction from "../defineFunction"; import buildCommon from "../buildCommon"; import mathMLTree from "../mathMLTree"; import utils from "../utils"; import stretchy from "../stretchy"; import {phasePath} from "../svgGeometry"; import {PathNode, SvgNode} from "../domTree"; import {calculateSize, makeEm} from "../units"; import {assertNodeType} from "../parseNode"; import * as html from "../buildHTML"; import * as mml from "../buildMathML"; const htmlBuilder = (group, options) => { // \cancel, \bcancel, \xcancel, \sout, \fbox, \colorbox, \fcolorbox, \phase // Some groups can return document fragments. Handle those by wrapping // them in a span. const inner = buildCommon.wrapFragment( html.buildGroup(group.body, options), options); const label = group.label.slice(1); let scale = options.sizeMultiplier; let img; let imgShift = 0; // In the LaTeX cancel package, line geometry is slightly different // depending on whether the subject is wider than it is tall, or vice versa. // We don't know the width of a group, so as a proxy, we test if // the subject is a single character. This captures most of the // subjects that should get the "tall" treatment. const isSingleChar = utils.isCharacterBox(group.body); if (label === "sout") { img = buildCommon.makeSpan(["stretchy", "sout"]); img.height = options.fontMetrics().defaultRuleThickness / scale; imgShift = -0.5 * options.fontMetrics().xHeight; } else if (label === "phase") { // Set a couple of dimensions from the steinmetz package. const lineWeight = calculateSize({number: 0.6, unit: "pt"}, options); const clearance = calculateSize({number: 0.35, unit: "ex"}, options); // Prevent size changes like \Huge from affecting line thickness const newOptions = options.havingBaseSizing(); scale = scale / newOptions.sizeMultiplier; const angleHeight = inner.height + inner.depth + lineWeight + clearance; // Reserve a left pad for the angle. inner.style.paddingLeft = makeEm(angleHeight / 2 + lineWeight); // Create an SVG const viewBoxHeight = Math.floor(1000 * angleHeight * scale); const path = phasePath(viewBoxHeight); const svgNode = new SvgNode([new PathNode("phase", path)], { "width": "400em", "height": makeEm(viewBoxHeight / 1000), "viewBox": `0 0 400000 ${viewBoxHeight}`, "preserveAspectRatio": "xMinYMin slice", }); // Wrap it in a span with overflow: hidden. img = buildCommon.makeSvgSpan(["hide-tail"], [svgNode], options); img.style.height = makeEm(angleHeight); imgShift = inner.depth + lineWeight + clearance; } else { // Add horizontal padding if (/cancel/.test(label)) { if (!isSingleChar) { inner.classes.push("cancel-pad"); } } else if (label === "angl") { inner.classes.push("anglpad"); } else { inner.classes.push("boxpad"); } // Add vertical padding let topPad = 0; let bottomPad = 0; let ruleThickness = 0; // ref: cancel package: \advance\totalheight2\p@ % "+2" if (/box/.test(label)) { ruleThickness = Math.max( options.fontMetrics().fboxrule, // default options.minRuleThickness, // User override. ); topPad = options.fontMetrics().fboxsep + (label === "colorbox" ? 0 : ruleThickness); bottomPad = topPad; } else if (label === "angl") { ruleThickness = Math.max( options.fontMetrics().defaultRuleThickness, options.minRuleThickness ); topPad = 4 * ruleThickness; // gap = 3 × line, plus the line itself. bottomPad = Math.max(0, 0.25 - inner.depth); } else { topPad = isSingleChar ? 0.2 : 0; bottomPad = topPad; } img = stretchy.encloseSpan(inner, label, topPad, bottomPad, options); if (/fbox|boxed|fcolorbox/.test(label)) { img.style.borderStyle = "solid"; img.style.borderWidth = makeEm(ruleThickness); } else if (label === "angl" && ruleThickness !== 0.049) { img.style.borderTopWidth = makeEm(ruleThickness); img.style.borderRightWidth = makeEm(ruleThickness); } imgShift = inner.depth + bottomPad; if (group.backgroundColor) { img.style.backgroundColor = group.backgroundColor; if (group.borderColor) { img.style.borderColor = group.borderColor; } } } let vlist; if (group.backgroundColor) { vlist = buildCommon.makeVList({ positionType: "individualShift", children: [ // Put the color background behind inner; {type: "elem", elem: img, shift: imgShift}, {type: "elem", elem: inner, shift: 0}, ], }, options); } else { const classes = /cancel|phase/.test(label) ? ["svg-align"] : []; vlist = buildCommon.makeVList({ positionType: "individualShift", children: [ // Write the \cancel stroke on top of inner. { type: "elem", elem: inner, shift: 0, }, { type: "elem", elem: img, shift: imgShift, wrapperClasses: classes, }, ], }, options); } if (/cancel/.test(label)) { // The cancel package documentation says that cancel lines add their height // to the expression, but tests show that isn't how it actually works. vlist.height = inner.height; vlist.depth = inner.depth; } if (/cancel/.test(label) && !isSingleChar) { // cancel does not create horiz space for its line extension. return buildCommon.makeSpan(["mord", "cancel-lap"], [vlist], options); } else { return buildCommon.makeSpan(["mord"], [vlist], options); } }; const mathmlBuilder = (group, options) => { let fboxsep = 0; const node = new mathMLTree.MathNode( (group.label.indexOf("colorbox") > -1) ? "mpadded" : "menclose", [mml.buildGroup(group.body, options)] ); switch (group.label) { case "\\cancel": node.setAttribute("notation", "updiagonalstrike"); break; case "\\bcancel": node.setAttribute("notation", "downdiagonalstrike"); break; case "\\phase": node.setAttribute("notation", "phasorangle"); break; case "\\sout": node.setAttribute("notation", "horizontalstrike"); break; case "\\fbox": node.setAttribute("notation", "box"); break; case "\\angl": node.setAttribute("notation", "actuarial"); break; case "\\fcolorbox": case "\\colorbox": // doesn't have a good notation option. So use // instead. Set some attributes that come included with . fboxsep = options.fontMetrics().fboxsep * options.fontMetrics().ptPerEm; node.setAttribute("width", `+${2 * fboxsep}pt`); node.setAttribute("height", `+${2 * fboxsep}pt`); node.setAttribute("lspace", `${fboxsep}pt`); // node.setAttribute("voffset", `${fboxsep}pt`); if (group.label === "\\fcolorbox") { const thk = Math.max( options.fontMetrics().fboxrule, // default options.minRuleThickness, // user override ); node.setAttribute("style", "border: " + thk + "em solid " + String(group.borderColor)); } break; case "\\xcancel": node.setAttribute("notation", "updiagonalstrike downdiagonalstrike"); break; } if (group.backgroundColor) { node.setAttribute("mathbackground", group.backgroundColor); } return node; }; defineFunction({ type: "enclose", names: ["\\colorbox"], props: { numArgs: 2, allowedInText: true, argTypes: ["color", "text"], }, handler({parser, funcName}, args, optArgs) { const color = assertNodeType(args[0], "color-token").color; const body = args[1]; return { type: "enclose", mode: parser.mode, label: funcName, backgroundColor: color, body, }; }, htmlBuilder, mathmlBuilder, }); defineFunction({ type: "enclose", names: ["\\fcolorbox"], props: { numArgs: 3, allowedInText: true, argTypes: ["color", "color", "text"], }, handler({parser, funcName}, args, optArgs) { const borderColor = assertNodeType(args[0], "color-token").color; const backgroundColor = assertNodeType(args[1], "color-token").color; const body = args[2]; return { type: "enclose", mode: parser.mode, label: funcName, backgroundColor, borderColor, body, }; }, htmlBuilder, mathmlBuilder, }); defineFunction({ type: "enclose", names: ["\\fbox"], props: { numArgs: 1, argTypes: ["hbox"], allowedInText: true, }, handler({parser}, args) { return { type: "enclose", mode: parser.mode, label: "\\fbox", body: args[0], }; }, }); defineFunction({ type: "enclose", names: ["\\cancel", "\\bcancel", "\\xcancel", "\\sout", "\\phase"], props: { numArgs: 1, }, handler({parser, funcName}, args) { const body = args[0]; return { type: "enclose", mode: parser.mode, label: funcName, body, }; }, htmlBuilder, mathmlBuilder, }); defineFunction({ type: "enclose", names: ["\\angl"], props: { numArgs: 1, argTypes: ["hbox"], allowedInText: false, }, handler({parser}, args) { return { type: "enclose", mode: parser.mode, label: "\\angl", body: args[0], }; }, });