forked from KaTeX/KaTeX
-
Notifications
You must be signed in to change notification settings - Fork 0
/
buildMathML.js
235 lines (210 loc) · 7.66 KB
/
buildMathML.js
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
// @flow
/**
* This file converts a parse tree into a cooresponding 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.substr(4, 2) === "tt") ||
(options.font && options.font.substr(4, 2) === "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: 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) {
return null;
}
const mode = group.mode;
if (font === "mathit") {
return "italic";
} else if (font === "boldsymbol") {
return "bold-italic";
}
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,
): MathDomNode[] {
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;
}
}
}
groups.push(group);
lastGroup = group;
}
// TODO(kevinb): combine \\not with mrels and mords
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,
): MathDomNode {
return makeRow(buildExpression(expression, options));
};
/**
* 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,
): MathDomNode {
if (!group) {
return new mathMLTree.MathNode("mrow");
}
if (groupBuilders[group.type]) {
// Call the groupBuilders function
// $FlowFixMe
const result: MathDomNode = groupBuilders[group.type](group, options);
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,
): DomSpan {
const expression = buildExpression(tree, options);
// 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]);
// 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.
// $FlowFixMe
return buildCommon.makeSpan(["katex-mathml"], [math]);
}