environments.js (7753B)
1 /* eslint no-constant-condition:0 */ 2 const parseData = require("./parseData"); 3 const ParseError = require("./ParseError"); 4 const Style = require("./Style"); 5 6 const ParseNode = parseData.ParseNode; 7 8 /** 9 * Parse the body of the environment, with rows delimited by \\ and 10 * columns delimited by &, and create a nested list in row-major order 11 * with one group per cell. If given an optional argument style 12 * ("text", "display", etc.), then each cell is cast into that style. 13 */ 14 function parseArray(parser, result, style) { 15 let row = []; 16 const body = [row]; 17 const rowGaps = []; 18 while (true) { 19 let cell = parser.parseExpression(false, null); 20 cell = new ParseNode("ordgroup", cell, parser.mode); 21 if (style) { 22 cell = new ParseNode("styling", { 23 style: style, 24 value: [cell], 25 }, parser.mode); 26 } 27 row.push(cell); 28 const next = parser.nextToken.text; 29 if (next === "&") { 30 parser.consume(); 31 } else if (next === "\\end") { 32 break; 33 } else if (next === "\\\\" || next === "\\cr") { 34 const cr = parser.parseFunction(); 35 rowGaps.push(cr.value.size); 36 row = []; 37 body.push(row); 38 } else { 39 throw new ParseError("Expected & or \\\\ or \\end", 40 parser.nextToken); 41 } 42 } 43 result.body = body; 44 result.rowGaps = rowGaps; 45 return new ParseNode(result.type, result, parser.mode); 46 } 47 48 /* 49 * An environment definition is very similar to a function definition: 50 * it is declared with a name or a list of names, a set of properties 51 * and a handler containing the actual implementation. 52 * 53 * The properties include: 54 * - numArgs: The number of arguments after the \begin{name} function. 55 * - argTypes: (optional) Just like for a function 56 * - allowedInText: (optional) Whether or not the environment is allowed inside 57 * text mode (default false) (not enforced yet) 58 * - numOptionalArgs: (optional) Just like for a function 59 * A bare number instead of that object indicates the numArgs value. 60 * 61 * The handler function will receive two arguments 62 * - context: information and references provided by the parser 63 * - args: an array of arguments passed to \begin{name} 64 * The context contains the following properties: 65 * - envName: the name of the environment, one of the listed names. 66 * - parser: the parser object 67 * - lexer: the lexer object 68 * - positions: the positions associated with these arguments from args. 69 * The handler must return a ParseResult. 70 */ 71 72 function defineEnvironment(names, props, handler) { 73 if (typeof names === "string") { 74 names = [names]; 75 } 76 if (typeof props === "number") { 77 props = { numArgs: props }; 78 } 79 // Set default values of environments 80 const data = { 81 numArgs: props.numArgs || 0, 82 argTypes: props.argTypes, 83 greediness: 1, 84 allowedInText: !!props.allowedInText, 85 numOptionalArgs: props.numOptionalArgs || 0, 86 handler: handler, 87 }; 88 for (let i = 0; i < names.length; ++i) { 89 module.exports[names[i]] = data; 90 } 91 } 92 93 // Decides on a style for cells in an array according to whether the given 94 // environment name starts with the letter 'd'. 95 function dCellStyle(envName) { 96 if (envName.substr(0, 1) === "d") { 97 return "display"; 98 } else { 99 return "text"; 100 } 101 } 102 103 // Arrays are part of LaTeX, defined in lttab.dtx so its documentation 104 // is part of the source2e.pdf file of LaTeX2e source documentation. 105 // {darray} is an {array} environment where cells are set in \displaystyle, 106 // as defined in nccmath.sty. 107 defineEnvironment(["array", "darray"], { 108 numArgs: 1, 109 }, function(context, args) { 110 let colalign = args[0]; 111 colalign = colalign.value.map ? colalign.value : [colalign]; 112 const cols = colalign.map(function(node) { 113 const ca = node.value; 114 if ("lcr".indexOf(ca) !== -1) { 115 return { 116 type: "align", 117 align: ca, 118 }; 119 } else if (ca === "|") { 120 return { 121 type: "separator", 122 separator: "|", 123 }; 124 } 125 throw new ParseError( 126 "Unknown column alignment: " + node.value, 127 node); 128 }); 129 let res = { 130 type: "array", 131 cols: cols, 132 hskipBeforeAndAfter: true, // \@preamble in lttab.dtx 133 }; 134 res = parseArray(context.parser, res, dCellStyle(context.envName)); 135 return res; 136 }); 137 138 // The matrix environments of amsmath builds on the array environment 139 // of LaTeX, which is discussed above. 140 defineEnvironment([ 141 "matrix", 142 "pmatrix", 143 "bmatrix", 144 "Bmatrix", 145 "vmatrix", 146 "Vmatrix", 147 ], { 148 }, function(context) { 149 const delimiters = { 150 "matrix": null, 151 "pmatrix": ["(", ")"], 152 "bmatrix": ["[", "]"], 153 "Bmatrix": ["\\{", "\\}"], 154 "vmatrix": ["|", "|"], 155 "Vmatrix": ["\\Vert", "\\Vert"], 156 }[context.envName]; 157 let res = { 158 type: "array", 159 hskipBeforeAndAfter: false, // \hskip -\arraycolsep in amsmath 160 }; 161 res = parseArray(context.parser, res, dCellStyle(context.envName)); 162 if (delimiters) { 163 res = new ParseNode("leftright", { 164 body: [res], 165 left: delimiters[0], 166 right: delimiters[1], 167 }, context.mode); 168 } 169 return res; 170 }); 171 172 // A cases environment (in amsmath.sty) is almost equivalent to 173 // \def\arraystretch{1.2}% 174 // \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right. 175 // {dcases} is a {cases} environment where cells are set in \displaystyle, 176 // as defined in mathtools.sty. 177 defineEnvironment([ 178 "cases", 179 "dcases", 180 ], { 181 }, function(context) { 182 let res = { 183 type: "array", 184 arraystretch: 1.2, 185 cols: [{ 186 type: "align", 187 align: "l", 188 pregap: 0, 189 // TODO(kevinb) get the current style. 190 // For now we use the metrics for TEXT style which is what we were 191 // doing before. Before attempting to get the current style we 192 // should look at TeX's behavior especially for \over and matrices. 193 postgap: Style.TEXT.metrics.quad, 194 }, { 195 type: "align", 196 align: "l", 197 pregap: 0, 198 postgap: 0, 199 }], 200 }; 201 res = parseArray(context.parser, res, dCellStyle(context.envName)); 202 res = new ParseNode("leftright", { 203 body: [res], 204 left: "\\{", 205 right: ".", 206 }, context.mode); 207 return res; 208 }); 209 210 // An aligned environment is like the align* environment 211 // except it operates within math mode. 212 // Note that we assume \nomallineskiplimit to be zero, 213 // so that \strut@ is the same as \strut. 214 defineEnvironment("aligned", { 215 }, function(context) { 216 let res = { 217 type: "array", 218 cols: [], 219 }; 220 res = parseArray(context.parser, res); 221 const emptyGroup = new ParseNode("ordgroup", [], context.mode); 222 let numCols = 0; 223 res.value.body.forEach(function(row) { 224 for (let i = 1; i < row.length; i += 2) { 225 row[i].value.unshift(emptyGroup); 226 } 227 if (numCols < row.length) { 228 numCols = row.length; 229 } 230 }); 231 for (let i = 0; i < numCols; ++i) { 232 let align = "r"; 233 let pregap = 0; 234 if (i % 2 === 1) { 235 align = "l"; 236 } else if (i > 0) { 237 pregap = 2; // one \qquad between columns 238 } 239 res.value.cols[i] = { 240 type: "align", 241 align: align, 242 pregap: pregap, 243 postgap: 0, 244 }; 245 } 246 return res; 247 });