Spaces:
Running
on
T4
Running
on
T4
| /* eslint no-console:0 */ | |
| /** | |
| * This file does the main work of building a domTree structure from a parse | |
| * tree. The entry point is the `buildHTML` function, which takes a parse tree. | |
| * Then, the buildExpression, buildGroup, and various groupTypes functions are | |
| * called, to produce a final HTML tree. | |
| */ | |
| var ParseError = require("./ParseError"); | |
| var Style = require("./Style"); | |
| var buildCommon = require("./buildCommon"); | |
| var delimiter = require("./delimiter"); | |
| var domTree = require("./domTree"); | |
| var fontMetrics = require("./fontMetrics"); | |
| var utils = require("./utils"); | |
| var makeSpan = buildCommon.makeSpan; | |
| /** | |
| * Take a list of nodes, build them in order, and return a list of the built | |
| * nodes. This function handles the `prev` node correctly, and passes the | |
| * previous element from the list as the prev of the next element. | |
| */ | |
| var buildExpression = function(expression, options, prev) { | |
| var groups = []; | |
| for (var i = 0; i < expression.length; i++) { | |
| var group = expression[i]; | |
| groups.push(buildGroup(group, options, prev)); | |
| prev = group; | |
| } | |
| return groups; | |
| }; | |
| // List of types used by getTypeOfGroup, | |
| // see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types | |
| var groupToType = { | |
| mathord: "mord", | |
| textord: "mord", | |
| bin: "mbin", | |
| rel: "mrel", | |
| text: "mord", | |
| open: "mopen", | |
| close: "mclose", | |
| inner: "minner", | |
| genfrac: "mord", | |
| array: "mord", | |
| spacing: "mord", | |
| punct: "mpunct", | |
| ordgroup: "mord", | |
| op: "mop", | |
| katex: "mord", | |
| overline: "mord", | |
| underline: "mord", | |
| rule: "mord", | |
| leftright: "minner", | |
| sqrt: "mord", | |
| accent: "mord", | |
| }; | |
| /** | |
| * Gets the final math type of an expression, given its group type. This type is | |
| * used to determine spacing between elements, and affects bin elements by | |
| * causing them to change depending on what types are around them. This type | |
| * must be attached to the outermost node of an element as a CSS class so that | |
| * spacing with its surrounding elements works correctly. | |
| * | |
| * Some elements can be mapped one-to-one from group type to math type, and | |
| * those are listed in the `groupToType` table. | |
| * | |
| * Others (usually elements that wrap around other elements) often have | |
| * recursive definitions, and thus call `getTypeOfGroup` on their inner | |
| * elements. | |
| */ | |
| var getTypeOfGroup = function(group) { | |
| if (group == null) { | |
| // Like when typesetting $^3$ | |
| return groupToType.mathord; | |
| } else if (group.type === "supsub") { | |
| return getTypeOfGroup(group.value.base); | |
| } else if (group.type === "llap" || group.type === "rlap") { | |
| return getTypeOfGroup(group.value); | |
| } else if (group.type === "color") { | |
| return getTypeOfGroup(group.value.value); | |
| } else if (group.type === "sizing") { | |
| return getTypeOfGroup(group.value.value); | |
| } else if (group.type === "styling") { | |
| return getTypeOfGroup(group.value.value); | |
| } else if (group.type === "delimsizing") { | |
| return groupToType[group.value.delimType]; | |
| } else { | |
| return groupToType[group.type]; | |
| } | |
| }; | |
| /** | |
| * Sometimes, groups perform special rules when they have superscripts or | |
| * subscripts attached to them. This function lets the `supsub` group know that | |
| * its inner element should handle the superscripts and subscripts instead of | |
| * handling them itself. | |
| */ | |
| var shouldHandleSupSub = function(group, options) { | |
| if (!group) { | |
| return false; | |
| } else if (group.type === "op") { | |
| // Operators handle supsubs differently when they have limits | |
| // (e.g. `\displaystyle\sum_2^3`) | |
| return group.value.limits && | |
| (options.style.size === Style.DISPLAY.size || | |
| group.value.alwaysHandleSupSub); | |
| } else if (group.type === "accent") { | |
| return isCharacterBox(group.value.base); | |
| } else { | |
| return null; | |
| } | |
| }; | |
| /** | |
| * Sometimes we want to pull out the innermost element of a group. In most | |
| * cases, this will just be the group itself, but when ordgroups and colors have | |
| * a single element, we want to pull that out. | |
| */ | |
| var getBaseElem = function(group) { | |
| if (!group) { | |
| return false; | |
| } else if (group.type === "ordgroup") { | |
| if (group.value.length === 1) { | |
| return getBaseElem(group.value[0]); | |
| } else { | |
| return group; | |
| } | |
| } else if (group.type === "color") { | |
| if (group.value.value.length === 1) { | |
| return getBaseElem(group.value.value[0]); | |
| } else { | |
| return group; | |
| } | |
| } else { | |
| return group; | |
| } | |
| }; | |
| /** | |
| * TeXbook algorithms often reference "character boxes", which are simply groups | |
| * with a single character in them. To decide if something is a character box, | |
| * we find its innermost group, and see if it is a single character. | |
| */ | |
| var isCharacterBox = function(group) { | |
| var baseElem = getBaseElem(group); | |
| // These are all they types of groups which hold single characters | |
| return baseElem.type === "mathord" || | |
| baseElem.type === "textord" || | |
| baseElem.type === "bin" || | |
| baseElem.type === "rel" || | |
| baseElem.type === "inner" || | |
| baseElem.type === "open" || | |
| baseElem.type === "close" || | |
| baseElem.type === "punct"; | |
| }; | |
| var makeNullDelimiter = function(options) { | |
| return makeSpan([ | |
| "sizing", "reset-" + options.size, "size5", | |
| options.style.reset(), Style.TEXT.cls(), | |
| "nulldelimiter", | |
| ]); | |
| }; | |
| /** | |
| * 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. | |
| */ | |
| var groupTypes = {}; | |
| groupTypes.mathord = function(group, options, prev) { | |
| return buildCommon.makeOrd(group, options, "mathord"); | |
| }; | |
| groupTypes.textord = function(group, options, prev) { | |
| return buildCommon.makeOrd(group, options, "textord"); | |
| }; | |
| groupTypes.bin = function(group, options, prev) { | |
| var className = "mbin"; | |
| // Pull out the most recent element. Do some special handling to find | |
| // things at the end of a \color group. Note that we don't use the same | |
| // logic for ordgroups (which count as ords). | |
| var prevAtom = prev; | |
| while (prevAtom && prevAtom.type === "color") { | |
| var atoms = prevAtom.value.value; | |
| prevAtom = atoms[atoms.length - 1]; | |
| } | |
| // See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19. | |
| // Here, we determine whether the bin should turn into an ord. We | |
| // currently only apply Rule 5. | |
| if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"], | |
| getTypeOfGroup(prevAtom))) { | |
| group.type = "textord"; | |
| className = "mord"; | |
| } | |
| return buildCommon.mathsym( | |
| group.value, group.mode, options.getColor(), [className]); | |
| }; | |
| groupTypes.rel = function(group, options, prev) { | |
| return buildCommon.mathsym( | |
| group.value, group.mode, options.getColor(), ["mrel"]); | |
| }; | |
| groupTypes.open = function(group, options, prev) { | |
| return buildCommon.mathsym( | |
| group.value, group.mode, options.getColor(), ["mopen"]); | |
| }; | |
| groupTypes.close = function(group, options, prev) { | |
| return buildCommon.mathsym( | |
| group.value, group.mode, options.getColor(), ["mclose"]); | |
| }; | |
| groupTypes.inner = function(group, options, prev) { | |
| return buildCommon.mathsym( | |
| group.value, group.mode, options.getColor(), ["minner"]); | |
| }; | |
| groupTypes.punct = function(group, options, prev) { | |
| return buildCommon.mathsym( | |
| group.value, group.mode, options.getColor(), ["mpunct"]); | |
| }; | |
| groupTypes.ordgroup = function(group, options, prev) { | |
| return makeSpan( | |
| ["mord", options.style.cls()], | |
| buildExpression(group.value, options.reset()) | |
| ); | |
| }; | |
| groupTypes.text = function(group, options, prev) { | |
| return makeSpan(["text", "mord", options.style.cls()], | |
| buildExpression(group.value.body, options.reset())); | |
| }; | |
| groupTypes.color = function(group, options, prev) { | |
| var elements = buildExpression( | |
| group.value.value, | |
| options.withColor(group.value.color), | |
| prev | |
| ); | |
| // \color isn't supposed to affect the type of the elements it contains. | |
| // To accomplish this, we wrap the results in a fragment, so the inner | |
| // elements will be able to directly interact with their neighbors. For | |
| // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3` | |
| return new buildCommon.makeFragment(elements); | |
| }; | |
| groupTypes.supsub = function(group, options, prev) { | |
| // 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.value.base, options)) { | |
| return groupTypes[group.value.base.type](group, options, prev); | |
| } | |
| var base = buildGroup(group.value.base, options.reset()); | |
| var supmid; | |
| var submid; | |
| var sup; | |
| var sub; | |
| if (group.value.sup) { | |
| sup = buildGroup(group.value.sup, | |
| options.withStyle(options.style.sup())); | |
| supmid = makeSpan( | |
| [options.style.reset(), options.style.sup().cls()], [sup]); | |
| } | |
| if (group.value.sub) { | |
| sub = buildGroup(group.value.sub, | |
| options.withStyle(options.style.sub())); | |
| submid = makeSpan( | |
| [options.style.reset(), options.style.sub().cls()], [sub]); | |
| } | |
| // Rule 18a | |
| var supShift; | |
| var subShift; | |
| if (isCharacterBox(group.value.base)) { | |
| supShift = 0; | |
| subShift = 0; | |
| } else { | |
| supShift = base.height - fontMetrics.metrics.supDrop; | |
| subShift = base.depth + fontMetrics.metrics.subDrop; | |
| } | |
| // Rule 18c | |
| var minSupShift; | |
| if (options.style === Style.DISPLAY) { | |
| minSupShift = fontMetrics.metrics.sup1; | |
| } else if (options.style.cramped) { | |
| minSupShift = fontMetrics.metrics.sup3; | |
| } else { | |
| minSupShift = fontMetrics.metrics.sup2; | |
| } | |
| // scriptspace is a font-size-independent size, so scale it | |
| // appropriately | |
| var multiplier = Style.TEXT.sizeMultiplier * | |
| options.style.sizeMultiplier; | |
| var scriptspace = | |
| (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em"; | |
| var supsub; | |
| if (!group.value.sup) { | |
| // Rule 18b | |
| subShift = Math.max( | |
| subShift, fontMetrics.metrics.sub1, | |
| sub.height - 0.8 * fontMetrics.metrics.xHeight); | |
| supsub = buildCommon.makeVList([ | |
| {type: "elem", elem: submid}, | |
| ], "shift", subShift, options); | |
| supsub.children[0].style.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) { | |
| supsub.children[0].style.marginLeft = -base.italic + "em"; | |
| } | |
| } else if (!group.value.sub) { | |
| // Rule 18c, d | |
| supShift = Math.max(supShift, minSupShift, | |
| sup.depth + 0.25 * fontMetrics.metrics.xHeight); | |
| supsub = buildCommon.makeVList([ | |
| {type: "elem", elem: supmid}, | |
| ], "shift", -supShift, options); | |
| supsub.children[0].style.marginRight = scriptspace; | |
| } else { | |
| supShift = Math.max( | |
| supShift, minSupShift, | |
| sup.depth + 0.25 * fontMetrics.metrics.xHeight); | |
| subShift = Math.max(subShift, fontMetrics.metrics.sub2); | |
| var ruleWidth = fontMetrics.metrics.defaultRuleThickness; | |
| // Rule 18e | |
| if ((supShift - sup.depth) - (sub.height - subShift) < | |
| 4 * ruleWidth) { | |
| subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height; | |
| var psi = 0.8 * fontMetrics.metrics.xHeight - | |
| (supShift - sup.depth); | |
| if (psi > 0) { | |
| supShift += psi; | |
| subShift -= psi; | |
| } | |
| } | |
| supsub = buildCommon.makeVList([ | |
| {type: "elem", elem: submid, shift: subShift}, | |
| {type: "elem", elem: supmid, shift: -supShift}, | |
| ], "individualShift", null, options); | |
| // See comment above about subscripts not being shifted | |
| if (base instanceof domTree.symbolNode) { | |
| supsub.children[0].style.marginLeft = -base.italic + "em"; | |
| } | |
| supsub.children[0].style.marginRight = scriptspace; | |
| supsub.children[1].style.marginRight = scriptspace; | |
| } | |
| return makeSpan([getTypeOfGroup(group.value.base)], | |
| [base, supsub]); | |
| }; | |
| groupTypes.genfrac = function(group, options, prev) { | |
| // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e). | |
| // Figure out what style this fraction should be in based on the | |
| // function used | |
| var fstyle = options.style; | |
| if (group.value.size === "display") { | |
| fstyle = Style.DISPLAY; | |
| } else if (group.value.size === "text") { | |
| fstyle = Style.TEXT; | |
| } | |
| var nstyle = fstyle.fracNum(); | |
| var dstyle = fstyle.fracDen(); | |
| var numer = buildGroup(group.value.numer, options.withStyle(nstyle)); | |
| var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]); | |
| var denom = buildGroup(group.value.denom, options.withStyle(dstyle)); | |
| var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom]); | |
| var ruleWidth; | |
| if (group.value.hasBarLine) { | |
| ruleWidth = fontMetrics.metrics.defaultRuleThickness / | |
| options.style.sizeMultiplier; | |
| } else { | |
| ruleWidth = 0; | |
| } | |
| // Rule 15b | |
| var numShift; | |
| var clearance; | |
| var denomShift; | |
| if (fstyle.size === Style.DISPLAY.size) { | |
| numShift = fontMetrics.metrics.num1; | |
| if (ruleWidth > 0) { | |
| clearance = 3 * ruleWidth; | |
| } else { | |
| clearance = 7 * fontMetrics.metrics.defaultRuleThickness; | |
| } | |
| denomShift = fontMetrics.metrics.denom1; | |
| } else { | |
| if (ruleWidth > 0) { | |
| numShift = fontMetrics.metrics.num2; | |
| clearance = ruleWidth; | |
| } else { | |
| numShift = fontMetrics.metrics.num3; | |
| clearance = 3 * fontMetrics.metrics.defaultRuleThickness; | |
| } | |
| denomShift = fontMetrics.metrics.denom2; | |
| } | |
| var frac; | |
| if (ruleWidth === 0) { | |
| // Rule 15c | |
| var candiateClearance = | |
| (numShift - numer.depth) - (denom.height - denomShift); | |
| if (candiateClearance < clearance) { | |
| numShift += 0.5 * (clearance - candiateClearance); | |
| denomShift += 0.5 * (clearance - candiateClearance); | |
| } | |
| frac = buildCommon.makeVList([ | |
| {type: "elem", elem: denomreset, shift: denomShift}, | |
| {type: "elem", elem: numerreset, shift: -numShift}, | |
| ], "individualShift", null, options); | |
| } else { | |
| // Rule 15d | |
| var axisHeight = fontMetrics.metrics.axisHeight; | |
| if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) < | |
| clearance) { | |
| numShift += | |
| clearance - ((numShift - numer.depth) - | |
| (axisHeight + 0.5 * ruleWidth)); | |
| } | |
| if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) < | |
| clearance) { | |
| denomShift += | |
| clearance - ((axisHeight - 0.5 * ruleWidth) - | |
| (denom.height - denomShift)); | |
| } | |
| var mid = makeSpan( | |
| [options.style.reset(), Style.TEXT.cls(), "frac-line"]); | |
| // Manually set the height of the line because its height is | |
| // created in CSS | |
| mid.height = ruleWidth; | |
| var midShift = -(axisHeight - 0.5 * ruleWidth); | |
| frac = buildCommon.makeVList([ | |
| {type: "elem", elem: denomreset, shift: denomShift}, | |
| {type: "elem", elem: mid, shift: midShift}, | |
| {type: "elem", elem: numerreset, shift: -numShift}, | |
| ], "individualShift", null, options); | |
| } | |
| // Since we manually change the style sometimes (with \dfrac or \tfrac), | |
| // account for the possible size change here. | |
| frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier; | |
| frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier; | |
| // Rule 15e | |
| var delimSize; | |
| if (fstyle.size === Style.DISPLAY.size) { | |
| delimSize = fontMetrics.metrics.delim1; | |
| } else { | |
| delimSize = fontMetrics.metrics.getDelim2(fstyle); | |
| } | |
| var leftDelim; | |
| var rightDelim; | |
| if (group.value.leftDelim == null) { | |
| leftDelim = makeNullDelimiter(options); | |
| } else { | |
| leftDelim = delimiter.customSizedDelim( | |
| group.value.leftDelim, delimSize, true, | |
| options.withStyle(fstyle), group.mode); | |
| } | |
| if (group.value.rightDelim == null) { | |
| rightDelim = makeNullDelimiter(options); | |
| } else { | |
| rightDelim = delimiter.customSizedDelim( | |
| group.value.rightDelim, delimSize, true, | |
| options.withStyle(fstyle), group.mode); | |
| } | |
| return makeSpan( | |
| ["mord", options.style.reset(), fstyle.cls()], | |
| [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim], | |
| options.getColor()); | |
| }; | |
| groupTypes.array = function(group, options, prev) { | |
| var r; | |
| var c; | |
| var nr = group.value.body.length; | |
| var nc = 0; | |
| var body = new Array(nr); | |
| // Horizontal spacing | |
| var pt = 1 / fontMetrics.metrics.ptPerEm; | |
| var arraycolsep = 5 * pt; // \arraycolsep in article.cls | |
| // Vertical spacing | |
| var baselineskip = 12 * pt; // see size10.clo | |
| // Default \arraystretch from lttab.dtx | |
| // TODO(gagern): may get redefined once we have user-defined macros | |
| var arraystretch = utils.deflt(group.value.arraystretch, 1); | |
| var arrayskip = arraystretch * baselineskip; | |
| var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and | |
| var arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx | |
| var totalHeight = 0; | |
| for (r = 0; r < group.value.body.length; ++r) { | |
| var inrow = group.value.body[r]; | |
| var height = arstrutHeight; // \@array adds an \@arstrut | |
| var depth = arstrutDepth; // to each tow (via the template) | |
| if (nc < inrow.length) { | |
| nc = inrow.length; | |
| } | |
| var outrow = new Array(inrow.length); | |
| for (c = 0; c < inrow.length; ++c) { | |
| var elt = buildGroup(inrow[c], options); | |
| if (depth < elt.depth) { | |
| depth = elt.depth; | |
| } | |
| if (height < elt.height) { | |
| height = elt.height; | |
| } | |
| outrow[c] = elt; | |
| } | |
| var gap = 0; | |
| if (group.value.rowGaps[r]) { | |
| gap = group.value.rowGaps[r].value; | |
| switch (gap.unit) { | |
| case "em": | |
| gap = gap.number; | |
| break; | |
| case "ex": | |
| gap = gap.number * fontMetrics.metrics.emPerEx; | |
| break; | |
| default: | |
| console.error("Can't handle unit " + gap.unit); | |
| gap = 0; | |
| } | |
| if (gap > 0) { // \@argarraycr | |
| gap += arstrutDepth; | |
| if (depth < gap) { | |
| depth = gap; // \@xargarraycr | |
| } | |
| gap = 0; | |
| } | |
| } | |
| outrow.height = height; | |
| outrow.depth = depth; | |
| totalHeight += height; | |
| outrow.pos = totalHeight; | |
| totalHeight += depth + gap; // \@yargarraycr | |
| body[r] = outrow; | |
| } | |
| var offset = totalHeight / 2 + fontMetrics.metrics.axisHeight; | |
| var colDescriptions = group.value.cols || []; | |
| var cols = []; | |
| var colSep; | |
| var colDescrNum; | |
| for (c = 0, colDescrNum = 0; | |
| // Continue while either there are more columns or more column | |
| // descriptions, so trailing separators don't get lost. | |
| c < nc || colDescrNum < colDescriptions.length; | |
| ++c, ++colDescrNum) { | |
| var colDescr = colDescriptions[colDescrNum] || {}; | |
| var firstSeparator = true; | |
| while (colDescr.type === "separator") { | |
| // If there is more than one separator in a row, add a space | |
| // between them. | |
| if (!firstSeparator) { | |
| colSep = makeSpan(["arraycolsep"], []); | |
| colSep.style.width = | |
| fontMetrics.metrics.doubleRuleSep + "em"; | |
| cols.push(colSep); | |
| } | |
| if (colDescr.separator === "|") { | |
| var separator = makeSpan( | |
| ["vertical-separator"], | |
| []); | |
| separator.style.height = totalHeight + "em"; | |
| separator.style.verticalAlign = | |
| -(totalHeight - offset) + "em"; | |
| cols.push(separator); | |
| } else { | |
| throw new ParseError( | |
| "Invalid separator type: " + colDescr.separator); | |
| } | |
| colDescrNum++; | |
| colDescr = colDescriptions[colDescrNum] || {}; | |
| firstSeparator = false; | |
| } | |
| if (c >= nc) { | |
| continue; | |
| } | |
| var sepwidth; | |
| if (c > 0 || group.value.hskipBeforeAndAfter) { | |
| sepwidth = utils.deflt(colDescr.pregap, arraycolsep); | |
| if (sepwidth !== 0) { | |
| colSep = makeSpan(["arraycolsep"], []); | |
| colSep.style.width = sepwidth + "em"; | |
| cols.push(colSep); | |
| } | |
| } | |
| var col = []; | |
| for (r = 0; r < nr; ++r) { | |
| var row = body[r]; | |
| var elem = row[c]; | |
| if (!elem) { | |
| continue; | |
| } | |
| var shift = row.pos - offset; | |
| elem.depth = row.depth; | |
| elem.height = row.height; | |
| col.push({type: "elem", elem: elem, shift: shift}); | |
| } | |
| col = buildCommon.makeVList(col, "individualShift", null, options); | |
| col = makeSpan( | |
| ["col-align-" + (colDescr.align || "c")], | |
| [col]); | |
| cols.push(col); | |
| if (c < nc - 1 || group.value.hskipBeforeAndAfter) { | |
| sepwidth = utils.deflt(colDescr.postgap, arraycolsep); | |
| if (sepwidth !== 0) { | |
| colSep = makeSpan(["arraycolsep"], []); | |
| colSep.style.width = sepwidth + "em"; | |
| cols.push(colSep); | |
| } | |
| } | |
| } | |
| body = makeSpan(["mtable"], cols); | |
| return makeSpan(["mord"], [body], options.getColor()); | |
| }; | |
| groupTypes.spacing = function(group, options, prev) { | |
| if (group.value === "\\ " || group.value === "\\space" || | |
| group.value === " " || 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. | |
| return makeSpan( | |
| ["mord", "mspace"], | |
| [buildCommon.mathsym(group.value, group.mode)] | |
| ); | |
| } else { | |
| // Other kinds of spaces are of arbitrary width. We use CSS to | |
| // generate these. | |
| return makeSpan( | |
| ["mord", "mspace", | |
| buildCommon.spacingFunctions[group.value].className]); | |
| } | |
| }; | |
| groupTypes.llap = function(group, options, prev) { | |
| var inner = makeSpan( | |
| ["inner"], [buildGroup(group.value.body, options.reset())]); | |
| var fix = makeSpan(["fix"], []); | |
| return makeSpan( | |
| ["llap", options.style.cls()], [inner, fix]); | |
| }; | |
| groupTypes.rlap = function(group, options, prev) { | |
| var inner = makeSpan( | |
| ["inner"], [buildGroup(group.value.body, options.reset())]); | |
| var fix = makeSpan(["fix"], []); | |
| return makeSpan( | |
| ["rlap", options.style.cls()], [inner, fix]); | |
| }; | |
| groupTypes.op = function(group, options, prev) { | |
| // Operators are handled in the TeXbook pg. 443-444, rule 13(a). | |
| var supGroup; | |
| var subGroup; | |
| var hasLimits = false; | |
| if (group.type === "supsub" ) { | |
| // If we have limits, supsub will pass us its group to handle. Pull | |
| // out the superscript and subscript and set the group to the op in | |
| // its base. | |
| supGroup = group.value.sup; | |
| subGroup = group.value.sub; | |
| group = group.value.base; | |
| hasLimits = true; | |
| } | |
| // Most operators have a large successor symbol, but these don't. | |
| var noSuccessor = [ | |
| "\\smallint", | |
| ]; | |
| var large = false; | |
| if (options.style.size === Style.DISPLAY.size && | |
| group.value.symbol && | |
| !utils.contains(noSuccessor, group.value.body)) { | |
| // Most symbol operators get larger in displaystyle (rule 13) | |
| large = true; | |
| } | |
| var base; | |
| var baseShift = 0; | |
| var slant = 0; | |
| if (group.value.symbol) { | |
| // If this is a symbol, create the symbol. | |
| var style = large ? "Size2-Regular" : "Size1-Regular"; | |
| base = buildCommon.makeSymbol( | |
| group.value.body, style, "math", options.getColor(), | |
| ["op-symbol", large ? "large-op" : "small-op", "mop"]); | |
| // Shift the symbol so its center lies on the axis (rule 13). It | |
| // appears that our fonts have the centers of the symbols already | |
| // almost on the axis, so these numbers are very small. Note we | |
| // don't actually apply this here, but instead it is used either in | |
| // the vlist creation or separately when there are no limits. | |
| baseShift = (base.height - base.depth) / 2 - | |
| fontMetrics.metrics.axisHeight * | |
| options.style.sizeMultiplier; | |
| // The slant of the symbol is just its italic correction. | |
| slant = base.italic; | |
| } else { | |
| // Otherwise, this is a text operator. Build the text from the | |
| // operator's name. | |
| // TODO(emily): Add a space in the middle of some of these | |
| // operators, like \limsup | |
| var output = []; | |
| for (var i = 1; i < group.value.body.length; i++) { | |
| output.push(buildCommon.mathsym(group.value.body[i], group.mode)); | |
| } | |
| base = makeSpan(["mop"], output, options.getColor()); | |
| } | |
| if (hasLimits) { | |
| // IE 8 clips \int if it is in a display: inline-block. We wrap it | |
| // in a new span so it is an inline, and works. | |
| base = makeSpan([], [base]); | |
| var supmid; | |
| var supKern; | |
| var submid; | |
| var subKern; | |
| // We manually have to handle the superscripts and subscripts. This, | |
| // aside from the kern calculations, is copied from supsub. | |
| if (supGroup) { | |
| var sup = buildGroup( | |
| supGroup, options.withStyle(options.style.sup())); | |
| supmid = makeSpan( | |
| [options.style.reset(), options.style.sup().cls()], [sup]); | |
| supKern = Math.max( | |
| fontMetrics.metrics.bigOpSpacing1, | |
| fontMetrics.metrics.bigOpSpacing3 - sup.depth); | |
| } | |
| if (subGroup) { | |
| var sub = buildGroup( | |
| subGroup, options.withStyle(options.style.sub())); | |
| submid = makeSpan( | |
| [options.style.reset(), options.style.sub().cls()], | |
| [sub]); | |
| subKern = Math.max( | |
| fontMetrics.metrics.bigOpSpacing2, | |
| fontMetrics.metrics.bigOpSpacing4 - sub.height); | |
| } | |
| // Build the final group as a vlist of the possible subscript, base, | |
| // and possible superscript. | |
| var finalGroup; | |
| var top; | |
| var bottom; | |
| if (!supGroup) { | |
| top = base.height - baseShift; | |
| finalGroup = buildCommon.makeVList([ | |
| {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, | |
| {type: "elem", elem: submid}, | |
| {type: "kern", size: subKern}, | |
| {type: "elem", elem: base}, | |
| ], "top", top, options); | |
| // Here, we shift the limits by the slant of the symbol. Note | |
| // that we are supposed to shift the limits by 1/2 of the slant, | |
| // but since we are centering the limits adding a full slant of | |
| // margin will shift by 1/2 that. | |
| finalGroup.children[0].style.marginLeft = -slant + "em"; | |
| } else if (!subGroup) { | |
| bottom = base.depth + baseShift; | |
| finalGroup = buildCommon.makeVList([ | |
| {type: "elem", elem: base}, | |
| {type: "kern", size: supKern}, | |
| {type: "elem", elem: supmid}, | |
| {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, | |
| ], "bottom", bottom, options); | |
| // See comment above about slants | |
| finalGroup.children[1].style.marginLeft = slant + "em"; | |
| } else if (!supGroup && !subGroup) { | |
| // This case probably shouldn't occur (this would mean the | |
| // supsub was sending us a group with no superscript or | |
| // subscript) but be safe. | |
| return base; | |
| } else { | |
| bottom = fontMetrics.metrics.bigOpSpacing5 + | |
| submid.height + submid.depth + | |
| subKern + | |
| base.depth + baseShift; | |
| finalGroup = buildCommon.makeVList([ | |
| {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, | |
| {type: "elem", elem: submid}, | |
| {type: "kern", size: subKern}, | |
| {type: "elem", elem: base}, | |
| {type: "kern", size: supKern}, | |
| {type: "elem", elem: supmid}, | |
| {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, | |
| ], "bottom", bottom, options); | |
| // See comment above about slants | |
| finalGroup.children[0].style.marginLeft = -slant + "em"; | |
| finalGroup.children[2].style.marginLeft = slant + "em"; | |
| } | |
| return makeSpan(["mop", "op-limits"], [finalGroup]); | |
| } else { | |
| if (group.value.symbol) { | |
| base.style.top = baseShift + "em"; | |
| } | |
| return base; | |
| } | |
| }; | |
| groupTypes.katex = function(group, options, prev) { | |
| // The KaTeX logo. The offsets for the K and a were chosen to look | |
| // good, but the offsets for the T, E, and X were taken from the | |
| // definition of \TeX in TeX (see TeXbook pg. 356) | |
| var k = makeSpan( | |
| ["k"], [buildCommon.mathsym("K", group.mode)]); | |
| var a = makeSpan( | |
| ["a"], [buildCommon.mathsym("A", group.mode)]); | |
| a.height = (a.height + 0.2) * 0.75; | |
| a.depth = (a.height - 0.2) * 0.75; | |
| var t = makeSpan( | |
| ["t"], [buildCommon.mathsym("T", group.mode)]); | |
| var e = makeSpan( | |
| ["e"], [buildCommon.mathsym("E", group.mode)]); | |
| e.height = (e.height - 0.2155); | |
| e.depth = (e.depth + 0.2155); | |
| var x = makeSpan( | |
| ["x"], [buildCommon.mathsym("X", group.mode)]); | |
| return makeSpan( | |
| ["katex-logo", "mord"], [k, a, t, e, x], options.getColor()); | |
| }; | |
| groupTypes.overline = function(group, options, prev) { | |
| // Overlines are handled in the TeXbook pg 443, Rule 9. | |
| // Build the inner group in the cramped style. | |
| var innerGroup = buildGroup(group.value.body, | |
| options.withStyle(options.style.cramp())); | |
| var ruleWidth = fontMetrics.metrics.defaultRuleThickness / | |
| options.style.sizeMultiplier; | |
| // Create the line above the body | |
| var line = makeSpan( | |
| [options.style.reset(), Style.TEXT.cls(), "overline-line"]); | |
| line.height = ruleWidth; | |
| line.maxFontSize = 1.0; | |
| // Generate the vlist, with the appropriate kerns | |
| var vlist = buildCommon.makeVList([ | |
| {type: "elem", elem: innerGroup}, | |
| {type: "kern", size: 3 * ruleWidth}, | |
| {type: "elem", elem: line}, | |
| {type: "kern", size: ruleWidth}, | |
| ], "firstBaseline", null, options); | |
| return makeSpan(["overline", "mord"], [vlist], options.getColor()); | |
| }; | |
| groupTypes.underline = function(group, options, prev) { | |
| // Underlines are handled in the TeXbook pg 443, Rule 10. | |
| // Build the inner group. | |
| var innerGroup = buildGroup(group.value.body, options); | |
| var ruleWidth = fontMetrics.metrics.defaultRuleThickness / | |
| options.style.sizeMultiplier; | |
| // Create the line above the body | |
| var line = makeSpan( | |
| [options.style.reset(), Style.TEXT.cls(), "underline-line"]); | |
| line.height = ruleWidth; | |
| line.maxFontSize = 1.0; | |
| // Generate the vlist, with the appropriate kerns | |
| var vlist = buildCommon.makeVList([ | |
| {type: "kern", size: ruleWidth}, | |
| {type: "elem", elem: line}, | |
| {type: "kern", size: 3 * ruleWidth}, | |
| {type: "elem", elem: innerGroup}, | |
| ], "top", innerGroup.height, options); | |
| return makeSpan(["underline", "mord"], [vlist], options.getColor()); | |
| }; | |
| groupTypes.sqrt = function(group, options, prev) { | |
| // Square roots are handled in the TeXbook pg. 443, Rule 11. | |
| // First, we do the same steps as in overline to build the inner group | |
| // and line | |
| var inner = buildGroup(group.value.body, | |
| options.withStyle(options.style.cramp())); | |
| var ruleWidth = fontMetrics.metrics.defaultRuleThickness / | |
| options.style.sizeMultiplier; | |
| var line = makeSpan( | |
| [options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [], | |
| options.getColor()); | |
| line.height = ruleWidth; | |
| line.maxFontSize = 1.0; | |
| var phi = ruleWidth; | |
| if (options.style.id < Style.TEXT.id) { | |
| phi = fontMetrics.metrics.xHeight; | |
| } | |
| // Calculate the clearance between the body and line | |
| var lineClearance = ruleWidth + phi / 4; | |
| var innerHeight = | |
| (inner.height + inner.depth) * options.style.sizeMultiplier; | |
| var minDelimiterHeight = innerHeight + lineClearance + ruleWidth; | |
| // Create a \surd delimiter of the required minimum size | |
| var delim = makeSpan(["sqrt-sign"], [ | |
| delimiter.customSizedDelim("\\surd", minDelimiterHeight, | |
| false, options, group.mode)], | |
| options.getColor()); | |
| var delimDepth = (delim.height + delim.depth) - ruleWidth; | |
| // Adjust the clearance based on the delimiter size | |
| if (delimDepth > inner.height + inner.depth + lineClearance) { | |
| lineClearance = | |
| (lineClearance + delimDepth - inner.height - inner.depth) / 2; | |
| } | |
| // Shift the delimiter so that its top lines up with the top of the line | |
| var delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height; | |
| delim.style.top = delimShift + "em"; | |
| delim.height -= delimShift; | |
| delim.depth += delimShift; | |
| // We add a special case here, because even when `inner` is empty, we | |
| // still get a line. So, we use a simple heuristic to decide if we | |
| // should omit the body entirely. (note this doesn't work for something | |
| // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for | |
| // it not to work. | |
| var body; | |
| if (inner.height === 0 && inner.depth === 0) { | |
| body = makeSpan(); | |
| } else { | |
| body = buildCommon.makeVList([ | |
| {type: "elem", elem: inner}, | |
| {type: "kern", size: lineClearance}, | |
| {type: "elem", elem: line}, | |
| {type: "kern", size: ruleWidth}, | |
| ], "firstBaseline", null, options); | |
| } | |
| if (!group.value.index) { | |
| return makeSpan(["sqrt", "mord"], [delim, body]); | |
| } else { | |
| // Handle the optional root index | |
| // The index is always in scriptscript style | |
| var root = buildGroup( | |
| group.value.index, | |
| options.withStyle(Style.SCRIPTSCRIPT)); | |
| var rootWrap = makeSpan( | |
| [options.style.reset(), Style.SCRIPTSCRIPT.cls()], | |
| [root]); | |
| // Figure out the height and depth of the inner part | |
| var innerRootHeight = Math.max(delim.height, body.height); | |
| var innerRootDepth = Math.max(delim.depth, body.depth); | |
| // The amount the index is shifted by. This is taken from the TeX | |
| // source, in the definition of `\r@@t`. | |
| var toShift = 0.6 * (innerRootHeight - innerRootDepth); | |
| // Build a VList with the superscript shifted up correctly | |
| var rootVList = buildCommon.makeVList( | |
| [{type: "elem", elem: rootWrap}], | |
| "shift", -toShift, options); | |
| // Add a class surrounding it so we can add on the appropriate | |
| // kerning | |
| var rootVListWrap = makeSpan(["root"], [rootVList]); | |
| return makeSpan(["sqrt", "mord"], [rootVListWrap, delim, body]); | |
| } | |
| }; | |
| groupTypes.sizing = function(group, options, prev) { | |
| // Handle sizing operators like \Huge. Real TeX doesn't actually allow | |
| // these functions inside of math expressions, so we do some special | |
| // handling. | |
| var inner = buildExpression(group.value.value, | |
| options.withSize(group.value.size), prev); | |
| var span = makeSpan(["mord"], | |
| [makeSpan(["sizing", "reset-" + options.size, group.value.size, | |
| options.style.cls()], | |
| inner)]); | |
| // Calculate the correct maxFontSize manually | |
| var fontSize = buildCommon.sizingMultiplier[group.value.size]; | |
| span.maxFontSize = fontSize * options.style.sizeMultiplier; | |
| return span; | |
| }; | |
| groupTypes.styling = function(group, options, prev) { | |
| // Style changes are handled in the TeXbook on pg. 442, Rule 3. | |
| // Figure out what style we're changing to. | |
| var style = { | |
| "display": Style.DISPLAY, | |
| "text": Style.TEXT, | |
| "script": Style.SCRIPT, | |
| "scriptscript": Style.SCRIPTSCRIPT, | |
| }; | |
| var newStyle = style[group.value.style]; | |
| // Build the inner expression in the new style. | |
| var inner = buildExpression( | |
| group.value.value, options.withStyle(newStyle), prev); | |
| return makeSpan([options.style.reset(), newStyle.cls()], inner); | |
| }; | |
| groupTypes.font = function(group, options, prev) { | |
| var font = group.value.font; | |
| return buildGroup(group.value.body, options.withFont(font), prev); | |
| }; | |
| groupTypes.delimsizing = function(group, options, prev) { | |
| var delim = group.value.value; | |
| if (delim === ".") { | |
| // Empty delimiters still count as elements, even though they don't | |
| // show anything. | |
| return makeSpan([groupToType[group.value.delimType]]); | |
| } | |
| // Use delimiter.sizedDelim to generate the delimiter. | |
| return makeSpan( | |
| [groupToType[group.value.delimType]], | |
| [delimiter.sizedDelim( | |
| delim, group.value.size, options, group.mode)]); | |
| }; | |
| groupTypes.leftright = function(group, options, prev) { | |
| // Build the inner expression | |
| var inner = buildExpression(group.value.body, options.reset()); | |
| var innerHeight = 0; | |
| var innerDepth = 0; | |
| // Calculate its height and depth | |
| for (var i = 0; i < inner.length; i++) { | |
| innerHeight = Math.max(inner[i].height, innerHeight); | |
| innerDepth = Math.max(inner[i].depth, innerDepth); | |
| } | |
| // The size of delimiters is the same, regardless of what style we are | |
| // in. Thus, to correctly calculate the size of delimiter we need around | |
| // a group, we scale down the inner size based on the size. | |
| innerHeight *= options.style.sizeMultiplier; | |
| innerDepth *= options.style.sizeMultiplier; | |
| var leftDelim; | |
| if (group.value.left === ".") { | |
| // Empty delimiters in \left and \right make null delimiter spaces. | |
| leftDelim = makeNullDelimiter(options); | |
| } else { | |
| // Otherwise, use leftRightDelim to generate the correct sized | |
| // delimiter. | |
| leftDelim = delimiter.leftRightDelim( | |
| group.value.left, innerHeight, innerDepth, options, | |
| group.mode); | |
| } | |
| // Add it to the beginning of the expression | |
| inner.unshift(leftDelim); | |
| var rightDelim; | |
| // Same for the right delimiter | |
| if (group.value.right === ".") { | |
| rightDelim = makeNullDelimiter(options); | |
| } else { | |
| rightDelim = delimiter.leftRightDelim( | |
| group.value.right, innerHeight, innerDepth, options, | |
| group.mode); | |
| } | |
| // Add it to the end of the expression. | |
| inner.push(rightDelim); | |
| return makeSpan( | |
| ["minner", options.style.cls()], inner, options.getColor()); | |
| }; | |
| groupTypes.rule = function(group, options, prev) { | |
| // Make an empty span for the rule | |
| var rule = makeSpan(["mord", "rule"], [], options.getColor()); | |
| // Calculate the shift, width, and height of the rule, and account for units | |
| var shift = 0; | |
| if (group.value.shift) { | |
| shift = group.value.shift.number; | |
| if (group.value.shift.unit === "ex") { | |
| shift *= fontMetrics.metrics.xHeight; | |
| } | |
| } | |
| var width = group.value.width.number; | |
| if (group.value.width.unit === "ex") { | |
| width *= fontMetrics.metrics.xHeight; | |
| } | |
| var height = group.value.height.number; | |
| if (group.value.height.unit === "ex") { | |
| height *= fontMetrics.metrics.xHeight; | |
| } | |
| // The sizes of rules are absolute, so make it larger if we are in a | |
| // smaller style. | |
| shift /= options.style.sizeMultiplier; | |
| width /= options.style.sizeMultiplier; | |
| height /= options.style.sizeMultiplier; | |
| // Style the rule to the right size | |
| rule.style.borderRightWidth = width + "em"; | |
| rule.style.borderTopWidth = height + "em"; | |
| rule.style.bottom = shift + "em"; | |
| // Record the height and width | |
| rule.width = width; | |
| rule.height = height + shift; | |
| rule.depth = -shift; | |
| return rule; | |
| }; | |
| groupTypes.accent = function(group, options, prev) { | |
| // Accents are handled in the TeXbook pg. 443, rule 12. | |
| var base = group.value.base; | |
| var supsubGroup; | |
| if (group.type === "supsub") { | |
| // If our base is a character box, and we have superscripts and | |
| // subscripts, the supsub will defer to us. In particular, we want | |
| // to attach the superscripts and subscripts to the inner body (so | |
| // that the position of the superscripts and subscripts won't be | |
| // affected by the height of the accent). We accomplish this by | |
| // sticking the base of the accent into the base of the supsub, and | |
| // rendering that, while keeping track of where the accent is. | |
| // The supsub group is the group that was passed in | |
| var supsub = group; | |
| // The real accent group is the base of the supsub group | |
| group = supsub.value.base; | |
| // The character box is the base of the accent group | |
| base = group.value.base; | |
| // Stick the character box into the base of the supsub group | |
| supsub.value.base = base; | |
| // Rerender the supsub group with its new base, and store that | |
| // result. | |
| supsubGroup = buildGroup( | |
| supsub, options.reset(), prev); | |
| } | |
| // Build the base group | |
| var body = buildGroup( | |
| base, options.withStyle(options.style.cramp())); | |
| // Calculate the skew of the accent. This is based on the line "If the | |
| // nucleus is not a single character, let s = 0; otherwise set s to the | |
| // kern amount for the nucleus followed by the \skewchar of its font." | |
| // Note that our skew metrics are just the kern between each character | |
| // and the skewchar. | |
| var skew; | |
| if (isCharacterBox(base)) { | |
| // If the base is a character box, then we want the skew of the | |
| // innermost character. To do that, we find the innermost character: | |
| var baseChar = getBaseElem(base); | |
| // Then, we render its group to get the symbol inside it | |
| var baseGroup = buildGroup( | |
| baseChar, options.withStyle(options.style.cramp())); | |
| // Finally, we pull the skew off of the symbol. | |
| skew = baseGroup.skew; | |
| // Note that we now throw away baseGroup, because the layers we | |
| // removed with getBaseElem might contain things like \color which | |
| // we can't get rid of. | |
| // TODO(emily): Find a better way to get the skew | |
| } else { | |
| skew = 0; | |
| } | |
| // calculate the amount of space between the body and the accent | |
| var clearance = Math.min(body.height, fontMetrics.metrics.xHeight); | |
| // Build the accent | |
| var accent = buildCommon.makeSymbol( | |
| group.value.accent, "Main-Regular", "math", options.getColor()); | |
| // Remove the italic correction of the accent, because it only serves to | |
| // shift the accent over to a place we don't want. | |
| accent.italic = 0; | |
| // The \vec character that the fonts use is a combining character, and | |
| // thus shows up much too far to the left. To account for this, we add a | |
| // specific class which shifts the accent over to where we want it. | |
| // TODO(emily): Fix this in a better way, like by changing the font | |
| var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null; | |
| var accentBody = makeSpan(["accent-body", vecClass], [ | |
| makeSpan([], [accent])]); | |
| accentBody = buildCommon.makeVList([ | |
| {type: "elem", elem: body}, | |
| {type: "kern", size: -clearance}, | |
| {type: "elem", elem: accentBody}, | |
| ], "firstBaseline", null, options); | |
| // Shift the accent over by the skew. Note we shift by twice the skew | |
| // because we are centering the accent, so by adding 2*skew to the left, | |
| // we shift it to the right by 1*skew. | |
| accentBody.children[1].style.marginLeft = 2 * skew + "em"; | |
| var accentWrap = makeSpan(["mord", "accent"], [accentBody]); | |
| if (supsubGroup) { | |
| // Here, we replace the "base" child of the supsub with our newly | |
| // generated accent. | |
| supsubGroup.children[0] = accentWrap; | |
| // Since we don't rerun the height calculation after replacing the | |
| // accent, we manually recalculate height. | |
| supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height); | |
| // Accents should always be ords, even when their innards are not. | |
| supsubGroup.classes[0] = "mord"; | |
| return supsubGroup; | |
| } else { | |
| return accentWrap; | |
| } | |
| }; | |
| groupTypes.phantom = function(group, options, prev) { | |
| var elements = buildExpression( | |
| group.value.value, | |
| options.withPhantom(), | |
| prev | |
| ); | |
| // \phantom isn't supposed to affect the elements it contains. | |
| // See "color" for more details. | |
| return new buildCommon.makeFragment(elements); | |
| }; | |
| /** | |
| * buildGroup is the function that takes a group and calls the correct groupType | |
| * function for it. It also handles the interaction of size and style changes | |
| * between parents and children. | |
| */ | |
| var buildGroup = function(group, options, prev) { | |
| if (!group) { | |
| return makeSpan(); | |
| } | |
| if (groupTypes[group.type]) { | |
| // Call the groupTypes function | |
| var groupNode = groupTypes[group.type](group, options, prev); | |
| var multiplier; | |
| // If the style changed between the parent and the current group, | |
| // account for the size difference | |
| if (options.style !== options.parentStyle) { | |
| multiplier = options.style.sizeMultiplier / | |
| options.parentStyle.sizeMultiplier; | |
| groupNode.height *= multiplier; | |
| groupNode.depth *= multiplier; | |
| } | |
| // If the size changed between the parent and the current group, account | |
| // for that size difference. | |
| if (options.size !== options.parentSize) { | |
| multiplier = buildCommon.sizingMultiplier[options.size] / | |
| buildCommon.sizingMultiplier[options.parentSize]; | |
| groupNode.height *= multiplier; | |
| groupNode.depth *= multiplier; | |
| } | |
| return groupNode; | |
| } else { | |
| throw new ParseError( | |
| "Got group of unknown type: '" + group.type + "'"); | |
| } | |
| }; | |
| /** | |
| * Take an entire parse tree, and build it into an appropriate set of HTML | |
| * nodes. | |
| */ | |
| var buildHTML = function(tree, options) { | |
| // buildExpression is destructive, so we need to make a clone | |
| // of the incoming tree so that it isn't accidentally changed | |
| tree = JSON.parse(JSON.stringify(tree)); | |
| // Build the expression contained in the tree | |
| var expression = buildExpression(tree, options); | |
| var body = makeSpan(["base", options.style.cls()], expression); | |
| // Add struts, which ensure that the top of the HTML element falls at the | |
| // height of the expression, and the bottom of the HTML element falls at the | |
| // depth of the expression. | |
| var topStrut = makeSpan(["strut"]); | |
| var bottomStrut = makeSpan(["strut", "bottom"]); | |
| topStrut.style.height = body.height + "em"; | |
| bottomStrut.style.height = (body.height + body.depth) + "em"; | |
| // We'd like to use `vertical-align: top` but in IE 9 this lowers the | |
| // baseline of the box to the bottom of this strut (instead staying in the | |
| // normal place) so we use an absolute value for vertical-align instead | |
| bottomStrut.style.verticalAlign = -body.depth + "em"; | |
| // Wrap the struts and body together | |
| var htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]); | |
| htmlNode.setAttribute("aria-hidden", "true"); | |
| return htmlNode; | |
| }; | |
| module.exports = buildHTML; | |