File size: 10,171 Bytes
bc20498 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
// @flow
/**
* This file converts a parse tree into a corresponding MathML tree. The main
* entry point is the `buildMathML` function, which takes a parse tree from the
* parser.
*/
import buildCommon from "./buildCommon";
import {getCharacterMetrics} from "./fontMetrics";
import mathMLTree from "./mathMLTree";
import ParseError from "./ParseError";
import symbols, {ligatures} from "./symbols";
import utils from "./utils";
import {_mathmlGroupBuilders as groupBuilders} from "./defineFunction";
import {MathNode, TextNode} from "./mathMLTree";
import type Options from "./Options";
import type {AnyParseNode, SymbolParseNode} from "./parseNode";
import type {DomSpan} from "./domTree";
import type {MathDomNode} from "./mathMLTree";
import type {FontVariant, Mode} from "./types";
/**
* Takes a symbol and converts it into a MathML text node after performing
* optional replacement from symbols.js.
*/
export const makeText = function(
text: string,
mode: Mode,
options?: Options,
): TextNode {
if (symbols[mode][text] && symbols[mode][text].replace &&
text.charCodeAt(0) !== 0xD835 &&
!(ligatures.hasOwnProperty(text) && options &&
((options.fontFamily && options.fontFamily.slice(4, 6) === "tt") ||
(options.font && options.font.slice(4, 6) === "tt")))) {
text = symbols[mode][text].replace;
}
return new mathMLTree.TextNode(text);
};
/**
* Wrap the given array of nodes in an <mrow> node if needed, i.e.,
* unless the array has length 1. Always returns a single node.
*/
export const makeRow = function(body: $ReadOnlyArray<MathDomNode>): MathDomNode {
if (body.length === 1) {
return body[0];
} else {
return new mathMLTree.MathNode("mrow", body);
}
};
/**
* Returns the math variant as a string or null if none is required.
*/
export const getVariant = function(
group: SymbolParseNode,
options: Options,
): ?FontVariant {
// Handle \text... font specifiers as best we can.
// MathML has a limited list of allowable mathvariant specifiers; see
// https://www.w3.org/TR/MathML3/chapter3.html#presm.commatt
if (options.fontFamily === "texttt") {
return "monospace";
} else if (options.fontFamily === "textsf") {
if (options.fontShape === "textit" &&
options.fontWeight === "textbf") {
return "sans-serif-bold-italic";
} else if (options.fontShape === "textit") {
return "sans-serif-italic";
} else if (options.fontWeight === "textbf") {
return "bold-sans-serif";
} else {
return "sans-serif";
}
} else if (options.fontShape === "textit" &&
options.fontWeight === "textbf") {
return "bold-italic";
} else if (options.fontShape === "textit") {
return "italic";
} else if (options.fontWeight === "textbf") {
return "bold";
}
const font = options.font;
if (!font || font === "mathnormal") {
return null;
}
const mode = group.mode;
if (font === "mathit") {
return "italic";
} else if (font === "boldsymbol") {
return group.type === "textord" ? "bold" : "bold-italic";
} else if (font === "mathbf") {
return "bold";
} else if (font === "mathbb") {
return "double-struck";
} else if (font === "mathfrak") {
return "fraktur";
} else if (font === "mathscr" || font === "mathcal") {
// MathML makes no distinction between script and calligraphic
return "script";
} else if (font === "mathsf") {
return "sans-serif";
} else if (font === "mathtt") {
return "monospace";
}
let text = group.text;
if (utils.contains(["\\imath", "\\jmath"], text)) {
return null;
}
if (symbols[mode][text] && symbols[mode][text].replace) {
text = symbols[mode][text].replace;
}
const fontName = buildCommon.fontMap[font].fontName;
if (getCharacterMetrics(text, fontName, mode)) {
return buildCommon.fontMap[font].variant;
}
return null;
};
/**
* Takes a list of nodes, builds them, and returns a list of the generated
* MathML nodes. Also combine consecutive <mtext> outputs into a single
* <mtext> tag.
*/
export const buildExpression = function(
expression: AnyParseNode[],
options: Options,
isOrdgroup?: boolean,
): MathNode[] {
if (expression.length === 1) {
const group = buildGroup(expression[0], options);
if (isOrdgroup && group instanceof MathNode && group.type === "mo") {
// When TeX writers want to suppress spacing on an operator,
// they often put the operator by itself inside braces.
group.setAttribute("lspace", "0em");
group.setAttribute("rspace", "0em");
}
return [group];
}
const groups = [];
let lastGroup;
for (let i = 0; i < expression.length; i++) {
const group = buildGroup(expression[i], options);
if (group instanceof MathNode && lastGroup instanceof MathNode) {
// Concatenate adjacent <mtext>s
if (group.type === 'mtext' && lastGroup.type === 'mtext'
&& group.getAttribute('mathvariant') ===
lastGroup.getAttribute('mathvariant')) {
lastGroup.children.push(...group.children);
continue;
// Concatenate adjacent <mn>s
} else if (group.type === 'mn' && lastGroup.type === 'mn') {
lastGroup.children.push(...group.children);
continue;
// Concatenate <mn>...</mn> followed by <mi>.</mi>
} else if (group.type === 'mi' && group.children.length === 1 &&
lastGroup.type === 'mn') {
const child = group.children[0];
if (child instanceof TextNode && child.text === '.') {
lastGroup.children.push(...group.children);
continue;
}
} else if (lastGroup.type === 'mi' && lastGroup.children.length === 1) {
const lastChild = lastGroup.children[0];
if (lastChild instanceof TextNode && lastChild.text === '\u0338' &&
(group.type === 'mo' || group.type === 'mi' ||
group.type === 'mn')) {
const child = group.children[0];
if (child instanceof TextNode && child.text.length > 0) {
// Overlay with combining character long solidus
child.text = child.text.slice(0, 1) + "\u0338" +
child.text.slice(1);
groups.pop();
}
}
}
}
groups.push(group);
lastGroup = group;
}
return groups;
};
/**
* Equivalent to buildExpression, but wraps the elements in an <mrow>
* if there's more than one. Returns a single node instead of an array.
*/
export const buildExpressionRow = function(
expression: AnyParseNode[],
options: Options,
isOrdgroup?: boolean,
): MathDomNode {
return makeRow(buildExpression(expression, options, isOrdgroup));
};
/**
* Takes a group from the parser and calls the appropriate groupBuilders function
* on it to produce a MathML node.
*/
export const buildGroup = function(
group: ?AnyParseNode,
options: Options,
): MathNode {
if (!group) {
return new mathMLTree.MathNode("mrow");
}
if (groupBuilders[group.type]) {
// Call the groupBuilders function
// $FlowFixMe
const result: MathDomNode = groupBuilders[group.type](group, options);
// $FlowFixMe
return result;
} else {
throw new ParseError(
"Got group of unknown type: '" + group.type + "'");
}
};
/**
* Takes a full parse tree and settings and builds a MathML representation of
* it. In particular, we put the elements from building the parse tree into a
* <semantics> tag so we can also include that TeX source as an annotation.
*
* Note that we actually return a domTree element with a `<math>` inside it so
* we can do appropriate styling.
*/
export default function buildMathML(
tree: AnyParseNode[],
texExpression: string,
options: Options,
isDisplayMode: boolean,
forMathmlOnly: boolean,
): DomSpan {
const expression = buildExpression(tree, options);
// TODO: Make a pass thru the MathML similar to buildHTML.traverseNonSpaceNodes
// and add spacing nodes. This is necessary only adjacent to math operators
// like \sin or \lim or to subsup elements that contain math operators.
// MathML takes care of the other spacing issues.
// Wrap up the expression in an mrow so it is presented in the semantics
// tag correctly, unless it's a single <mrow> or <mtable>.
let wrapper;
if (expression.length === 1 && expression[0] instanceof MathNode &&
utils.contains(["mrow", "mtable"], expression[0].type)) {
wrapper = expression[0];
} else {
wrapper = new mathMLTree.MathNode("mrow", expression);
}
// Build a TeX annotation of the source
const annotation = new mathMLTree.MathNode(
"annotation", [new mathMLTree.TextNode(texExpression)]);
annotation.setAttribute("encoding", "application/x-tex");
const semantics = new mathMLTree.MathNode(
"semantics", [wrapper, annotation]);
const math = new mathMLTree.MathNode("math", [semantics]);
math.setAttribute("xmlns", "http://www.w3.org/1998/Math/MathML");
if (isDisplayMode) {
math.setAttribute("display", "block");
}
// You can't style <math> nodes, so we wrap the node in a span.
// NOTE: The span class is not typed to have <math> nodes as children, and
// we don't want to make the children type more generic since the children
// of span are expected to have more fields in `buildHtml` contexts.
const wrapperClass = forMathmlOnly ? "katex" : "katex-mathml";
// $FlowFixMe
return buildCommon.makeSpan([wrapperClass], [math]);
}
|