|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import ParseError from "./ParseError"; |
|
import Style from "./Style"; |
|
import buildCommon from "./buildCommon"; |
|
import {Span, Anchor} from "./domTree"; |
|
import utils from "./utils"; |
|
import {makeEm} from "./units"; |
|
import {spacings, tightSpacings} from "./spacingData"; |
|
import {_htmlGroupBuilders as groupBuilders} from "./defineFunction"; |
|
import {DocumentFragment} from "./tree"; |
|
|
|
import type Options from "./Options"; |
|
import type {AnyParseNode} from "./parseNode"; |
|
import type {HtmlDomNode, DomSpan} from "./domTree"; |
|
|
|
const makeSpan = buildCommon.makeSpan; |
|
|
|
|
|
|
|
|
|
const binLeftCanceller = ["leftmost", "mbin", "mopen", "mrel", "mop", "mpunct"]; |
|
const binRightCanceller = ["rightmost", "mrel", "mclose", "mpunct"]; |
|
|
|
const styleMap = { |
|
"display": Style.DISPLAY, |
|
"text": Style.TEXT, |
|
"script": Style.SCRIPT, |
|
"scriptscript": Style.SCRIPTSCRIPT, |
|
}; |
|
|
|
type Side = "left" | "right"; |
|
|
|
const DomEnum = { |
|
mord: "mord", |
|
mop: "mop", |
|
mbin: "mbin", |
|
mrel: "mrel", |
|
mopen: "mopen", |
|
mclose: "mclose", |
|
mpunct: "mpunct", |
|
minner: "minner", |
|
}; |
|
type DomType = $Keys<typeof DomEnum>; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const buildExpression = function( |
|
expression: AnyParseNode[], |
|
options: Options, |
|
isRealGroup: boolean | "root", |
|
surrounding: [?DomType, ?DomType] = [null, null], |
|
): HtmlDomNode[] { |
|
|
|
const groups: HtmlDomNode[] = []; |
|
for (let i = 0; i < expression.length; i++) { |
|
const output = buildGroup(expression[i], options); |
|
if (output instanceof DocumentFragment) { |
|
const children: $ReadOnlyArray<HtmlDomNode> = output.children; |
|
groups.push(...children); |
|
} else { |
|
groups.push(output); |
|
} |
|
} |
|
|
|
|
|
buildCommon.tryCombineChars(groups); |
|
|
|
|
|
|
|
if (!isRealGroup) { |
|
return groups; |
|
} |
|
|
|
let glueOptions = options; |
|
if (expression.length === 1) { |
|
const node = expression[0]; |
|
if (node.type === "sizing") { |
|
glueOptions = options.havingSize(node.size); |
|
} else if (node.type === "styling") { |
|
glueOptions = options.havingStyle(styleMap[node.style]); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
const dummyPrev = makeSpan([surrounding[0] || "leftmost"], [], options); |
|
const dummyNext = makeSpan([surrounding[1] || "rightmost"], [], options); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isRoot = (isRealGroup === "root"); |
|
traverseNonSpaceNodes(groups, (node, prev) => { |
|
const prevType = prev.classes[0]; |
|
const type = node.classes[0]; |
|
if (prevType === "mbin" && utils.contains(binRightCanceller, type)) { |
|
prev.classes[0] = "mord"; |
|
} else if (type === "mbin" && utils.contains(binLeftCanceller, prevType)) { |
|
node.classes[0] = "mord"; |
|
} |
|
}, {node: dummyPrev}, dummyNext, isRoot); |
|
|
|
traverseNonSpaceNodes(groups, (node, prev) => { |
|
const prevType = getTypeOfDomTree(prev); |
|
const type = getTypeOfDomTree(node); |
|
|
|
|
|
const space = prevType && type ? (node.hasClass("mtight") |
|
? tightSpacings[prevType][type] |
|
: spacings[prevType][type]) : null; |
|
if (space) { |
|
return buildCommon.makeGlue(space, glueOptions); |
|
} |
|
}, {node: dummyPrev}, dummyNext, isRoot); |
|
|
|
return groups; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const traverseNonSpaceNodes = function( |
|
nodes: HtmlDomNode[], |
|
callback: (HtmlDomNode, HtmlDomNode) => ?HtmlDomNode, |
|
prev: {| |
|
node: HtmlDomNode, |
|
insertAfter?: HtmlDomNode => void, |
|
|}, |
|
next: ?HtmlDomNode, |
|
isRoot: boolean, |
|
) { |
|
if (next) { |
|
nodes.push(next); |
|
} |
|
let i = 0; |
|
for (; i < nodes.length; i++) { |
|
const node = nodes[i]; |
|
const partialGroup = checkPartialGroup(node); |
|
if (partialGroup) { |
|
|
|
traverseNonSpaceNodes(partialGroup.children, |
|
callback, prev, null, isRoot); |
|
continue; |
|
} |
|
|
|
|
|
|
|
const nonspace = !node.hasClass("mspace"); |
|
if (nonspace) { |
|
const result = callback(node, prev.node); |
|
if (result) { |
|
if (prev.insertAfter) { |
|
prev.insertAfter(result); |
|
} else { |
|
nodes.unshift(result); |
|
i++; |
|
} |
|
} |
|
} |
|
|
|
if (nonspace) { |
|
prev.node = node; |
|
} else if (isRoot && node.hasClass("newline")) { |
|
prev.node = makeSpan(["leftmost"]); |
|
} |
|
prev.insertAfter = (index => n => { |
|
nodes.splice(index + 1, 0, n); |
|
i++; |
|
})(i); |
|
} |
|
if (next) { |
|
nodes.pop(); |
|
} |
|
}; |
|
|
|
|
|
const checkPartialGroup = function( |
|
node: HtmlDomNode, |
|
): ?(DocumentFragment<HtmlDomNode> | Anchor | DomSpan) { |
|
if (node instanceof DocumentFragment || node instanceof Anchor |
|
|| (node instanceof Span && node.hasClass("enclosing"))) { |
|
return node; |
|
} |
|
return null; |
|
}; |
|
|
|
|
|
const getOutermostNode = function( |
|
node: HtmlDomNode, |
|
side: Side, |
|
): HtmlDomNode { |
|
const partialGroup = checkPartialGroup(node); |
|
if (partialGroup) { |
|
const children = partialGroup.children; |
|
if (children.length) { |
|
if (side === "right") { |
|
return getOutermostNode(children[children.length - 1], "right"); |
|
} else if (side === "left") { |
|
return getOutermostNode(children[0], "left"); |
|
} |
|
} |
|
} |
|
return node; |
|
}; |
|
|
|
|
|
|
|
export const getTypeOfDomTree = function( |
|
node: ?HtmlDomNode, |
|
side: ?Side, |
|
): ?DomType { |
|
if (!node) { |
|
return null; |
|
} |
|
if (side) { |
|
node = getOutermostNode(node, side); |
|
} |
|
|
|
|
|
return DomEnum[node.classes[0]] || null; |
|
}; |
|
|
|
export const makeNullDelimiter = function( |
|
options: Options, |
|
classes: string[], |
|
): DomSpan { |
|
const moreClasses = ["nulldelimiter"].concat(options.baseSizingClasses()); |
|
return makeSpan(classes.concat(moreClasses)); |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const buildGroup = function( |
|
group: ?AnyParseNode, |
|
options: Options, |
|
baseOptions?: Options, |
|
): HtmlDomNode { |
|
if (!group) { |
|
return makeSpan(); |
|
} |
|
|
|
if (groupBuilders[group.type]) { |
|
|
|
|
|
let groupNode: HtmlDomNode = groupBuilders[group.type](group, options); |
|
|
|
|
|
|
|
if (baseOptions && options.size !== baseOptions.size) { |
|
groupNode = makeSpan(options.sizingClasses(baseOptions), |
|
[groupNode], options); |
|
|
|
const multiplier = |
|
options.sizeMultiplier / baseOptions.sizeMultiplier; |
|
|
|
groupNode.height *= multiplier; |
|
groupNode.depth *= multiplier; |
|
} |
|
|
|
return groupNode; |
|
} else { |
|
throw new ParseError( |
|
"Got group of unknown type: '" + group.type + "'"); |
|
} |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildHTMLUnbreakable(children, options) { |
|
|
|
const body = makeSpan(["base"], children, options); |
|
|
|
|
|
|
|
|
|
const strut = makeSpan(["strut"]); |
|
strut.style.height = makeEm(body.height + body.depth); |
|
if (body.depth) { |
|
strut.style.verticalAlign = makeEm(-body.depth); |
|
} |
|
body.children.unshift(strut); |
|
|
|
return body; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export default function buildHTML(tree: AnyParseNode[], options: Options): DomSpan { |
|
|
|
let tag = null; |
|
if (tree.length === 1 && tree[0].type === "tag") { |
|
tag = tree[0].tag; |
|
tree = tree[0].body; |
|
} |
|
|
|
|
|
const expression = buildExpression(tree, options, "root"); |
|
|
|
let eqnNum; |
|
if (expression.length === 2 && expression[1].hasClass("tag")) { |
|
|
|
eqnNum = expression.pop(); |
|
} |
|
|
|
const children = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let parts = []; |
|
for (let i = 0; i < expression.length; i++) { |
|
parts.push(expression[i]); |
|
if (expression[i].hasClass("mbin") || |
|
expression[i].hasClass("mrel") || |
|
expression[i].hasClass("allowbreak")) { |
|
|
|
|
|
let nobreak = false; |
|
while (i < expression.length - 1 && |
|
expression[i + 1].hasClass("mspace") && |
|
!expression[i + 1].hasClass("newline")) { |
|
i++; |
|
parts.push(expression[i]); |
|
if (expression[i].hasClass("nobreak")) { |
|
nobreak = true; |
|
} |
|
} |
|
|
|
if (!nobreak) { |
|
children.push(buildHTMLUnbreakable(parts, options)); |
|
parts = []; |
|
} |
|
} else if (expression[i].hasClass("newline")) { |
|
|
|
parts.pop(); |
|
if (parts.length > 0) { |
|
children.push(buildHTMLUnbreakable(parts, options)); |
|
parts = []; |
|
} |
|
|
|
children.push(expression[i]); |
|
} |
|
} |
|
if (parts.length > 0) { |
|
children.push(buildHTMLUnbreakable(parts, options)); |
|
} |
|
|
|
|
|
let tagChild; |
|
if (tag) { |
|
tagChild = buildHTMLUnbreakable( |
|
buildExpression(tag, options, true) |
|
); |
|
tagChild.classes = ["tag"]; |
|
children.push(tagChild); |
|
} else if (eqnNum) { |
|
children.push(eqnNum); |
|
} |
|
|
|
const htmlNode = makeSpan(["katex-html"], children); |
|
htmlNode.setAttribute("aria-hidden", "true"); |
|
|
|
|
|
|
|
if (tagChild) { |
|
const strut = tagChild.children[0]; |
|
strut.style.height = makeEm(htmlNode.height + htmlNode.depth); |
|
if (htmlNode.depth) { |
|
strut.style.verticalAlign = makeEm(-htmlNode.depth); |
|
} |
|
} |
|
|
|
return htmlNode; |
|
} |
|
|