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;
}