From 5bcdeec4add8576df4645a2aef87314ee7d26ea8 Mon Sep 17 00:00:00 2001 From: Ashish Myles Date: Wed, 14 Mar 2018 10:38:35 -0400 Subject: [PATCH] Format buildHTML groupType definitions in preparation for adding types. (#1206) * Format buildHTML groupType definitions in preparation for adding types. 1) Collapse groupType definitions (a bunch of functions all of the same type defined on the groupType object) into a single object in preparation for putting types only on the base object. 2) Do minor reformatting to account for the +4 indent above which pushes some lines past 80 characters. 3) Do other minor formatting changes for readability, taking advantage of ES6. * Review fixes. --- src/buildHTML.js | 651 +++++++++++++++++++++++------------------------ 1 file changed, 314 insertions(+), 337 deletions(-) diff --git a/src/buildHTML.js b/src/buildHTML.js index 71c57deb..ada2ec0e 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -63,8 +63,7 @@ export const buildExpression = function(expression, options, isRealGroup, // Parse expressions into `groups`. const rawGroups = []; for (let i = 0; i < expression.length; i++) { - const group = expression[i]; - const output = buildGroup(group, options); + const output = buildGroup(expression[i], options); if (output instanceof domTree.documentFragment) { rawGroups.push(...output.children); } else { @@ -216,24 +215,22 @@ export const isLeftTight = function(node) { * handling them itself. */ const shouldHandleSupSub = function(group, options) { - if (!group.value.base) { + const base = group.value.base; + if (!base) { return false; + } else if (base.type === "op") { + // Operators handle supsubs differently when they have limits + // (e.g. `\displaystyle\sum_2^3`) + return base.value.limits && + (options.style.size === Style.DISPLAY.size || + base.value.alwaysHandleSupSub); + } else if (base.type === "accent") { + return utils.isCharacterBox(base.value.base); + } else if (base.type === "horizBrace") { + const isSup = !group.value.sub; + return (isSup === base.value.isOver); } else { - const base = group.value.base; - if (base.type === "op") { - // Operators handle supsubs differently when they have limits - // (e.g. `\displaystyle\sum_2^3`) - return base.value.limits && - (options.style.size === Style.DISPLAY.size || - base.value.alwaysHandleSupSub); - } else if (base.type === "accent") { - return utils.isCharacterBox(base.value.base); - } else if (base.type === "horizBrace") { - const isSup = (group.value.sub ? false : true); - return (isSup === base.value.isOver); - } else { - return null; - } + return false; } }; @@ -246,374 +243,354 @@ export const makeNullDelimiter = function(options, classes) { * This is a map of group types to the function used to handle that type. * Simpler types come at the beginning, while complicated types come afterwards. */ -export const groupTypes = {}; +export const groupTypes = { + mathord: (group, options) => buildCommon.makeOrd(group, options, "mathord"), -groupTypes.mathord = function(group, options) { - return buildCommon.makeOrd(group, options, "mathord"); -}; + textord: (group, options) => buildCommon.makeOrd(group, options, "textord"), -groupTypes.textord = function(group, options) { - return buildCommon.makeOrd(group, options, "textord"); -}; + bin: (group, options) => + buildCommon.mathsym(group.value, group.mode, options, ["mbin"]), -groupTypes.bin = function(group, options) { - return buildCommon.mathsym( - group.value, group.mode, options, ["mbin"]); -}; + rel: (group, options) => + buildCommon.mathsym(group.value, group.mode, options, ["mrel"]), -groupTypes.rel = function(group, options) { - return buildCommon.mathsym( - group.value, group.mode, options, ["mrel"]); -}; + open: (group, options) => + buildCommon.mathsym(group.value, group.mode, options, ["mopen"]), -groupTypes.open = function(group, options) { - return buildCommon.mathsym( - group.value, group.mode, options, ["mopen"]); -}; + close: (group, options) => + buildCommon.mathsym(group.value, group.mode, options, ["mclose"]), -groupTypes.close = function(group, options) { - return buildCommon.mathsym( - group.value, group.mode, options, ["mclose"]); -}; + inner: (group, options) => + buildCommon.mathsym(group.value, group.mode, options, ["minner"]), -groupTypes.inner = function(group, options) { - return buildCommon.mathsym( - group.value, group.mode, options, ["minner"]); -}; + punct: (group, options) => + buildCommon.mathsym(group.value, group.mode, options, ["mpunct"]), -groupTypes.punct = function(group, options) { - return buildCommon.mathsym( - group.value, group.mode, options, ["mpunct"]); -}; + ordgroup: (group, options) => makeSpan( + ["mord"], buildExpression(group.value, options, true), options), -groupTypes.ordgroup = function(group, options) { - return makeSpan(["mord"], - buildExpression(group.value, options, true), - options - ); -}; + supsub(group, options) { + // Superscript and subscripts are handled in the TeXbook on page + // 445-446, rules 18(a-f). -groupTypes.supsub = function(group, options) { - // Superscript and subscripts are handled in the TeXbook on page - // 445-446, rules 18(a-f). - - // Here is where we defer to the inner group if it should handle - // superscripts and subscripts itself. - if (shouldHandleSupSub(group, options)) { - return groupTypes[group.value.base.type](group, options); - } - - const base = buildGroup(group.value.base, options); - let supm; - let subm; - - const metrics = options.fontMetrics(); - let newOptions; - - // Rule 18a - let supShift = 0; - let subShift = 0; - - if (group.value.sup) { - newOptions = options.havingStyle(options.style.sup()); - supm = buildGroup(group.value.sup, newOptions, options); - if (!utils.isCharacterBox(group.value.base)) { - supShift = base.height - newOptions.fontMetrics().supDrop - * newOptions.sizeMultiplier / options.sizeMultiplier; - } - } - - if (group.value.sub) { - newOptions = options.havingStyle(options.style.sub()); - subm = buildGroup(group.value.sub, newOptions, options); - if (!utils.isCharacterBox(group.value.base)) { - subShift = base.depth + newOptions.fontMetrics().subDrop - * newOptions.sizeMultiplier / options.sizeMultiplier; - } - } - - // Rule 18c - let minSupShift; - if (options.style === Style.DISPLAY) { - minSupShift = metrics.sup1; - } else if (options.style.cramped) { - minSupShift = metrics.sup3; - } else { - minSupShift = metrics.sup2; - } - - // scriptspace is a font-size-independent size, so scale it - // appropriately - const multiplier = options.sizeMultiplier; - const scriptspace = - (0.5 / metrics.ptPerEm) / multiplier + "em"; - - let supsub; - if (!group.value.sup) { - // Rule 18b - subShift = Math.max( - subShift, metrics.sub1, - subm.height - 0.8 * metrics.xHeight); - - const vlistElem = [{type: "elem", elem: subm, marginRight: scriptspace}]; - // Subscripts shouldn't be shifted by the base's italic correction. - // Account for that by shifting the subscript back the appropriate - // amount. Note we only do this when the base is a single symbol. - if (base instanceof domTree.symbolNode) { - vlistElem[0].marginLeft = -base.italic + "em"; + // Here is where we defer to the inner group if it should handle + // superscripts and subscripts itself. + if (shouldHandleSupSub(group, options)) { + return groupTypes[group.value.base.type](group, options); } - supsub = buildCommon.makeVList({ - positionType: "shift", - positionData: subShift, - children: vlistElem, - }, options); - } else if (!group.value.sub) { - // Rule 18c, d - supShift = Math.max(supShift, minSupShift, - supm.depth + 0.25 * metrics.xHeight); + const base = buildGroup(group.value.base, options); + let supm; + let subm; - supsub = buildCommon.makeVList({ - positionType: "shift", - positionData: -supShift, - children: [{type: "elem", elem: supm, marginRight: scriptspace}], - }, options); - } else { - supShift = Math.max( - supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight); - subShift = Math.max(subShift, metrics.sub2); + const metrics = options.fontMetrics(); + let newOptions; - const ruleWidth = metrics.defaultRuleThickness; + // Rule 18a + let supShift = 0; + let subShift = 0; - // Rule 18e - if ((supShift - supm.depth) - (subm.height - subShift) < - 4 * ruleWidth) { - subShift = 4 * ruleWidth - (supShift - supm.depth) + subm.height; - const psi = 0.8 * metrics.xHeight - (supShift - supm.depth); - if (psi > 0) { - supShift += psi; - subShift -= psi; + if (group.value.sup) { + newOptions = options.havingStyle(options.style.sup()); + supm = buildGroup(group.value.sup, newOptions, options); + if (!utils.isCharacterBox(group.value.base)) { + supShift = base.height - newOptions.fontMetrics().supDrop + * newOptions.sizeMultiplier / options.sizeMultiplier; } } - const vlistElem = [ - {type: "elem", elem: subm, shift: subShift, marginRight: scriptspace}, - {type: "elem", elem: supm, shift: -supShift, marginRight: scriptspace}, - ]; - // See comment above about subscripts not being shifted - if (base instanceof domTree.symbolNode) { - vlistElem[0].marginLeft = -base.italic + "em"; + if (group.value.sub) { + newOptions = options.havingStyle(options.style.sub()); + subm = buildGroup(group.value.sub, newOptions, options); + if (!utils.isCharacterBox(group.value.base)) { + subShift = base.depth + newOptions.fontMetrics().subDrop + * newOptions.sizeMultiplier / options.sizeMultiplier; + } } - supsub = buildCommon.makeVList({ - positionType: "individualShift", - children: vlistElem, - }, options); - } - - // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align - const mclass = getTypeOfDomTree(base) || "mord"; - return makeSpan([mclass], - [base, makeSpan(["msupsub"], [supsub])], - options); -}; - -groupTypes.spacing = function(group, options) { - if (buildCommon.regularSpace.hasOwnProperty(group.value)) { - // Spaces are generated by adding an actual space. Each of these - // things has an entry in the symbols table, so these will be turned - // into appropriate outputs. - if (group.mode === "text") { - return buildCommon.makeOrd(group, options, "textord"); + // Rule 18c + let minSupShift; + if (options.style === Style.DISPLAY) { + minSupShift = metrics.sup1; + } else if (options.style.cramped) { + minSupShift = metrics.sup3; } else { - return makeSpan(["mspace"], - [buildCommon.mathsym(group.value, group.mode, options)], - options); + minSupShift = metrics.sup2; } - } else { - // Other kinds of spaces are of arbitrary width. We use CSS to - // generate these. - return makeSpan( - ["mspace", buildCommon.spacingFunctions[group.value].className], - [], options); - } -}; -groupTypes.horizBrace = function(group, options) { - const style = options.style; + // scriptspace is a font-size-independent size, so scale it + // appropriately for use as the marginRight. + const multiplier = options.sizeMultiplier; + const marginRight = (0.5 / metrics.ptPerEm) / multiplier + "em"; - const hasSupSub = (group.type === "supsub"); - let supSubGroup; - let newOptions; - if (hasSupSub) { - // Ref: LaTeX source2e: }}}}\limits} - // i.e. LaTeX treats the brace similar to an op and passes it - // with \limits, so we need to assign supsub style. - if (group.value.sup) { - newOptions = options.havingStyle(style.sup()); - supSubGroup = buildGroup(group.value.sup, newOptions, options); + let supsub; + if (!group.value.sup) { + // Rule 18b + subShift = Math.max( + subShift, metrics.sub1, + subm.height - 0.8 * metrics.xHeight); + + const vlistElem = [{type: "elem", elem: subm, marginRight}]; + // Subscripts shouldn't be shifted by the base's italic correction. + // Account for that by shifting the subscript back the appropriate + // amount. Note we only do this when the base is a single symbol. + if (base instanceof domTree.symbolNode) { + vlistElem[0].marginLeft = -base.italic + "em"; + } + + supsub = buildCommon.makeVList({ + positionType: "shift", + positionData: subShift, + children: vlistElem, + }, options); + } else if (!group.value.sub) { + // Rule 18c, d + supShift = Math.max(supShift, minSupShift, + supm.depth + 0.25 * metrics.xHeight); + + supsub = buildCommon.makeVList({ + positionType: "shift", + positionData: -supShift, + children: [{type: "elem", elem: supm, marginRight}], + }, options); } else { - newOptions = options.havingStyle(style.sub()); - supSubGroup = buildGroup(group.value.sub, newOptions, options); + supShift = Math.max( + supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight); + subShift = Math.max(subShift, metrics.sub2); + + const ruleWidth = metrics.defaultRuleThickness; + + // Rule 18e + const maxWidth = 4 * ruleWidth; + if ((supShift - supm.depth) - (subm.height - subShift) < maxWidth) { + subShift = maxWidth - (supShift - supm.depth) + subm.height; + const psi = 0.8 * metrics.xHeight - (supShift - supm.depth); + if (psi > 0) { + supShift += psi; + subShift -= psi; + } + } + + const vlistElem = [ + {type: "elem", elem: subm, shift: subShift, marginRight}, + {type: "elem", elem: supm, shift: -supShift, marginRight}, + ]; + // See comment above about subscripts not being shifted. + if (base instanceof domTree.symbolNode) { + vlistElem[0].marginLeft = -base.italic + "em"; + } + + supsub = buildCommon.makeVList({ + positionType: "individualShift", + children: vlistElem, + }, options); } - group = group.value.base; - } - // Build the base group - const body = buildGroup( - group.value.base, options.havingBaseStyle(Style.DISPLAY)); + // Wrap the supsub vlist in a span.msupsub to reset text-align. + const mclass = getTypeOfDomTree(base) || "mord"; + return makeSpan([mclass], + [base, makeSpan(["msupsub"], [supsub])], + options); + }, - // Create the stretchy element - const braceBody = stretchy.svgSpan(group, options); + spacing(group, options) { + if (buildCommon.regularSpace.hasOwnProperty(group.value)) { + // Spaces are generated by adding an actual space. Each of these + // things has an entry in the symbols table, so these will be turned + // into appropriate outputs. + if (group.mode === "text") { + return buildCommon.makeOrd(group, options, "textord"); + } else { + return makeSpan(["mspace"], + [buildCommon.mathsym(group.value, group.mode, options)], + options); + } + } else { + // Other kinds of spaces are of arbitrary width. We use CSS to + // generate these. + return makeSpan( + ["mspace", buildCommon.spacingFunctions[group.value].className], + [], options); + } + }, - // Generate the vlist, with the appropriate kerns ┏━━━━━━━━┓ - // This first vlist contains the subject matter and the brace: equation - let vlist; - if (group.value.isOver) { - vlist = buildCommon.makeVList({ - positionType: "firstBaseline", - children: [ - {type: "elem", elem: body}, - {type: "kern", size: 0.1}, - {type: "elem", elem: braceBody}, - ], - }, options); - vlist.children[0].children[0].children[1].classes.push("svg-align"); - } else { - vlist = buildCommon.makeVList({ - positionType: "bottom", - positionData: body.depth + 0.1 + braceBody.height, - children: [ - {type: "elem", elem: braceBody}, - {type: "kern", size: 0.1}, - {type: "elem", elem: body}, - ], - }, options); - vlist.children[0].children[0].children[0].classes.push("svg-align"); - } + horizBrace(group, options) { + const style = options.style; - if (hasSupSub) { - // In order to write the supsub, wrap the first vlist in another vlist: - // They can't all go in the same vlist, because the note might be wider - // than the equation. We want the equation to control the brace width. + const hasSupSub = (group.type === "supsub"); + let supSubGroup; + let newOptions; + if (hasSupSub) { + // Ref: LaTeX source2e: }}}}\limits} + // i.e. LaTeX treats the brace similar to an op and passes it + // with \limits, so we need to assign supsub style. + if (group.value.sup) { + newOptions = options.havingStyle(style.sup()); + supSubGroup = buildGroup(group.value.sup, newOptions, options); + } else { + newOptions = options.havingStyle(style.sub()); + supSubGroup = buildGroup(group.value.sub, newOptions, options); + } + group = group.value.base; + } - // note long note long note - // ┏━━━━━━━━┓ or ┏━━━┓ not ┏━━━━━━━━━┓ - // equation eqn eqn + // Build the base group + const body = buildGroup( + group.value.base, options.havingBaseStyle(Style.DISPLAY)); - const vSpan = makeSpan(["mord", - (group.value.isOver ? "mover" : "munder")], - [vlist], options); + // Create the stretchy element + const braceBody = stretchy.svgSpan(group, options); + // Generate the vlist, with the appropriate kerns ┏━━━━━━━━┓ + // This first vlist contains the content and the brace: equation + let vlist; if (group.value.isOver) { vlist = buildCommon.makeVList({ positionType: "firstBaseline", children: [ - {type: "elem", elem: vSpan}, - {type: "kern", size: 0.2}, - {type: "elem", elem: supSubGroup}, + {type: "elem", elem: body}, + {type: "kern", size: 0.1}, + {type: "elem", elem: braceBody}, + ], + }, options); + vlist.children[0].children[0].children[1].classes.push("svg-align"); + } else { + vlist = buildCommon.makeVList({ + positionType: "bottom", + positionData: body.depth + 0.1 + braceBody.height, + children: [ + {type: "elem", elem: braceBody}, + {type: "kern", size: 0.1}, + {type: "elem", elem: body}, + ], + }, options); + vlist.children[0].children[0].children[0].classes.push("svg-align"); + } + + if (hasSupSub) { + // To write the supsub, wrap the first vlist in another vlist: + // They can't all go in the same vlist, because the note might be + // wider than the equation. We want the equation to control the + // brace width. + + // note long note long note + // ┏━━━━━━━━┓ or ┏━━━┓ not ┏━━━━━━━━━┓ + // equation eqn eqn + + const vSpan = makeSpan( + ["mord", (group.value.isOver ? "mover" : "munder")], + [vlist], options); + + if (group.value.isOver) { + vlist = buildCommon.makeVList({ + positionType: "firstBaseline", + children: [ + {type: "elem", elem: vSpan}, + {type: "kern", size: 0.2}, + {type: "elem", elem: supSubGroup}, + ], + }, options); + } else { + vlist = buildCommon.makeVList({ + positionType: "bottom", + positionData: vSpan.depth + 0.2 + supSubGroup.height, + children: [ + {type: "elem", elem: supSubGroup}, + {type: "kern", size: 0.2}, + {type: "elem", elem: vSpan}, + ], + }, options); + } + } + + return makeSpan(["mord", (group.value.isOver ? "mover" : "munder")], + [vlist], options); + }, + + xArrow(group, options) { + const style = options.style; + + // Build the argument groups in the appropriate style. + // Ref: amsmath.dtx: \hbox{$\scriptstyle\mkern#3mu{#6}\mkern#4mu$}% + + let newOptions = options.havingStyle(style.sup()); + const upperGroup = buildGroup(group.value.body, newOptions, options); + upperGroup.classes.push("x-arrow-pad"); + + let lowerGroup; + if (group.value.below) { + // Build the lower group + newOptions = options.havingStyle(style.sub()); + lowerGroup = buildGroup(group.value.below, newOptions, options); + lowerGroup.classes.push("x-arrow-pad"); + } + + const arrowBody = stretchy.svgSpan(group, options); + + // Re shift: Note that stretchy.svgSpan returned arrowBody.depth = 0. + // The point we want on the math axis is at 0.5 * arrowBody.height. + const arrowShift = -options.fontMetrics().axisHeight + + 0.5 * arrowBody.height; + // 2 mu kern. Ref: amsmath.dtx: #7\if0#2\else\mkern#2mu\fi + let upperShift = + -options.fontMetrics().axisHeight - 0.5 * arrowBody.height - 0.111; + if (group.value.label === "\\xleftequilibrium") { + upperShift -= upperGroup.depth; + } + + // Generate the vlist + let vlist; + if (group.value.below) { + const lowerShift = -options.fontMetrics().axisHeight + + lowerGroup.height + 0.5 * arrowBody.height + + 0.111; + vlist = buildCommon.makeVList({ + positionType: "individualShift", + children: [ + {type: "elem", elem: upperGroup, shift: upperShift}, + {type: "elem", elem: arrowBody, shift: arrowShift}, + {type: "elem", elem: lowerGroup, shift: lowerShift}, ], }, options); } else { vlist = buildCommon.makeVList({ - positionType: "bottom", - positionData: vSpan.depth + 0.2 + supSubGroup.height, + positionType: "individualShift", children: [ - {type: "elem", elem: supSubGroup}, - {type: "kern", size: 0.2}, - {type: "elem", elem: vSpan}, + {type: "elem", elem: upperGroup, shift: upperShift}, + {type: "elem", elem: arrowBody, shift: arrowShift}, ], }, options); } - } - return makeSpan(["mord", (group.value.isOver ? "mover" : "munder")], - [vlist], options); -}; + vlist.children[0].children[0].children[1].classes.push("svg-align"); -groupTypes.xArrow = function(group, options) { - const style = options.style; + return makeSpan(["mrel", "x-arrow"], [vlist], options); + }, - // Build the argument groups in the appropriate style. - // Ref: amsmath.dtx: \hbox{$\scriptstyle\mkern#3mu{#6}\mkern#4mu$}% + mclass(group, options) { + const elements = buildExpression(group.value.value, options, true); - let newOptions = options.havingStyle(style.sup()); - const upperGroup = buildGroup(group.value.body, newOptions, options); - upperGroup.classes.push("x-arrow-pad"); + return makeSpan([group.value.mclass], elements, options); + }, - let lowerGroup; - if (group.value.below) { - // Build the lower group - newOptions = options.havingStyle(style.sub()); - lowerGroup = buildGroup(group.value.below, newOptions, options); - lowerGroup.classes.push("x-arrow-pad"); - } - - const arrowBody = stretchy.svgSpan(group, options); - - // Re shift: Note that stretchy.svgSpan returned arrowBody.depth = 0. - // The point we want on the math axis is at 0.5 * arrowBody.height. - const arrowShift = -options.fontMetrics().axisHeight + - 0.5 * arrowBody.height; - // 2 mu kern. Ref: amsmath.dtx: #7\if0#2\else\mkern#2mu\fi - let upperShift = -options.fontMetrics().axisHeight - - 0.5 * arrowBody.height - 0.111; - if (group.value.label === "\\xleftequilibrium") { - upperShift -= upperGroup.depth; - } - - // Generate the vlist - let vlist; - if (group.value.below) { - const lowerShift = -options.fontMetrics().axisHeight - + lowerGroup.height + 0.5 * arrowBody.height - + 0.111; - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [ - {type: "elem", elem: upperGroup, shift: upperShift}, - {type: "elem", elem: arrowBody, shift: arrowShift}, - {type: "elem", elem: lowerGroup, shift: lowerShift}, - ], + raisebox(group, options) { + const body = groupTypes.sizing({value: { + value: [{ + type: "text", + value: { + body: group.value.value, + font: "mathrm", // simulate \textrm + }, + }], + size: 6, // simulate \normalsize + }}, options); + const dy = calculateSize(group.value.dy.value, options); + return buildCommon.makeVList({ + positionType: "shift", + positionData: -dy, + children: [{type: "elem", elem: body}], }, options); - } else { - vlist = buildCommon.makeVList({ - positionType: "individualShift", - children: [ - {type: "elem", elem: upperGroup, shift: upperShift}, - {type: "elem", elem: arrowBody, shift: arrowShift}, - ], - }, options); - } - - vlist.children[0].children[0].children[1].classes.push("svg-align"); - - return makeSpan(["mrel", "x-arrow"], [vlist], options); -}; - -groupTypes.mclass = function(group, options) { - const elements = buildExpression(group.value.value, options, true); - - return makeSpan([group.value.mclass], elements, options); -}; - -groupTypes.raisebox = function(group, options) { - const body = groupTypes.sizing({value: { - value: [{ - type: "text", - value: { - body: group.value.value, - font: "mathrm", // simulate \textrm - }, - }], - size: 6, // simulate \normalsize - }}, options); - const dy = calculateSize(group.value.dy.value, options); - return buildCommon.makeVList({ - positionType: "shift", - positionData: -dy, - children: [{type: "elem", elem: body}], - }, options); + }, }; /** @@ -636,8 +613,8 @@ export const buildGroup = function(group, options, baseOptions) { groupNode = makeSpan(options.sizingClasses(baseOptions), [groupNode], options); - const multiplier = options.sizeMultiplier / - baseOptions.sizeMultiplier; + const multiplier = + options.sizeMultiplier / baseOptions.sizeMultiplier; groupNode.height *= multiplier; groupNode.depth *= multiplier;