UNPKG

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