UNPKG

11.4 kBJavaScriptView Raw
1// @flow
2import buildCommon from "../buildCommon";
3import defineFunction from "../defineFunction";
4import delimiter from "../delimiter";
5import mathMLTree from "../mathMLTree";
6import ParseError from "../ParseError";
7import utils from "../utils";
8import {assertNodeType, checkSymbolNodeType} from "../parseNode";
9
10import * as html from "../buildHTML";
11import * as mml from "../buildMathML";
12
13import type Options from "../Options";
14import type {AnyParseNode, ParseNode, SymbolParseNode} from "../parseNode";
15import type {FunctionContext} from "../defineFunction";
16
17// Extra data needed for the delimiter handler down below
18const delimiterSizes = {
19 "\\bigl" : {mclass: "mopen", size: 1},
20 "\\Bigl" : {mclass: "mopen", size: 2},
21 "\\biggl": {mclass: "mopen", size: 3},
22 "\\Biggl": {mclass: "mopen", size: 4},
23 "\\bigr" : {mclass: "mclose", size: 1},
24 "\\Bigr" : {mclass: "mclose", size: 2},
25 "\\biggr": {mclass: "mclose", size: 3},
26 "\\Biggr": {mclass: "mclose", size: 4},
27 "\\bigm" : {mclass: "mrel", size: 1},
28 "\\Bigm" : {mclass: "mrel", size: 2},
29 "\\biggm": {mclass: "mrel", size: 3},
30 "\\Biggm": {mclass: "mrel", size: 4},
31 "\\big" : {mclass: "mord", size: 1},
32 "\\Big" : {mclass: "mord", size: 2},
33 "\\bigg" : {mclass: "mord", size: 3},
34 "\\Bigg" : {mclass: "mord", size: 4},
35};
36
37const delimiters = [
38 "(", "\\lparen", ")", "\\rparen",
39 "[", "\\lbrack", "]", "\\rbrack",
40 "\\{", "\\lbrace", "\\}", "\\rbrace",
41 "\\lfloor", "\\rfloor", "\u230a", "\u230b",
42 "\\lceil", "\\rceil", "\u2308", "\u2309",
43 "<", ">", "\\langle", "\u27e8", "\\rangle", "\u27e9", "\\lt", "\\gt",
44 "\\lvert", "\\rvert", "\\lVert", "\\rVert",
45 "\\lgroup", "\\rgroup", "\u27ee", "\u27ef",
46 "\\lmoustache", "\\rmoustache", "\u23b0", "\u23b1",
47 "/", "\\backslash",
48 "|", "\\vert", "\\|", "\\Vert",
49 "\\uparrow", "\\Uparrow",
50 "\\downarrow", "\\Downarrow",
51 "\\updownarrow", "\\Updownarrow",
52 ".",
53];
54
55type IsMiddle = {delim: string, options: Options};
56
57// Delimiter functions
58function checkDelimiter(
59 delim: AnyParseNode,
60 context: FunctionContext,
61): SymbolParseNode {
62 const symDelim = checkSymbolNodeType(delim);
63 if (symDelim && utils.contains(delimiters, symDelim.text)) {
64 return symDelim;
65 } else {
66 throw new ParseError(
67 "Invalid delimiter: '" +
68 (symDelim ? symDelim.text : JSON.stringify(delim)) +
69 "' after '" + context.funcName + "'", delim);
70 }
71}
72
73defineFunction({
74 type: "delimsizing",
75 names: [
76 "\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
77 "\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
78 "\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
79 "\\big", "\\Big", "\\bigg", "\\Bigg",
80 ],
81 props: {
82 numArgs: 1,
83 },
84 handler: (context, args) => {
85 const delim = checkDelimiter(args[0], context);
86
87 return {
88 type: "delimsizing",
89 mode: context.parser.mode,
90 size: delimiterSizes[context.funcName].size,
91 mclass: delimiterSizes[context.funcName].mclass,
92 delim: delim.text,
93 };
94 },
95 htmlBuilder: (group, options) => {
96 if (group.delim === ".") {
97 // Empty delimiters still count as elements, even though they don't
98 // show anything.
99 return buildCommon.makeSpan([group.mclass]);
100 }
101
102 // Use delimiter.sizedDelim to generate the delimiter.
103 return delimiter.sizedDelim(
104 group.delim, group.size, options, group.mode, [group.mclass]);
105 },
106 mathmlBuilder: (group) => {
107 const children = [];
108
109 if (group.delim !== ".") {
110 children.push(mml.makeText(group.delim, group.mode));
111 }
112
113 const node = new mathMLTree.MathNode("mo", children);
114
115 if (group.mclass === "mopen" ||
116 group.mclass === "mclose") {
117 // Only some of the delimsizing functions act as fences, and they
118 // return "mopen" or "mclose" mclass.
119 node.setAttribute("fence", "true");
120 } else {
121 // Explicitly disable fencing if it's not a fence, to override the
122 // defaults.
123 node.setAttribute("fence", "false");
124 }
125
126 return node;
127 },
128});
129
130
131function assertParsed(group: ParseNode<"leftright">) {
132 if (!group.body) {
133 throw new Error("Bug: The leftright ParseNode wasn't fully parsed.");
134 }
135}
136
137
138defineFunction({
139 type: "leftright-right",
140 names: ["\\right"],
141 props: {
142 numArgs: 1,
143 },
144 handler: (context, args) => {
145 // \left case below triggers parsing of \right in
146 // `const right = parser.parseFunction();`
147 // uses this return value.
148 return {
149 type: "leftright-right",
150 mode: context.parser.mode,
151 delim: checkDelimiter(args[0], context).text,
152 };
153 },
154});
155
156
157defineFunction({
158 type: "leftright",
159 names: ["\\left"],
160 props: {
161 numArgs: 1,
162 },
163 handler: (context, args) => {
164 const delim = checkDelimiter(args[0], context);
165
166 const parser = context.parser;
167 // Parse out the implicit body
168 ++parser.leftrightDepth;
169 // parseExpression stops before '\\right'
170 const body = parser.parseExpression(false);
171 --parser.leftrightDepth;
172 // Check the next token
173 parser.expect("\\right", false);
174 const right = assertNodeType(parser.parseFunction(), "leftright-right");
175 return {
176 type: "leftright",
177 mode: parser.mode,
178 body,
179 left: delim.text,
180 right: right.delim,
181 };
182 },
183 htmlBuilder: (group, options) => {
184 assertParsed(group);
185 // Build the inner expression
186 const inner = html.buildExpression(group.body, options, true,
187 ["mopen", "mclose"]);
188
189 let innerHeight = 0;
190 let innerDepth = 0;
191 let hadMiddle = false;
192
193 // Calculate its height and depth
194 for (let i = 0; i < inner.length; i++) {
195 // Property `isMiddle` not defined on `span`. See comment in
196 // "middle"'s htmlBuilder.
197 // $FlowFixMe
198 if (inner[i].isMiddle) {
199 hadMiddle = true;
200 } else {
201 innerHeight = Math.max(inner[i].height, innerHeight);
202 innerDepth = Math.max(inner[i].depth, innerDepth);
203 }
204 }
205
206 // The size of delimiters is the same, regardless of what style we are
207 // in. Thus, to correctly calculate the size of delimiter we need around
208 // a group, we scale down the inner size based on the size.
209 innerHeight *= options.sizeMultiplier;
210 innerDepth *= options.sizeMultiplier;
211
212 let leftDelim;
213 if (group.left === ".") {
214 // Empty delimiters in \left and \right make null delimiter spaces.
215 leftDelim = html.makeNullDelimiter(options, ["mopen"]);
216 } else {
217 // Otherwise, use leftRightDelim to generate the correct sized
218 // delimiter.
219 leftDelim = delimiter.leftRightDelim(
220 group.left, innerHeight, innerDepth, options,
221 group.mode, ["mopen"]);
222 }
223 // Add it to the beginning of the expression
224 inner.unshift(leftDelim);
225
226 // Handle middle delimiters
227 if (hadMiddle) {
228 for (let i = 1; i < inner.length; i++) {
229 const middleDelim = inner[i];
230 // Property `isMiddle` not defined on `span`. See comment in
231 // "middle"'s htmlBuilder.
232 // $FlowFixMe
233 const isMiddle: IsMiddle = middleDelim.isMiddle;
234 if (isMiddle) {
235 // Apply the options that were active when \middle was called
236 inner[i] = delimiter.leftRightDelim(
237 isMiddle.delim, innerHeight, innerDepth,
238 isMiddle.options, group.mode, []);
239 }
240 }
241 }
242
243 let rightDelim;
244 // Same for the right delimiter
245 if (group.right === ".") {
246 rightDelim = html.makeNullDelimiter(options, ["mclose"]);
247 } else {
248 rightDelim = delimiter.leftRightDelim(
249 group.right, innerHeight, innerDepth, options,
250 group.mode, ["mclose"]);
251 }
252 // Add it to the end of the expression.
253 inner.push(rightDelim);
254
255 return buildCommon.makeSpan(["minner"], inner, options);
256 },
257 mathmlBuilder: (group, options) => {
258 assertParsed(group);
259 const inner = mml.buildExpression(group.body, options);
260
261 if (group.left !== ".") {
262 const leftNode = new mathMLTree.MathNode(
263 "mo", [mml.makeText(group.left, group.mode)]);
264
265 leftNode.setAttribute("fence", "true");
266
267 inner.unshift(leftNode);
268 }
269
270 if (group.right !== ".") {
271 const rightNode = new mathMLTree.MathNode(
272 "mo", [mml.makeText(group.right, group.mode)]);
273
274 rightNode.setAttribute("fence", "true");
275
276 inner.push(rightNode);
277 }
278
279 return mml.makeRow(inner);
280 },
281});
282
283defineFunction({
284 type: "middle",
285 names: ["\\middle"],
286 props: {
287 numArgs: 1,
288 },
289 handler: (context, args) => {
290 const delim = checkDelimiter(args[0], context);
291 if (!context.parser.leftrightDepth) {
292 throw new ParseError("\\middle without preceding \\left", delim);
293 }
294
295 return {
296 type: "middle",
297 mode: context.parser.mode,
298 delim: delim.text,
299 };
300 },
301 htmlBuilder: (group, options) => {
302 let middleDelim;
303 if (group.delim === ".") {
304 middleDelim = html.makeNullDelimiter(options, []);
305 } else {
306 middleDelim = delimiter.sizedDelim(
307 group.delim, 1, options,
308 group.mode, []);
309
310 const isMiddle: IsMiddle = {delim: group.delim, options};
311 // Property `isMiddle` not defined on `span`. It is only used in
312 // this file above.
313 // TODO: Fix this violation of the `span` type and possibly rename
314 // things since `isMiddle` sounds like a boolean, but is a struct.
315 // $FlowFixMe
316 middleDelim.isMiddle = isMiddle;
317 }
318 return middleDelim;
319 },
320 mathmlBuilder: (group, options) => {
321 // A Firefox \middle will strech a character vertically only if it
322 // is in the fence part of the operator dictionary at:
323 // https://www.w3.org/TR/MathML3/appendixc.html.
324 // So we need to avoid U+2223 and use plain "|" instead.
325 const textNode = (group.delim === "\\vert" || group.delim === "|")
326 ? mml.makeText("|", "text")
327 : mml.makeText(group.delim, group.mode);
328 const middleNode = new mathMLTree.MathNode("mo", [textNode]);
329 middleNode.setAttribute("fence", "true");
330 // MathML gives 5/18em spacing to each <mo> element.
331 // \middle should get delimiter spacing instead.
332 middleNode.setAttribute("lspace", "0.05em");
333 middleNode.setAttribute("rspace", "0.05em");
334 return middleNode;
335 },
336});