diff --git a/.babelrc b/.babelrc index 39bc17f6..08e9719e 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,7 @@ { "presets": ["es2015"], "plugins": [ - "transform-runtime" + "transform-runtime", + "transform-class-properties" ] } diff --git a/.eslintrc b/.eslintrc index 74c9e4d3..8146c443 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,5 @@ { + "parser": "babel-eslint", "rules": { "arrow-spacing": 2, "brace-style": [2, "1tbs", { "allowSingleLine": true }], @@ -14,7 +15,7 @@ "indent": [2, 4, {"SwitchCase": 1}], "keyword-spacing": 2, "linebreak-style": [2, "unix"], - "max-len": [2, 80, 4, { "ignoreUrls": true, "ignorePattern": "\\brequire\\([\"']|eslint-disable" }], + "max-len": [2, 84, 4, { "ignoreUrls": true, "ignorePattern": "\\brequire\\([\"']|eslint-disable" }], "no-alert": 2, "no-array-constructor": 2, "no-console": 2, diff --git a/dockers/Screenshotter/screenshotter.js b/dockers/Screenshotter/screenshotter.js index 045813e5..968a7176 100644 --- a/dockers/Screenshotter/screenshotter.js +++ b/dockers/Screenshotter/screenshotter.js @@ -291,7 +291,7 @@ function findHostIP() { } if (katexIP !== "*any*" || katexURL) { if (!katexURL) { - katexURL = "http://" + katexIP + ":" + katexPort + "/babel/"; + katexURL = "http://" + katexIP + ":" + katexPort + "/"; console.log("KaTeX URL is " + katexURL); } process.nextTick(takeScreenshots); @@ -303,7 +303,7 @@ function findHostIP() { app.get("/ss-connect.js", function(req, res, next) { if (!katexURL) { katexIP = req.query.ip; - katexURL = "http://" + katexIP + ":" + katexPort + "/babel/"; + katexURL = "http://" + katexIP + ":" + katexPort + "/"; console.log("KaTeX URL is " + katexURL); process.nextTick(takeScreenshots); } diff --git a/katex.js b/katex.js index 203598a3..8327592f 100644 --- a/katex.js +++ b/katex.js @@ -7,12 +7,12 @@ * errors in the expression, or errors in javascript handling. */ -const ParseError = require("./src/ParseError"); -const Settings = require("./src/Settings"); +import ParseError from "./src/ParseError"; +import Settings from "./src/Settings"; -const buildTree = require("./src/buildTree"); -const parseTree = require("./src/parseTree"); -const utils = require("./src/utils"); +import buildTree from "./src/buildTree"; +import parseTree from "./src/parseTree"; +import utils from "./src/utils"; /** * Parse and build an expression, and place that expression in the DOM node diff --git a/package.json b/package.json index 6e794ffe..89af860b 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,11 @@ ], "license": "MIT", "devDependencies": { + "babel-eslint": "^7.2.0", + "babel-plugin-transform-class-properties": "^6.23.0", "babel-plugin-transform-runtime": "^6.15.0", "babel-preset-es2015": "^6.18.0", + "babel-register": "^6.24.0", "babelify": "^7.3.0", "browserify": "^13.3.0", "clean-css": "^3.4.23", diff --git a/server.js b/server.js index c7a1ae34..193df9a1 100644 --- a/server.js +++ b/server.js @@ -15,7 +15,7 @@ if (require.main === module) { ":date[iso] :method :url HTTP/:http-version - :status")); } -function serveBrowserified(file, standaloneName, doBabelify) { +function serveBrowserified(file, standaloneName) { return function(req, res, next) { let files; if (Array.isArray(file)) { @@ -26,10 +26,9 @@ function serveBrowserified(file, standaloneName, doBabelify) { files = [path.join(__dirname, file)]; } - const options = {}; - if (doBabelify) { - options.transform = [babelify]; - } + const options = { + transform: [babelify], + }; if (standaloneName) { options.standalone = standaloneName; } @@ -46,30 +45,24 @@ function serveBrowserified(file, standaloneName, doBabelify) { }; } -function twoBrowserified(url, file, standaloneName) { - app.get(url, serveBrowserified(file, standaloneName, false)); - app.get("/babel" + url, serveBrowserified(file, standaloneName, true)); +function browserified(url, file, standaloneName) { + app.get(url, serveBrowserified(file, standaloneName)); } -function twoUse(url, handler) { - app.use(url, handler); - app.use("/babel" + url, handler); +function getStatic(url, file) { + app.use(url, express.static(path.join(__dirname, file))); } -function twoStatic(url, file) { - twoUse(url, express.static(path.join(__dirname, file))); -} - -twoBrowserified("/katex.js", "katex", "katex"); -twoUse("/test/jasmine", express.static(path.dirname( +browserified("/katex.js", "katex", "katex"); +app.use("/test/jasmine", express.static(path.dirname( require.resolve("jasmine-core/lib/jasmine-core/jasmine.js")))); -twoBrowserified("/test/katex-spec.js", "test/*[Ss]pec.js"); -twoBrowserified( +browserified("/test/katex-spec.js", "test/*[Ss]pec.js"); +browserified( "/contrib/auto-render/auto-render.js", "contrib/auto-render/auto-render", "renderMathInElement"); -twoUse("/katex.css", function(req, res, next) { +app.use("/katex.css", function(req, res, next) { const lessfile = path.join(__dirname, "static", "katex.less"); fs.readFile(lessfile, {encoding: "utf8"}, function(err, data) { if (err) { @@ -93,10 +86,10 @@ twoUse("/katex.css", function(req, res, next) { }); }); -twoStatic("", "static"); -twoStatic("", "build"); -twoStatic("/test", "test"); -twoStatic("/contrib", "contrib"); +getStatic("", "static"); +getStatic("", "build"); +getStatic("/test", "test"); +getStatic("/contrib", "contrib"); app.use(function(err, req, res, next) { console.error(err.stack); diff --git a/src/Lexer.js b/src/Lexer.js index bf8f9531..88fc3fdc 100644 --- a/src/Lexer.js +++ b/src/Lexer.js @@ -11,15 +11,8 @@ * kinds. */ -const matchAt = require("match-at"); - -const ParseError = require("./ParseError"); - -// The main lexer class -function Lexer(input) { - this.input = input; - this.pos = 0; -} +import matchAt from "match-at"; +import ParseError from "./ParseError"; /** * The resulting token returned from `lex`. @@ -40,26 +33,28 @@ function Lexer(input) { * @param {number=} end the end offset, zero-based exclusive * @param {Lexer=} lexer the lexer which in turn holds the input string */ -function Token(text, start, end, lexer) { - this.text = text; - this.start = start; - this.end = end; - this.lexer = lexer; -} - -/** - * Given a pair of tokens (this and endToken), compute a “Token” encompassing - * the whole input range enclosed by these two. - * - * @param {Token} endToken last token of the range, inclusive - * @param {string} text the text of the newly constructed token - */ -Token.prototype.range = function(endToken, text) { - if (endToken.lexer !== this.lexer) { - return new Token(text); // sorry, no position information available +class Token { + constructor(text, start, end, lexer) { + this.text = text; + this.start = start; + this.end = end; + this.lexer = lexer; } - return new Token(text, this.start, endToken.end, this.lexer); -}; + + /** + * Given a pair of tokens (this and endToken), compute a “Token” encompassing + * the whole input range enclosed by these two. + * + * @param {Token} endToken last token of the range, inclusive + * @param {string} text the text of the newly constructed token + */ + range(endToken, text) { + if (endToken.lexer !== this.lexer) { + return new Token(text); // sorry, no position information available + } + return new Token(text, this.start, endToken.end, this.lexer); + } +} /* The following tokenRegex * - matches typical whitespace (but not NBSP etc.) using its first group @@ -84,26 +79,36 @@ const tokenRegex = new RegExp( ")" ); -/** - * This function lexes a single token. +/* + * Main Lexer class */ -Lexer.prototype.lex = function() { - const input = this.input; - const pos = this.pos; - if (pos === input.length) { - return new Token("EOF", pos, pos, this); +class Lexer { + constructor(input) { + this.input = input; + this.pos = 0; } - const match = matchAt(tokenRegex, input, pos); - if (match === null) { - throw new ParseError( - "Unexpected character: '" + input[pos] + "'", - new Token(input[pos], pos, pos + 1, this)); + + /** + * This function lexes a single token. + */ + lex() { + const input = this.input; + const pos = this.pos; + if (pos === input.length) { + return new Token("EOF", pos, pos, this); + } + const match = matchAt(tokenRegex, input, pos); + if (match === null) { + throw new ParseError( + "Unexpected character: '" + input[pos] + "'", + new Token(input[pos], pos, pos + 1, this)); + } + const text = match[2] || " "; + const start = this.pos; + this.pos += match[0].length; + const end = this.pos; + return new Token(text, start, end, this); } - const text = match[2] || " "; - const start = this.pos; - this.pos += match[0].length; - const end = this.pos; - return new Token(text, start, end, this); -}; +} module.exports = Lexer; diff --git a/src/MacroExpander.js b/src/MacroExpander.js index 608feafe..38882207 100644 --- a/src/MacroExpander.js +++ b/src/MacroExpander.js @@ -3,144 +3,146 @@ * until only non-macro tokens remain. */ -const Lexer = require("./Lexer"); -const builtinMacros = require("./macros"); -const ParseError = require("./ParseError"); -const objectAssign = require("object-assign"); +import Lexer from "./Lexer"; +import builtinMacros from "./macros"; +import ParseError from "./ParseError"; +import objectAssign from "object-assign"; -function MacroExpander(input, macros) { - this.lexer = new Lexer(input); - this.macros = objectAssign({}, builtinMacros, macros); - this.stack = []; // contains tokens in REVERSE order - this.discardedWhiteSpace = []; -} +class MacroExpander { + constructor(input, macros) { + this.lexer = new Lexer(input); + this.macros = objectAssign({}, builtinMacros, macros); + this.stack = []; // contains tokens in REVERSE order + this.discardedWhiteSpace = []; + } -/** - * Recursively expand first token, then return first non-expandable token. - * - * At the moment, macro expansion doesn't handle delimited macros, - * i.e. things like those defined by \def\foo#1\end{…}. - * See the TeX book page 202ff. for details on how those should behave. - */ -MacroExpander.prototype.nextToken = function() { - for (;;) { - if (this.stack.length === 0) { - this.stack.push(this.lexer.lex()); - } - const topToken = this.stack.pop(); - const name = topToken.text; - if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) { - return topToken; - } - let tok; - let expansion = this.macros[name]; - if (typeof expansion === "string") { - let numArgs = 0; - if (expansion.indexOf("#") !== -1) { - const stripped = expansion.replace(/##/g, ""); - while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { - ++numArgs; + /** + * Recursively expand first token, then return first non-expandable token. + * + * At the moment, macro expansion doesn't handle delimited macros, + * i.e. things like those defined by \def\foo#1\end{…}. + * See the TeX book page 202ff. for details on how those should behave. + */ + nextToken() { + for (;;) { + if (this.stack.length === 0) { + this.stack.push(this.lexer.lex()); + } + const topToken = this.stack.pop(); + const name = topToken.text; + if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) { + return topToken; + } + let tok; + let expansion = this.macros[name]; + if (typeof expansion === "string") { + let numArgs = 0; + if (expansion.indexOf("#") !== -1) { + const stripped = expansion.replace(/##/g, ""); + while (stripped.indexOf("#" + (numArgs + 1)) !== -1) { + ++numArgs; + } } - } - const bodyLexer = new Lexer(expansion); - expansion = []; - tok = bodyLexer.lex(); - while (tok.text !== "EOF") { - expansion.push(tok); + const bodyLexer = new Lexer(expansion); + expansion = []; tok = bodyLexer.lex(); + while (tok.text !== "EOF") { + expansion.push(tok); + tok = bodyLexer.lex(); + } + expansion.reverse(); // to fit in with stack using push and pop + expansion.numArgs = numArgs; + this.macros[name] = expansion; } - expansion.reverse(); // to fit in with stack using push and pop - expansion.numArgs = numArgs; - this.macros[name] = expansion; - } - if (expansion.numArgs) { - const args = []; - let i; - // obtain arguments, either single token or balanced {…} group - for (i = 0; i < expansion.numArgs; ++i) { - const startOfArg = this.get(true); - if (startOfArg.text === "{") { - const arg = []; - let depth = 1; - while (depth !== 0) { - tok = this.get(false); - arg.push(tok); - if (tok.text === "{") { - ++depth; - } else if (tok.text === "}") { - --depth; - } else if (tok.text === "EOF") { + if (expansion.numArgs) { + const args = []; + let i; + // obtain arguments, either single token or balanced {…} group + for (i = 0; i < expansion.numArgs; ++i) { + const startOfArg = this.get(true); + if (startOfArg.text === "{") { + const arg = []; + let depth = 1; + while (depth !== 0) { + tok = this.get(false); + arg.push(tok); + if (tok.text === "{") { + ++depth; + } else if (tok.text === "}") { + --depth; + } else if (tok.text === "EOF") { + throw new ParseError( + "End of input in macro argument", + startOfArg); + } + } + arg.pop(); // remove last } + arg.reverse(); // like above, to fit in with stack order + args[i] = arg; + } else if (startOfArg.text === "EOF") { + throw new ParseError( + "End of input expecting macro argument", topToken); + } else { + args[i] = [startOfArg]; + } + } + // paste arguments in place of the placeholders + expansion = expansion.slice(); // make a shallow copy + for (i = expansion.length - 1; i >= 0; --i) { + tok = expansion[i]; + if (tok.text === "#") { + if (i === 0) { throw new ParseError( - "End of input in macro argument", - startOfArg); + "Incomplete placeholder at end of macro body", + tok); + } + tok = expansion[--i]; // next token on stack + if (tok.text === "#") { // ## → # + expansion.splice(i + 1, 1); // drop first # + } else if (/^[1-9]$/.test(tok.text)) { + // expansion.splice(i, 2, arg[0], arg[1], …) + // to replace placeholder with the indicated argument. + // TODO: use spread once we move to ES2015 + expansion.splice.apply( + expansion, + [i, 2].concat(args[tok.text - 1])); + } else { + throw new ParseError( + "Not a valid argument number", + tok); } } - arg.pop(); // remove last } - arg.reverse(); // like above, to fit in with stack order - args[i] = arg; - } else if (startOfArg.text === "EOF") { - throw new ParseError( - "End of input expecting macro argument", topToken); - } else { - args[i] = [startOfArg]; } } - // paste arguments in place of the placeholders - expansion = expansion.slice(); // make a shallow copy - for (i = expansion.length - 1; i >= 0; --i) { - tok = expansion[i]; - if (tok.text === "#") { - if (i === 0) { - throw new ParseError( - "Incomplete placeholder at end of macro body", - tok); - } - tok = expansion[--i]; // next token on stack - if (tok.text === "#") { // ## → # - expansion.splice(i + 1, 1); // drop first # - } else if (/^[1-9]$/.test(tok.text)) { - // expansion.splice(i, 2, arg[0], arg[1], …) - // to replace placeholder with the indicated argument. - // TODO: use spread once we move to ES2015 - expansion.splice.apply( - expansion, - [i, 2].concat(args[tok.text - 1])); - } else { - throw new ParseError( - "Not a valid argument number", - tok); - } - } + this.stack = this.stack.concat(expansion); + } + } + + get(ignoreSpace) { + this.discardedWhiteSpace = []; + let token = this.nextToken(); + if (ignoreSpace) { + while (token.text === " ") { + this.discardedWhiteSpace.push(token); + token = this.nextToken(); } } - this.stack = this.stack.concat(expansion); + return token; } -}; -MacroExpander.prototype.get = function(ignoreSpace) { - this.discardedWhiteSpace = []; - let token = this.nextToken(); - if (ignoreSpace) { - while (token.text === " ") { - this.discardedWhiteSpace.push(token); - token = this.nextToken(); + /** + * Undo the effect of the preceding call to the get method. + * A call to this method MUST be immediately preceded and immediately followed + * by a call to get. Only used during mode switching, i.e. after one token + * was got in the old mode but should get got again in a new mode + * with possibly different whitespace handling. + */ + unget(token) { + this.stack.push(token); + while (this.discardedWhiteSpace.length !== 0) { + this.stack.push(this.discardedWhiteSpace.pop()); } } - return token; -}; - -/** - * Undo the effect of the preceding call to the get method. - * A call to this method MUST be immediately preceded and immediately followed - * by a call to get. Only used during mode switching, i.e. after one token - * was got in the old mode but should get got again in a new mode - * with possibly different whitespace handling. - */ -MacroExpander.prototype.unget = function(token) { - this.stack.push(token); - while (this.discardedWhiteSpace.length !== 0) { - this.stack.push(this.discardedWhiteSpace.pop()); - } -}; +} module.exports = MacroExpander; diff --git a/src/Options.js b/src/Options.js index a3bc7172..55cdfd23 100644 --- a/src/Options.js +++ b/src/Options.js @@ -5,7 +5,7 @@ * `.reset` functions. */ -const fontMetrics = require("./fontMetrics"); +import fontMetrics from "./fontMetrics"; const BASESIZE = 6; @@ -31,6 +31,10 @@ const sizeMultipliers = [ 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488, ]; +const sizeAtStyle = function(size, style) { + return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1]; +}; + /** * This is the main options class. It contains the current style, size, color, * and font. @@ -38,238 +42,236 @@ const sizeMultipliers = [ * Options objects should not be modified. To create a new Options with * different properties, call a `.having*` method. */ -function Options(data) { - this.style = data.style; - this.color = data.color; - this.size = data.size || BASESIZE; - this.textSize = data.textSize || this.size; - this.phantom = data.phantom; - this.font = data.font; - this.sizeMultiplier = sizeMultipliers[this.size - 1]; - this._fontMetrics = null; -} +class Options { + constructor(data) { + this.style = data.style; + this.color = data.color; + this.size = data.size || BASESIZE; + this.textSize = data.textSize || this.size; + this.phantom = data.phantom; + this.font = data.font; + this.sizeMultiplier = sizeMultipliers[this.size - 1]; + this._fontMetrics = null; + } -/** - * Returns a new options object with the same properties as "this". Properties - * from "extension" will be copied to the new options object. - */ -Options.prototype.extend = function(extension) { - const data = { - style: this.style, - size: this.size, - textSize: this.textSize, - color: this.color, - phantom: this.phantom, - font: this.font, - }; + /** + * Returns a new options object with the same properties as "this". Properties + * from "extension" will be copied to the new options object. + */ + extend(extension) { + const data = { + style: this.style, + size: this.size, + textSize: this.textSize, + color: this.color, + phantom: this.phantom, + font: this.font, + }; - for (const key in extension) { - if (extension.hasOwnProperty(key)) { - data[key] = extension[key]; + for (const key in extension) { + if (extension.hasOwnProperty(key)) { + data[key] = extension[key]; + } + } + + return new Options(data); + } + + /** + * Return an options object with the given style. If `this.style === style`, + * returns `this`. + */ + havingStyle(style) { + if (this.style === style) { + return this; + } else { + return this.extend({ + style: style, + size: sizeAtStyle(this.textSize, style), + }); } } - return new Options(data); -}; + /** + * Return an options object with a cramped version of the current style. If + * the current style is cramped, returns `this`. + */ + havingCrampedStyle() { + return this.havingStyle(this.style.cramp()); + } -function sizeAtStyle(size, style) { - return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1]; + /** + * Return an options object with the given size and in at least `\textstyle`. + * Returns `this` if appropriate. + */ + havingSize(size) { + if (this.size === size && this.textSize === size) { + return this; + } else { + return this.extend({ + style: this.style.text(), + size: size, + textSize: size, + }); + } + } + + /** + * Like `this.havingSize(BASESIZE).havingStyle(style)`. If `style` is omitted, + * changes to at least `\textstyle`. + */ + havingBaseStyle(style) { + style = style || this.style.text(); + const wantSize = sizeAtStyle(BASESIZE, style); + if (this.size === wantSize && this.textSize === BASESIZE + && this.style === style) { + return this; + } else { + return this.extend({ + style: style, + size: wantSize, + baseSize: BASESIZE, + }); + } + } + + /** + * Create a new options object with the given color. + */ + withColor(color) { + return this.extend({ + color: color, + }); + } + + /** + * Create a new options object with "phantom" set to true. + */ + withPhantom() { + return this.extend({ + phantom: true, + }); + } + + /** + * Create a new options objects with the give font. + */ + withFont(font) { + return this.extend({ + font: font || this.font, + }); + } + + /** + * Return the CSS sizing classes required to switch from enclosing options + * `oldOptions` to `this`. Returns an array of classes. + */ + sizingClasses(oldOptions) { + if (oldOptions.size !== this.size) { + return ["sizing", "reset-size" + oldOptions.size, "size" + this.size]; + } else { + return []; + } + } + + /** + * Return the CSS sizing classes required to switch to the base size. Like + * `this.havingSize(BASESIZE).sizingClasses(this)`. + */ + baseSizingClasses() { + if (this.size !== BASESIZE) { + return ["sizing", "reset-size" + this.size, "size" + BASESIZE]; + } else { + return []; + } + } + + /** + * Return the font metrics for this size. + */ + fontMetrics() { + if (!this._fontMetrics) { + this._fontMetrics = fontMetrics.getFontMetrics(this.size); + } + return this._fontMetrics; + } + + /** + * A map of color names to CSS colors. + * TODO(emily): Remove this when we have real macros + */ + static colorMap = { + "katex-blue": "#6495ed", + "katex-orange": "#ffa500", + "katex-pink": "#ff00af", + "katex-red": "#df0030", + "katex-green": "#28ae7b", + "katex-gray": "gray", + "katex-purple": "#9d38bd", + "katex-blueA": "#ccfaff", + "katex-blueB": "#80f6ff", + "katex-blueC": "#63d9ea", + "katex-blueD": "#11accd", + "katex-blueE": "#0c7f99", + "katex-tealA": "#94fff5", + "katex-tealB": "#26edd5", + "katex-tealC": "#01d1c1", + "katex-tealD": "#01a995", + "katex-tealE": "#208170", + "katex-greenA": "#b6ffb0", + "katex-greenB": "#8af281", + "katex-greenC": "#74cf70", + "katex-greenD": "#1fab54", + "katex-greenE": "#0d923f", + "katex-goldA": "#ffd0a9", + "katex-goldB": "#ffbb71", + "katex-goldC": "#ff9c39", + "katex-goldD": "#e07d10", + "katex-goldE": "#a75a05", + "katex-redA": "#fca9a9", + "katex-redB": "#ff8482", + "katex-redC": "#f9685d", + "katex-redD": "#e84d39", + "katex-redE": "#bc2612", + "katex-maroonA": "#ffbde0", + "katex-maroonB": "#ff92c6", + "katex-maroonC": "#ed5fa6", + "katex-maroonD": "#ca337c", + "katex-maroonE": "#9e034e", + "katex-purpleA": "#ddd7ff", + "katex-purpleB": "#c6b9fc", + "katex-purpleC": "#aa87ff", + "katex-purpleD": "#7854ab", + "katex-purpleE": "#543b78", + "katex-mintA": "#f5f9e8", + "katex-mintB": "#edf2df", + "katex-mintC": "#e0e5cc", + "katex-grayA": "#f6f7f7", + "katex-grayB": "#f0f1f2", + "katex-grayC": "#e3e5e6", + "katex-grayD": "#d6d8da", + "katex-grayE": "#babec2", + "katex-grayF": "#888d93", + "katex-grayG": "#626569", + "katex-grayH": "#3b3e40", + "katex-grayI": "#21242c", + "katex-kaBlue": "#314453", + "katex-kaGreen": "#71B307", + }; + + /** + * Gets the CSS color of the current options object, accounting for the + * `colorMap`. + */ + getColor() { + if (this.phantom) { + return "transparent"; + } else { + return Options.colorMap[this.color] || this.color; + } + } } -/** - * Return an options object with the given style. If `this.style === style`, - * returns `this`. - */ -Options.prototype.havingStyle = function(style) { - if (this.style === style) { - return this; - } else { - return this.extend({ - style: style, - size: sizeAtStyle(this.textSize, style), - }); - } -}; - -/** - * Return an options object with a cramped version of the current style. If - * the current style is cramped, returns `this`. - */ -Options.prototype.havingCrampedStyle = function() { - return this.havingStyle(this.style.cramp()); -}; - -/** - * Return an options object with the given size and in at least `\textstyle`. - * Returns `this` if appropriate. - */ -Options.prototype.havingSize = function(size) { - if (this.size === size && this.textSize === size) { - return this; - } else { - return this.extend({ - style: this.style.text(), - size: size, - textSize: size, - }); - } -}; - -/** - * Like `this.havingSize(BASESIZE).havingStyle(style)`. If `style` is omitted, - * changes to at least `\textstyle`. - */ -Options.prototype.havingBaseStyle = function(style) { - style = style || this.style.text(); - const wantSize = sizeAtStyle(BASESIZE, style); - if (this.size === wantSize && this.textSize === BASESIZE - && this.style === style) { - return this; - } else { - return this.extend({ - style: style, - size: wantSize, - baseSize: BASESIZE, - }); - } -}; - -/** - * Create a new options object with the given color. - */ -Options.prototype.withColor = function(color) { - return this.extend({ - color: color, - }); -}; - -/** - * Create a new options object with "phantom" set to true. - */ -Options.prototype.withPhantom = function() { - return this.extend({ - phantom: true, - }); -}; - -/** - * Create a new options objects with the give font. - */ -Options.prototype.withFont = function(font) { - return this.extend({ - font: font || this.font, - }); -}; - -/** - * Return the CSS sizing classes required to switch from enclosing options - * `oldOptions` to `this`. Returns an array of classes. - */ -Options.prototype.sizingClasses = function(oldOptions) { - if (oldOptions.size !== this.size) { - return ["sizing", "reset-size" + oldOptions.size, "size" + this.size]; - } else { - return []; - } -}; - -/** - * Return the CSS sizing classes required to switch to the base size. Like - * `this.havingSize(BASESIZE).sizingClasses(this)`. - */ -Options.prototype.baseSizingClasses = function() { - if (this.size !== BASESIZE) { - return ["sizing", "reset-size" + this.size, "size" + BASESIZE]; - } else { - return []; - } -}; - -/** - * Return the font metrics for this size. - */ -Options.prototype.fontMetrics = function() { - if (!this._fontMetrics) { - this._fontMetrics = fontMetrics.getFontMetrics(this.size); - } - return this._fontMetrics; -}; - -/** - * A map of color names to CSS colors. - * TODO(emily): Remove this when we have real macros - */ -const colorMap = { - "katex-blue": "#6495ed", - "katex-orange": "#ffa500", - "katex-pink": "#ff00af", - "katex-red": "#df0030", - "katex-green": "#28ae7b", - "katex-gray": "gray", - "katex-purple": "#9d38bd", - "katex-blueA": "#ccfaff", - "katex-blueB": "#80f6ff", - "katex-blueC": "#63d9ea", - "katex-blueD": "#11accd", - "katex-blueE": "#0c7f99", - "katex-tealA": "#94fff5", - "katex-tealB": "#26edd5", - "katex-tealC": "#01d1c1", - "katex-tealD": "#01a995", - "katex-tealE": "#208170", - "katex-greenA": "#b6ffb0", - "katex-greenB": "#8af281", - "katex-greenC": "#74cf70", - "katex-greenD": "#1fab54", - "katex-greenE": "#0d923f", - "katex-goldA": "#ffd0a9", - "katex-goldB": "#ffbb71", - "katex-goldC": "#ff9c39", - "katex-goldD": "#e07d10", - "katex-goldE": "#a75a05", - "katex-redA": "#fca9a9", - "katex-redB": "#ff8482", - "katex-redC": "#f9685d", - "katex-redD": "#e84d39", - "katex-redE": "#bc2612", - "katex-maroonA": "#ffbde0", - "katex-maroonB": "#ff92c6", - "katex-maroonC": "#ed5fa6", - "katex-maroonD": "#ca337c", - "katex-maroonE": "#9e034e", - "katex-purpleA": "#ddd7ff", - "katex-purpleB": "#c6b9fc", - "katex-purpleC": "#aa87ff", - "katex-purpleD": "#7854ab", - "katex-purpleE": "#543b78", - "katex-mintA": "#f5f9e8", - "katex-mintB": "#edf2df", - "katex-mintC": "#e0e5cc", - "katex-grayA": "#f6f7f7", - "katex-grayB": "#f0f1f2", - "katex-grayC": "#e3e5e6", - "katex-grayD": "#d6d8da", - "katex-grayE": "#babec2", - "katex-grayF": "#888d93", - "katex-grayG": "#626569", - "katex-grayH": "#3b3e40", - "katex-grayI": "#21242c", - "katex-kaBlue": "#314453", - "katex-kaGreen": "#71B307", -}; - -/** - * Gets the CSS color of the current options object, accounting for the - * `colorMap`. - */ -Options.prototype.getColor = function() { - if (this.phantom) { - return "transparent"; - } else { - return colorMap[this.color] || this.color; - } -}; - /** * The base size index. */ diff --git a/src/ParseError.js b/src/ParseError.js index 161f5ece..862ed8b8 100644 --- a/src/ParseError.js +++ b/src/ParseError.js @@ -9,53 +9,55 @@ * @param {string} message The error message * @param {(Token|ParseNode)=} token An object providing position information */ -function ParseError(message, token) { - let error = "KaTeX parse error: " + message; - let start; - let end; +class ParseError { + constructor(message, token) { + let error = "KaTeX parse error: " + message; + let start; + let end; - if (token && token.lexer && token.start <= token.end) { - // If we have the input and a position, make the error a bit fancier + if (token && token.lexer && token.start <= token.end) { + // If we have the input and a position, make the error a bit fancier - // Get the input - const input = token.lexer.input; + // Get the input + const input = token.lexer.input; - // Prepend some information - start = token.start; - end = token.end; - if (start === input.length) { - error += " at end of input: "; - } else { - error += " at position " + (start + 1) + ": "; + // Prepend some information + start = token.start; + end = token.end; + if (start === input.length) { + error += " at end of input: "; + } else { + error += " at position " + (start + 1) + ": "; + } + + // Underline token in question using combining underscores + const underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); + + // Extract some context from the input and add it to the error + let left; + if (start > 15) { + left = "…" + input.slice(start - 15, start); + } else { + left = input.slice(0, start); + } + let right; + if (end + 15 < input.length) { + right = input.slice(end, end + 15) + "…"; + } else { + right = input.slice(end); + } + error += left + underlined + right; } - // Underline token in question using combining underscores - const underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); + // Some hackery to make ParseError a prototype of Error + // See http://stackoverflow.com/a/8460753 + const self = new Error(error); + self.name = "ParseError"; + self.__proto__ = ParseError.prototype; - // Extract some context from the input and add it to the error - let left; - if (start > 15) { - left = "…" + input.slice(start - 15, start); - } else { - left = input.slice(0, start); - } - let right; - if (end + 15 < input.length) { - right = input.slice(end, end + 15) + "…"; - } else { - right = input.slice(end); - } - error += left + underlined + right; + self.position = start; + return self; } - - // Some hackery to make ParseError a prototype of Error - // See http://stackoverflow.com/a/8460753 - const self = new Error(error); - self.name = "ParseError"; - self.__proto__ = ParseError.prototype; - - self.position = start; - return self; } // More hackery diff --git a/src/Parser.js b/src/Parser.js index 46e1dbb4..07d3cf41 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -1,13 +1,12 @@ /* eslint no-constant-condition:0 */ -const functions = require("./functions"); -const environments = require("./environments"); -const MacroExpander = require("./MacroExpander"); -const symbols = require("./symbols"); -const utils = require("./utils"); -const cjkRegex = require("./unicodeRegexes").cjkRegex; - -const parseData = require("./parseData"); -const ParseError = require("./ParseError"); +import functions from "./functions"; +import environments from "./environments"; +import MacroExpander from "./MacroExpander"; +import symbols from "./symbols"; +import utils from "./utils"; +import { cjkRegex } from "./unicodeRegexes"; +import { ParseNode } from "./parseData"; +import ParseError from "./ParseError"; /** * This file contains the parser used to parse out a TeX expression from the @@ -43,26 +42,6 @@ const ParseError = require("./ParseError"); * standalone object which can be used as an argument to another function. */ -/** - * Main Parser class - */ -function Parser(input, settings) { - // Create a new macro expander (gullet) and (indirectly via that) also a - // new lexer (mouth) for this parser (stomach, in the language of TeX) - this.gullet = new MacroExpander(input, settings.macros); - // Use old \color behavior (same as LaTeX's \textcolor) if requested. - // We do this after the macros object has been copied by MacroExpander. - if (settings.colorIsTextColor) { - this.gullet.macros["\\color"] = "\\textcolor"; - } - // Store the settings for use in parsing - this.settings = settings; - // Count leftright depth (for \middle errors) - this.leftrightDepth = 0; -} - -const ParseNode = parseData.ParseNode; - /** * An initial function (without its arguments), or an argument to a function. * The `result` argument should be a ParseNode. @@ -74,849 +53,865 @@ function ParseFuncOrArgument(result, isFunction, token) { this.token = token; } -/** - * Checks a result to make sure it has the right type, and throws an - * appropriate error otherwise. - * - * @param {boolean=} consume whether to consume the expected token, - * defaults to true - */ -Parser.prototype.expect = function(text, consume) { - if (this.nextToken.text !== text) { - throw new ParseError( - "Expected '" + text + "', got '" + this.nextToken.text + "'", - this.nextToken - ); - } - if (consume !== false) { - this.consume(); - } -}; - -/** - * Considers the current look ahead token as consumed, - * and fetches the one after that as the new look ahead. - */ -Parser.prototype.consume = function() { - this.nextToken = this.gullet.get(this.mode === "math"); -}; - -Parser.prototype.switchMode = function(newMode) { - this.gullet.unget(this.nextToken); - this.mode = newMode; - this.consume(); -}; - -/** - * Main parsing function, which parses an entire input. - * - * @return {?Array.} - */ -Parser.prototype.parse = function() { - // Try to parse the input - this.mode = "math"; - this.consume(); - const parse = this.parseInput(); - return parse; -}; - -/** - * Parses an entire input tree. - */ -Parser.prototype.parseInput = function() { - // Parse an expression - const expression = this.parseExpression(false); - // If we succeeded, make sure there's an EOF at the end - this.expect("EOF", false); - return expression; -}; - -const endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"]; - -/** - * Parses an "expression", which is a list of atoms. - * - * @param {boolean} breakOnInfix Should the parsing stop when we hit infix - * nodes? This happens when functions have higher precendence - * than infix nodes in implicit parses. - * - * @param {?string} breakOnTokenText The text of the token that the expression - * should end with, or `null` if something else should end the - * expression. - * - * @return {ParseNode} - */ -Parser.prototype.parseExpression = function(breakOnInfix, breakOnTokenText) { - const body = []; - // Keep adding atoms to the body until we can't parse any more atoms (either - // we reached the end, a }, or a \right) - while (true) { - const lex = this.nextToken; - if (endOfExpression.indexOf(lex.text) !== -1) { - break; - } - if (breakOnTokenText && lex.text === breakOnTokenText) { - break; - } - if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) { - break; - } - const atom = this.parseAtom(); - if (!atom) { - if (!this.settings.throwOnError && lex.text[0] === "\\") { - const errorNode = this.handleUnsupportedCmd(); - body.push(errorNode); - continue; - } - - break; - } - body.push(atom); - } - return this.handleInfixNodes(body); -}; - -/** - * Rewrites infix operators such as \over with corresponding commands such - * as \frac. - * - * There can only be one infix operator per group. If there's more than one - * then the expression is ambiguous. This can be resolved by adding {}. - * - * @returns {Array} - */ -Parser.prototype.handleInfixNodes = function(body) { - let overIndex = -1; - let funcName; - - for (let i = 0; i < body.length; i++) { - const node = body[i]; - if (node.type === "infix") { - if (overIndex !== -1) { - throw new ParseError( - "only one infix operator per group", - node.value.token); - } - overIndex = i; - funcName = node.value.replaceWith; +class Parser { + constructor(input, settings) { + // Create a new macro expander (gullet) and (indirectly via that) also a + // new lexer (mouth) for this parser (stomach, in the language of TeX) + this.gullet = new MacroExpander(input, settings.macros); + // Use old \color behavior (same as LaTeX's \textcolor) if requested. + // We do this after the macros object has been copied by MacroExpander. + if (settings.colorIsTextColor) { + this.gullet.macros["\\color"] = "\\textcolor"; } + // Store the settings for use in parsing + this.settings = settings; + // Count leftright depth (for \middle errors) + this.leftrightDepth = 0; } - if (overIndex !== -1) { - let numerNode; - let denomNode; - - const numerBody = body.slice(0, overIndex); - const denomBody = body.slice(overIndex + 1); - - if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { - numerNode = numerBody[0]; - } else { - numerNode = new ParseNode("ordgroup", numerBody, this.mode); - } - - if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { - denomNode = denomBody[0]; - } else { - denomNode = new ParseNode("ordgroup", denomBody, this.mode); - } - - const value = this.callFunction( - funcName, [numerNode, denomNode], null); - return [new ParseNode(value.type, value, this.mode)]; - } else { - return body; - } -}; - -// The greediness of a superscript or subscript -const SUPSUB_GREEDINESS = 1; - -/** - * Handle a subscript or superscript with nice errors. - */ -Parser.prototype.handleSupSubscript = function(name) { - const symbolToken = this.nextToken; - const symbol = symbolToken.text; - this.consume(); - const group = this.parseGroup(); - - if (!group) { - if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") { - return this.handleUnsupportedCmd(); - } else { + /** + * Checks a result to make sure it has the right type, and throws an + * appropriate error otherwise. + * + * @param {boolean=} consume whether to consume the expected token, + * defaults to true + */ + expect(text, consume) { + if (this.nextToken.text !== text) { throw new ParseError( - "Expected group after '" + symbol + "'", - symbolToken + "Expected '" + text + "', got '" + this.nextToken.text + "'", + this.nextToken ); } - } else if (group.isFunction) { - // ^ and _ have a greediness, so handle interactions with functions' - // greediness - const funcGreediness = functions[group.result].greediness; - if (funcGreediness > SUPSUB_GREEDINESS) { - return this.parseFunction(group); - } else { - throw new ParseError( - "Got function '" + group.result + "' with no arguments " + - "as " + name, symbolToken); - } - } else { - return group.result; - } -}; - -/** - * Converts the textual input of an unsupported command into a text node - * contained within a color node whose color is determined by errorColor - */ -Parser.prototype.handleUnsupportedCmd = function() { - const text = this.nextToken.text; - const textordArray = []; - - for (let i = 0; i < text.length; i++) { - textordArray.push(new ParseNode("textord", text[i], "text")); - } - - const textNode = new ParseNode( - "text", - { - body: textordArray, - type: "text", - }, - this.mode); - - const colorNode = new ParseNode( - "color", - { - color: this.settings.errorColor, - value: [textNode], - type: "color", - }, - this.mode); - - this.consume(); - return colorNode; -}; - -/** - * Parses a group with optional super/subscripts. - * - * @return {?ParseNode} - */ -Parser.prototype.parseAtom = function() { - // The body of an atom is an implicit group, so that things like - // \left(x\right)^2 work correctly. - const base = this.parseImplicitGroup(); - - // In text mode, we don't have superscripts or subscripts - if (this.mode === "text") { - return base; - } - - // Note that base may be empty (i.e. null) at this point. - - let superscript; - let subscript; - while (true) { - // Lex the first token - const lex = this.nextToken; - - if (lex.text === "\\limits" || lex.text === "\\nolimits") { - // We got a limit control - if (!base || base.type !== "op") { - throw new ParseError( - "Limit controls must follow a math operator", - lex); - } else { - const limits = lex.text === "\\limits"; - base.value.limits = limits; - base.value.alwaysHandleSupSub = true; - } + if (consume !== false) { this.consume(); - } else if (lex.text === "^") { - // We got a superscript start - if (superscript) { - throw new ParseError("Double superscript", lex); - } - superscript = this.handleSupSubscript("superscript"); - } else if (lex.text === "_") { - // We got a subscript start - if (subscript) { - throw new ParseError("Double subscript", lex); - } - subscript = this.handleSupSubscript("subscript"); - } else if (lex.text === "'") { - // We got a prime - if (superscript) { - throw new ParseError("Double superscript", lex); - } - const prime = new ParseNode("textord", "\\prime", this.mode); - - // Many primes can be grouped together, so we handle this here - const primes = [prime]; - this.consume(); - // Keep lexing tokens until we get something that's not a prime - while (this.nextToken.text === "'") { - // For each one, add another prime to the list - primes.push(prime); - this.consume(); - } - // If there's a superscript following the primes, combine that - // superscript in with the primes. - if (this.nextToken.text === "^") { - primes.push(this.handleSupSubscript("superscript")); - } - // Put everything into an ordgroup as the superscript - superscript = new ParseNode("ordgroup", primes, this.mode); - } else { - // If it wasn't ^, _, or ', stop parsing super/subscripts - break; } } - if (superscript || subscript) { - // If we got either a superscript or subscript, create a supsub - return new ParseNode("supsub", { - base: base, - sup: superscript, - sub: subscript, - }, this.mode); - } else { - // Otherwise return the original body - return base; - } -}; - -// A list of the size-changing functions, for use in parseImplicitGroup -const sizeFuncs = [ - "\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small", - "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", -]; - -// A list of the style-changing functions, for use in parseImplicitGroup -const styleFuncs = [ - "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle", -]; - -// Old font functions -const oldFontFuncs = { - "\\rm": "mathrm", - "\\sf": "mathsf", - "\\tt": "mathtt", - "\\bf": "mathbf", - "\\it": "mathit", - //"\\sl": "textsl", - //"\\sc": "textsc", -}; - -/** - * Parses an implicit group, which is a group that starts at the end of a - * specified, and ends right before a higher explicit group ends, or at EOL. It - * is used for functions that appear to affect the current style, like \Large or - * \textrm, where instead of keeping a style we just pretend that there is an - * implicit grouping after it until the end of the group. E.g. - * small text {\Large large text} small text again - * It is also used for \left and \right to get the correct grouping. - * - * @return {?ParseNode} - */ -Parser.prototype.parseImplicitGroup = function() { - const start = this.parseSymbol(); - - if (start == null) { - // If we didn't get anything we handle, fall back to parseFunction - return this.parseFunction(); + /** + * Considers the current look ahead token as consumed, + * and fetches the one after that as the new look ahead. + */ + consume() { + this.nextToken = this.gullet.get(this.mode === "math"); } - const func = start.result; - - if (func === "\\left") { - // If we see a left: - // Parse the entire left function (including the delimiter) - const left = this.parseFunction(start); - // Parse out the implicit body - ++this.leftrightDepth; - const body = this.parseExpression(false); - --this.leftrightDepth; - // Check the next token - this.expect("\\right", false); - const right = this.parseFunction(); - return new ParseNode("leftright", { - body: body, - left: left.value.value, - right: right.value.value, - }, this.mode); - } else if (func === "\\begin") { - // begin...end is similar to left...right - const begin = this.parseFunction(start); - const envName = begin.value.name; - if (!environments.hasOwnProperty(envName)) { - throw new ParseError( - "No such environment: " + envName, begin.value.nameGroup); - } - // Build the environment object. Arguments and other information will - // be made available to the begin and end methods using properties. - const env = environments[envName]; - const args = this.parseArguments("\\begin{" + envName + "}", env); - const context = { - mode: this.mode, - envName: envName, - parser: this, - positions: args.pop(), - }; - const result = env.handler(context, args); - this.expect("\\end", false); - const endNameToken = this.nextToken; - const end = this.parseFunction(); - if (end.value.name !== envName) { - throw new ParseError( - "Mismatch: \\begin{" + envName + "} matched " + - "by \\end{" + end.value.name + "}", - endNameToken); - } - result.position = end.position; - return result; - } else if (utils.contains(sizeFuncs, func)) { - // If we see a sizing function, parse out the implicit body - this.consumeSpaces(); - const body = this.parseExpression(false); - return new ParseNode("sizing", { - // Figure out what size to use based on the list of functions above - size: utils.indexOf(sizeFuncs, func) + 1, - value: body, - }, this.mode); - } else if (utils.contains(styleFuncs, func)) { - // If we see a styling function, parse out the implicit body - this.consumeSpaces(); - const body = this.parseExpression(true); - return new ParseNode("styling", { - // Figure out what style to use by pulling out the style from - // the function name - style: func.slice(1, func.length - 5), - value: body, - }, this.mode); - } else if (func in oldFontFuncs) { - const style = oldFontFuncs[func]; - // If we see an old font function, parse out the implicit body - this.consumeSpaces(); - const body = this.parseExpression(true); - if (style.slice(0, 4) === 'text') { - return new ParseNode("text", { - style: style, - body: new ParseNode("ordgroup", body, this.mode), - }, this.mode); - } else { - return new ParseNode("font", { - font: style, - body: new ParseNode("ordgroup", body, this.mode), - }, this.mode); - } - } else if (func === "\\color") { - // If we see a styling function, parse out the implicit body - const color = this.parseColorGroup(false); - if (!color) { - throw new ParseError("\\color not followed by color"); - } - const body = this.parseExpression(true); - return new ParseNode("color", { - type: "color", - color: color.result.value, - value: body, - }, this.mode); - } else if (func === "$") { - if (this.mode === "math") { - throw new ParseError("$ within math mode"); - } + switchMode(newMode) { + this.gullet.unget(this.nextToken); + this.mode = newMode; this.consume(); - const outerMode = this.mode; - this.switchMode("math"); - const body = this.parseExpression(false, "$"); - this.expect("$", true); - this.switchMode(outerMode); - return new ParseNode("styling", { - style: "text", - value: body, - }, "math"); - } else { - // Defer to parseFunction if it's not a function we handle - return this.parseFunction(start); - } -}; - -/** - * Parses an entire function, including its base and all of its arguments. - * The base might either have been parsed already, in which case - * it is provided as an argument, or it's the next group in the input. - * - * @param {ParseFuncOrArgument=} baseGroup optional as described above - * @return {?ParseNode} - */ -Parser.prototype.parseFunction = function(baseGroup) { - if (!baseGroup) { - baseGroup = this.parseGroup(); } - if (baseGroup) { - if (baseGroup.isFunction) { - const func = baseGroup.result; - const funcData = functions[func]; - if (this.mode === "text" && !funcData.allowedInText) { - throw new ParseError( - "Can't use function '" + func + "' in text mode", - baseGroup.token); + /** + * Main parsing function, which parses an entire input. + * + * @return {?Array.} + */ + parse() { + // Try to parse the input + this.mode = "math"; + this.consume(); + const parse = this.parseInput(); + return parse; + } + + /** + * Parses an entire input tree. + */ + parseInput() { + // Parse an expression + const expression = this.parseExpression(false); + // If we succeeded, make sure there's an EOF at the end + this.expect("EOF", false); + return expression; + } + + static endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"]; + + /** + * Parses an "expression", which is a list of atoms. + * + * @param {boolean} breakOnInfix Should the parsing stop when we hit infix + * nodes? This happens when functions have higher precendence + * than infix nodes in implicit parses. + * + * @param {?string} breakOnTokenText The text of the token that the expression + * should end with, or `null` if something else should end the + * expression. + * + * @return {ParseNode} + */ + parseExpression(breakOnInfix, breakOnTokenText) { + const body = []; + // Keep adding atoms to the body until we can't parse any more atoms (either + // we reached the end, a }, or a \right) + while (true) { + const lex = this.nextToken; + if (Parser.endOfExpression.indexOf(lex.text) !== -1) { + break; } + if (breakOnTokenText && lex.text === breakOnTokenText) { + break; + } + if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) { + break; + } + const atom = this.parseAtom(); + if (!atom) { + if (!this.settings.throwOnError && lex.text[0] === "\\") { + const errorNode = this.handleUnsupportedCmd(); + body.push(errorNode); + continue; + } - const args = this.parseArguments(func, funcData); - const token = baseGroup.token; - const result = this.callFunction(func, args, args.pop(), token); - return new ParseNode(result.type, result, this.mode); - } else { - return baseGroup.result; + break; + } + body.push(atom); } - } else { - return null; - } -}; - -/** - * Call a function handler with a suitable context and arguments. - */ -Parser.prototype.callFunction = function(name, args, positions, token) { - const context = { - funcName: name, - parser: this, - positions: positions, - token: token, - }; - return functions[name].handler(context, args); -}; - -/** - * Parses the arguments of a function or environment - * - * @param {string} func "\name" or "\begin{name}" - * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData - * @return the array of arguments, with the list of positions as last element - */ -Parser.prototype.parseArguments = function(func, funcData) { - const totalArgs = funcData.numArgs + funcData.numOptionalArgs; - if (totalArgs === 0) { - return [[this.pos]]; + return this.handleInfixNodes(body); } - const baseGreediness = funcData.greediness; - const positions = [this.pos]; - const args = []; + /** + * Rewrites infix operators such as \over with corresponding commands such + * as \frac. + * + * There can only be one infix operator per group. If there's more than one + * then the expression is ambiguous. This can be resolved by adding {}. + * + * @returns {Array} + */ + handleInfixNodes(body) { + let overIndex = -1; + let funcName; - for (let i = 0; i < totalArgs; i++) { - const nextToken = this.nextToken; - const argType = funcData.argTypes && funcData.argTypes[i]; - let arg; - if (i < funcData.numOptionalArgs) { - if (argType) { - arg = this.parseGroupOfType(argType, true); - } else { - arg = this.parseGroup(true); + for (let i = 0; i < body.length; i++) { + const node = body[i]; + if (node.type === "infix") { + if (overIndex !== -1) { + throw new ParseError( + "only one infix operator per group", + node.value.token); + } + overIndex = i; + funcName = node.value.replaceWith; } - if (!arg) { - args.push(null); - positions.push(this.pos); - continue; + } + + if (overIndex !== -1) { + let numerNode; + let denomNode; + + const numerBody = body.slice(0, overIndex); + const denomBody = body.slice(overIndex + 1); + + if (numerBody.length === 1 && numerBody[0].type === "ordgroup") { + numerNode = numerBody[0]; + } else { + numerNode = new ParseNode("ordgroup", numerBody, this.mode); + } + + if (denomBody.length === 1 && denomBody[0].type === "ordgroup") { + denomNode = denomBody[0]; + } else { + denomNode = new ParseNode("ordgroup", denomBody, this.mode); + } + + const value = this.callFunction( + funcName, [numerNode, denomNode], null); + return [new ParseNode(value.type, value, this.mode)]; + } else { + return body; + } + } + + // The greediness of a superscript or subscript + static SUPSUB_GREEDINESS = 1; + + /** + * Handle a subscript or superscript with nice errors. + */ + handleSupSubscript(name) { + const symbolToken = this.nextToken; + const symbol = symbolToken.text; + this.consume(); + const group = this.parseGroup(); + + if (!group) { + if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") { + return this.handleUnsupportedCmd(); + } else { + throw new ParseError( + "Expected group after '" + symbol + "'", + symbolToken + ); + } + } else if (group.isFunction) { + // ^ and _ have a greediness, so handle interactions with functions' + // greediness + const funcGreediness = functions[group.result].greediness; + if (funcGreediness > Parser.SUPSUB_GREEDINESS) { + return this.parseFunction(group); + } else { + throw new ParseError( + "Got function '" + group.result + "' with no arguments " + + "as " + name, symbolToken); } } else { - if (argType) { - arg = this.parseGroupOfType(argType); - } else { - arg = this.parseGroup(); - } - if (!arg) { - if (!this.settings.throwOnError && - this.nextToken.text[0] === "\\") { - arg = new ParseFuncOrArgument( - this.handleUnsupportedCmd(this.nextToken.text), - false); - } else { + return group.result; + } + } + + /** + * Converts the textual input of an unsupported command into a text node + * contained within a color node whose color is determined by errorColor + */ + handleUnsupportedCmd() { + const text = this.nextToken.text; + const textordArray = []; + + for (let i = 0; i < text.length; i++) { + textordArray.push(new ParseNode("textord", text[i], "text")); + } + + const textNode = new ParseNode( + "text", + { + body: textordArray, + type: "text", + }, + this.mode); + + const colorNode = new ParseNode( + "color", + { + color: this.settings.errorColor, + value: [textNode], + type: "color", + }, + this.mode); + + this.consume(); + return colorNode; + } + + /** + * Parses a group with optional super/subscripts. + * + * @return {?ParseNode} + */ + parseAtom() { + // The body of an atom is an implicit group, so that things like + // \left(x\right)^2 work correctly. + const base = this.parseImplicitGroup(); + + // In text mode, we don't have superscripts or subscripts + if (this.mode === "text") { + return base; + } + + // Note that base may be empty (i.e. null) at this point. + + let superscript; + let subscript; + while (true) { + // Lex the first token + const lex = this.nextToken; + + if (lex.text === "\\limits" || lex.text === "\\nolimits") { + // We got a limit control + if (!base || base.type !== "op") { throw new ParseError( - "Expected group after '" + func + "'", nextToken); + "Limit controls must follow a math operator", + lex); + } else { + const limits = lex.text === "\\limits"; + base.value.limits = limits; + base.value.alwaysHandleSupSub = true; + } + this.consume(); + } else if (lex.text === "^") { + // We got a superscript start + if (superscript) { + throw new ParseError("Double superscript", lex); + } + superscript = this.handleSupSubscript("superscript"); + } else if (lex.text === "_") { + // We got a subscript start + if (subscript) { + throw new ParseError("Double subscript", lex); + } + subscript = this.handleSupSubscript("subscript"); + } else if (lex.text === "'") { + // We got a prime + if (superscript) { + throw new ParseError("Double superscript", lex); + } + const prime = new ParseNode("textord", "\\prime", this.mode); + + // Many primes can be grouped together, so we handle this here + const primes = [prime]; + this.consume(); + // Keep lexing tokens until we get something that's not a prime + while (this.nextToken.text === "'") { + // For each one, add another prime to the list + primes.push(prime); + this.consume(); + } + // If there's a superscript following the primes, combine that + // superscript in with the primes. + if (this.nextToken.text === "^") { + primes.push(this.handleSupSubscript("superscript")); + } + // Put everything into an ordgroup as the superscript + superscript = new ParseNode("ordgroup", primes, this.mode); + } else { + // If it wasn't ^, _, or ', stop parsing super/subscripts + break; + } + } + + if (superscript || subscript) { + // If we got either a superscript or subscript, create a supsub + return new ParseNode("supsub", { + base: base, + sup: superscript, + sub: subscript, + }, this.mode); + } else { + // Otherwise return the original body + return base; + } + } + + // A list of the size-changing functions, for use in parseImplicitGroup + static sizeFuncs = [ + "\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small", + "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", + ]; + + // A list of the style-changing functions, for use in parseImplicitGroup + static styleFuncs = [ + "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle", + ]; + + // Old font functions + static oldFontFuncs = { + "\\rm": "mathrm", + "\\sf": "mathsf", + "\\tt": "mathtt", + "\\bf": "mathbf", + "\\it": "mathit", + //"\\sl": "textsl", + //"\\sc": "textsc", + }; + + /** + * Parses an implicit group, which is a group that starts at the end of a + * specified, and ends right before a higher explicit group ends, or at EOL. It + * is used for functions that appear to affect the current style, like \Large or + * \textrm, where instead of keeping a style we just pretend that there is an + * implicit grouping after it until the end of the group. E.g. + * small text {\Large large text} small text again + * It is also used for \left and \right to get the correct grouping. + * + * @return {?ParseNode} + */ + parseImplicitGroup() { + const start = this.parseSymbol(); + + if (start == null) { + // If we didn't get anything we handle, fall back to parseFunction + return this.parseFunction(); + } + + const func = start.result; + + if (func === "\\left") { + // If we see a left: + // Parse the entire left function (including the delimiter) + const left = this.parseFunction(start); + // Parse out the implicit body + ++this.leftrightDepth; + const body = this.parseExpression(false); + --this.leftrightDepth; + // Check the next token + this.expect("\\right", false); + const right = this.parseFunction(); + return new ParseNode("leftright", { + body: body, + left: left.value.value, + right: right.value.value, + }, this.mode); + } else if (func === "\\begin") { + // begin...end is similar to left...right + const begin = this.parseFunction(start); + const envName = begin.value.name; + if (!environments.hasOwnProperty(envName)) { + throw new ParseError( + "No such environment: " + envName, begin.value.nameGroup); + } + // Build the environment object. Arguments and other information will + // be made available to the begin and end methods using properties. + const env = environments[envName]; + const args = this.parseArguments("\\begin{" + envName + "}", env); + const context = { + mode: this.mode, + envName: envName, + parser: this, + positions: args.pop(), + }; + const result = env.handler(context, args); + this.expect("\\end", false); + const endNameToken = this.nextToken; + const end = this.parseFunction(); + if (end.value.name !== envName) { + throw new ParseError( + "Mismatch: \\begin{" + envName + "} matched " + + "by \\end{" + end.value.name + "}", + endNameToken); + } + result.position = end.position; + return result; + } else if (utils.contains(Parser.sizeFuncs, func)) { + // If we see a sizing function, parse out the implicit body + this.consumeSpaces(); + const body = this.parseExpression(false); + return new ParseNode("sizing", { + // Figure out what size to use based on the list of functions above + size: utils.indexOf(Parser.sizeFuncs, func) + 1, + value: body, + }, this.mode); + } else if (utils.contains(Parser.styleFuncs, func)) { + // If we see a styling function, parse out the implicit body + this.consumeSpaces(); + const body = this.parseExpression(true); + return new ParseNode("styling", { + // Figure out what style to use by pulling out the style from + // the function name + style: func.slice(1, func.length - 5), + value: body, + }, this.mode); + } else if (func in Parser.oldFontFuncs) { + const style = Parser.oldFontFuncs[func]; + // If we see an old font function, parse out the implicit body + this.consumeSpaces(); + const body = this.parseExpression(true); + if (style.slice(0, 4) === 'text') { + return new ParseNode("text", { + style: style, + body: new ParseNode("ordgroup", body, this.mode), + }, this.mode); + } else { + return new ParseNode("font", { + font: style, + body: new ParseNode("ordgroup", body, this.mode), + }, this.mode); + } + } else if (func === "\\color") { + // If we see a styling function, parse out the implicit body + const color = this.parseColorGroup(false); + if (!color) { + throw new ParseError("\\color not followed by color"); + } + const body = this.parseExpression(true); + return new ParseNode("color", { + type: "color", + color: color.result.value, + value: body, + }, this.mode); + } else if (func === "$") { + if (this.mode === "math") { + throw new ParseError("$ within math mode"); + } + this.consume(); + const outerMode = this.mode; + this.switchMode("math"); + const body = this.parseExpression(false, "$"); + this.expect("$", true); + this.switchMode(outerMode); + return new ParseNode("styling", { + style: "text", + value: body, + }, "math"); + } else { + // Defer to parseFunction if it's not a function we handle + return this.parseFunction(start); + } + } + + /** + * Parses an entire function, including its base and all of its arguments. + * The base might either have been parsed already, in which case + * it is provided as an argument, or it's the next group in the input. + * + * @param {ParseFuncOrArgument=} baseGroup optional as described above + * @return {?ParseNode} + */ + parseFunction(baseGroup) { + if (!baseGroup) { + baseGroup = this.parseGroup(); + } + + if (baseGroup) { + if (baseGroup.isFunction) { + const func = baseGroup.result; + const funcData = functions[func]; + if (this.mode === "text" && !funcData.allowedInText) { + throw new ParseError( + "Can't use function '" + func + "' in text mode", + baseGroup.token); + } + + const args = this.parseArguments(func, funcData); + const token = baseGroup.token; + const result = this.callFunction(func, args, args.pop(), token); + return new ParseNode(result.type, result, this.mode); + } else { + return baseGroup.result; + } + } else { + return null; + } + } + + /** + * Call a function handler with a suitable context and arguments. + */ + callFunction(name, args, positions, token) { + const context = { + funcName: name, + parser: this, + positions: positions, + token: token, + }; + return functions[name].handler(context, args); + } + + /** + * Parses the arguments of a function or environment + * + * @param {string} func "\name" or "\begin{name}" + * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData + * @return the array of arguments, with the list of positions as last element + */ + parseArguments(func, funcData) { + const totalArgs = funcData.numArgs + funcData.numOptionalArgs; + if (totalArgs === 0) { + return [[this.pos]]; + } + + const baseGreediness = funcData.greediness; + const positions = [this.pos]; + const args = []; + + for (let i = 0; i < totalArgs; i++) { + const nextToken = this.nextToken; + const argType = funcData.argTypes && funcData.argTypes[i]; + let arg; + if (i < funcData.numOptionalArgs) { + if (argType) { + arg = this.parseGroupOfType(argType, true); + } else { + arg = this.parseGroup(true); + } + if (!arg) { + args.push(null); + positions.push(this.pos); + continue; + } + } else { + if (argType) { + arg = this.parseGroupOfType(argType); + } else { + arg = this.parseGroup(); + } + if (!arg) { + if (!this.settings.throwOnError && + this.nextToken.text[0] === "\\") { + arg = new ParseFuncOrArgument( + this.handleUnsupportedCmd(this.nextToken.text), + false); + } else { + throw new ParseError( + "Expected group after '" + func + "'", nextToken); + } } } - } - let argNode; - if (arg.isFunction) { - const argGreediness = - functions[arg.result].greediness; - if (argGreediness > baseGreediness) { - argNode = this.parseFunction(arg); + let argNode; + if (arg.isFunction) { + const argGreediness = + functions[arg.result].greediness; + if (argGreediness > baseGreediness) { + argNode = this.parseFunction(arg); + } else { + throw new ParseError( + "Got function '" + arg.result + "' as " + + "argument to '" + func + "'", nextToken); + } } else { - throw new ParseError( - "Got function '" + arg.result + "' as " + - "argument to '" + func + "'", nextToken); + argNode = arg.result; } - } else { - argNode = arg.result; + args.push(argNode); + positions.push(this.pos); } - args.push(argNode); - positions.push(this.pos); + + args.push(positions); + + return args; } - args.push(positions); - - return args; -}; - - -/** - * Parses a group when the mode is changing. - * - * @return {?ParseFuncOrArgument} - */ -Parser.prototype.parseGroupOfType = function(innerMode, optional) { - const outerMode = this.mode; - // Handle `original` argTypes - if (innerMode === "original") { - innerMode = outerMode; - } - - if (innerMode === "color") { - return this.parseColorGroup(optional); - } - if (innerMode === "size") { - return this.parseSizeGroup(optional); - } - - this.switchMode(innerMode); - if (innerMode === "text") { - // text mode is special because it should ignore the whitespace before - // it - this.consumeSpaces(); - } - // By the time we get here, innerMode is one of "text" or "math". - // We switch the mode of the parser, recurse, then restore the old mode. - const res = this.parseGroup(optional); - this.switchMode(outerMode); - return res; -}; - -Parser.prototype.consumeSpaces = function() { - while (this.nextToken.text === " ") { - this.consume(); - } -}; - -/** - * Parses a group, essentially returning the string formed by the - * brace-enclosed tokens plus some position information. - * - * @param {string} modeName Used to describe the mode in error messages - * @param {boolean=} optional Whether the group is optional or required - */ -Parser.prototype.parseStringGroup = function(modeName, optional) { - if (optional && this.nextToken.text !== "[") { - return null; - } - const outerMode = this.mode; - this.mode = "text"; - this.expect(optional ? "[" : "{"); - let str = ""; - const firstToken = this.nextToken; - let lastToken = firstToken; - while (this.nextToken.text !== (optional ? "]" : "}")) { - if (this.nextToken.text === "EOF") { - throw new ParseError( - "Unexpected end of input in " + modeName, - firstToken.range(this.nextToken, str)); + /** + * Parses a group when the mode is changing. + * + * @return {?ParseFuncOrArgument} + */ + parseGroupOfType(innerMode, optional) { + const outerMode = this.mode; + // Handle `original` argTypes + if (innerMode === "original") { + innerMode = outerMode; } - lastToken = this.nextToken; - str += lastToken.text; - this.consume(); - } - this.mode = outerMode; - this.expect(optional ? "]" : "}"); - return firstToken.range(lastToken, str); -}; -/** - * Parses a regex-delimited group: the largest sequence of tokens - * whose concatenated strings match `regex`. Returns the string - * formed by the tokens plus some position information. - * - * @param {RegExp} regex - * @param {string} modeName Used to describe the mode in error messages - */ -Parser.prototype.parseRegexGroup = function(regex, modeName) { - const outerMode = this.mode; - this.mode = "text"; - const firstToken = this.nextToken; - let lastToken = firstToken; - let str = ""; - while (this.nextToken.text !== "EOF" - && regex.test(str + this.nextToken.text)) { - lastToken = this.nextToken; - str += lastToken.text; - this.consume(); - } - if (str === "") { - throw new ParseError( - "Invalid " + modeName + ": '" + firstToken.text + "'", - firstToken); - } - this.mode = outerMode; - return firstToken.range(lastToken, str); -}; + if (innerMode === "color") { + return this.parseColorGroup(optional); + } + if (innerMode === "size") { + return this.parseSizeGroup(optional); + } -/** - * Parses a color description. - */ -Parser.prototype.parseColorGroup = function(optional) { - const res = this.parseStringGroup("color", optional); - if (!res) { - return null; + this.switchMode(innerMode); + if (innerMode === "text") { + // text mode is special because it should ignore the whitespace before + // it + this.consumeSpaces(); + } + // By the time we get here, innerMode is one of "text" or "math". + // We switch the mode of the parser, recurse, then restore the old mode. + const res = this.parseGroup(optional); + this.switchMode(outerMode); + return res; } - const match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text); - if (!match) { - throw new ParseError("Invalid color: '" + res.text + "'", res); - } - return new ParseFuncOrArgument( - new ParseNode("color", match[0], this.mode), - false); -}; -/** - * Parses a size specification, consisting of magnitude and unit. - */ -Parser.prototype.parseSizeGroup = function(optional) { - let res; - if (!optional && this.nextToken.text !== "{") { - res = this.parseRegexGroup( - /^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/, "size"); - } else { - res = this.parseStringGroup("size", optional); + consumeSpaces() { + while (this.nextToken.text === " ") { + this.consume(); + } } - if (!res) { - return null; - } - const match = (/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text); - if (!match) { - throw new ParseError("Invalid size: '" + res.text + "'", res); - } - const data = { - number: +(match[1] + match[2]), // sign + magnitude, cast to number - unit: match[3], - }; - if (data.unit !== "em" && data.unit !== "ex" && data.unit !== "mu") { - throw new ParseError("Invalid unit: '" + data.unit + "'", res); - } - return new ParseFuncOrArgument( - new ParseNode("color", data, this.mode), - false); -}; -/** - * If the argument is false or absent, this parses an ordinary group, - * which is either a single nucleus (like "x") or an expression - * in braces (like "{x+y}"). - * If the argument is true, it parses either a bracket-delimited expression - * (like "[x+y]") or returns null to indicate the absence of a - * bracket-enclosed group. - * - * @param {boolean=} optional Whether the group is optional or required - * @return {?ParseFuncOrArgument} - */ -Parser.prototype.parseGroup = function(optional) { - const firstToken = this.nextToken; - // Try to parse an open brace - if (this.nextToken.text === (optional ? "[" : "{")) { - // If we get a brace, parse an expression - this.consume(); - const expression = this.parseExpression(false, optional ? "]" : null); - const lastToken = this.nextToken; - // Make sure we get a close brace + /** + * Parses a group, essentially returning the string formed by the + * brace-enclosed tokens plus some position information. + * + * @param {string} modeName Used to describe the mode in error messages + * @param {boolean=} optional Whether the group is optional or required + */ + parseStringGroup(modeName, optional) { + if (optional && this.nextToken.text !== "[") { + return null; + } + const outerMode = this.mode; + this.mode = "text"; + this.expect(optional ? "[" : "{"); + let str = ""; + const firstToken = this.nextToken; + let lastToken = firstToken; + while (this.nextToken.text !== (optional ? "]" : "}")) { + if (this.nextToken.text === "EOF") { + throw new ParseError( + "Unexpected end of input in " + modeName, + firstToken.range(this.nextToken, str)); + } + lastToken = this.nextToken; + str += lastToken.text; + this.consume(); + } + this.mode = outerMode; this.expect(optional ? "]" : "}"); - if (this.mode === "text") { - this.formLigatures(expression); + return firstToken.range(lastToken, str); + } + + /** + * Parses a regex-delimited group: the largest sequence of tokens + * whose concatenated strings match `regex`. Returns the string + * formed by the tokens plus some position information. + * + * @param {RegExp} regex + * @param {string} modeName Used to describe the mode in error messages + */ + parseRegexGroup(regex, modeName) { + const outerMode = this.mode; + this.mode = "text"; + const firstToken = this.nextToken; + let lastToken = firstToken; + let str = ""; + while (this.nextToken.text !== "EOF" + && regex.test(str + this.nextToken.text)) { + lastToken = this.nextToken; + str += lastToken.text; + this.consume(); + } + if (str === "") { + throw new ParseError( + "Invalid " + modeName + ": '" + firstToken.text + "'", + firstToken); + } + this.mode = outerMode; + return firstToken.range(lastToken, str); + } + + /** + * Parses a color description. + */ + parseColorGroup(optional) { + const res = this.parseStringGroup("color", optional); + if (!res) { + return null; + } + const match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text); + if (!match) { + throw new ParseError("Invalid color: '" + res.text + "'", res); } return new ParseFuncOrArgument( - new ParseNode("ordgroup", expression, this.mode, - firstToken, lastToken), + new ParseNode("color", match[0], this.mode), false); - } else { - // Otherwise, just return a nucleus, or nothing for an optional group - return optional ? null : this.parseSymbol(); } -}; -/** - * Form ligature-like combinations of characters for text mode. - * This includes inputs like "--", "---", "``" and "''". - * The result will simply replace multiple textord nodes with a single - * character in each value by a single textord node having multiple - * characters in its value. The representation is still ASCII source. - * - * @param {Array.} group the nodes of this group, - * list will be moified in place - */ -Parser.prototype.formLigatures = function(group) { - let n = group.length - 1; - for (let i = 0; i < n; ++i) { - const a = group[i]; - const v = a.value; - if (v === "-" && group[i + 1].value === "-") { - if (i + 1 < n && group[i + 2].value === "-") { - group.splice(i, 3, new ParseNode( - "textord", "---", "text", a, group[i + 2])); - n -= 2; - } else { + /** + * Parses a size specification, consisting of magnitude and unit. + */ + parseSizeGroup(optional) { + let res; + if (!optional && this.nextToken.text !== "{") { + res = this.parseRegexGroup( + /^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/, "size"); + } else { + res = this.parseStringGroup("size", optional); + } + if (!res) { + return null; + } + const match = (/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text); + if (!match) { + throw new ParseError("Invalid size: '" + res.text + "'", res); + } + const data = { + number: +(match[1] + match[2]), // sign + magnitude, cast to number + unit: match[3], + }; + if (data.unit !== "em" && data.unit !== "ex" && data.unit !== "mu") { + throw new ParseError("Invalid unit: '" + data.unit + "'", res); + } + return new ParseFuncOrArgument( + new ParseNode("color", data, this.mode), + false); + } + + /** + * If the argument is false or absent, this parses an ordinary group, + * which is either a single nucleus (like "x") or an expression + * in braces (like "{x+y}"). + * If the argument is true, it parses either a bracket-delimited expression + * (like "[x+y]") or returns null to indicate the absence of a + * bracket-enclosed group. + * + * @param {boolean=} optional Whether the group is optional or required + * @return {?ParseFuncOrArgument} + */ + parseGroup(optional) { + const firstToken = this.nextToken; + // Try to parse an open brace + if (this.nextToken.text === (optional ? "[" : "{")) { + // If we get a brace, parse an expression + this.consume(); + const expression = this.parseExpression(false, optional ? "]" : null); + const lastToken = this.nextToken; + // Make sure we get a close brace + this.expect(optional ? "]" : "}"); + if (this.mode === "text") { + this.formLigatures(expression); + } + return new ParseFuncOrArgument( + new ParseNode("ordgroup", expression, this.mode, + firstToken, lastToken), + false); + } else { + // Otherwise, just return a nucleus, or nothing for an optional group + return optional ? null : this.parseSymbol(); + } + } + + /** + * Form ligature-like combinations of characters for text mode. + * This includes inputs like "--", "---", "``" and "''". + * The result will simply replace multiple textord nodes with a single + * character in each value by a single textord node having multiple + * characters in its value. The representation is still ASCII source. + * + * @param {Array.} group the nodes of this group, + * list will be moified in place + */ + formLigatures(group) { + let n = group.length - 1; + for (let i = 0; i < n; ++i) { + const a = group[i]; + const v = a.value; + if (v === "-" && group[i + 1].value === "-") { + if (i + 1 < n && group[i + 2].value === "-") { + group.splice(i, 3, new ParseNode( + "textord", "---", "text", a, group[i + 2])); + n -= 2; + } else { + group.splice(i, 2, new ParseNode( + "textord", "--", "text", a, group[i + 1])); + n -= 1; + } + } + if ((v === "'" || v === "`") && group[i + 1].value === v) { group.splice(i, 2, new ParseNode( - "textord", "--", "text", a, group[i + 1])); + "textord", v + v, "text", a, group[i + 1])); n -= 1; } } - if ((v === "'" || v === "`") && group[i + 1].value === v) { - group.splice(i, 2, new ParseNode( - "textord", v + v, "text", a, group[i + 1])); - n -= 1; + } + + /** + * Parse a single symbol out of the string. Here, we handle both the functions + * we have defined, as well as the single character symbols + * + * @return {?ParseFuncOrArgument} + */ + parseSymbol() { + const nucleus = this.nextToken; + + if (functions[nucleus.text]) { + this.consume(); + // If there exists a function with this name, we return the function and + // say that it is a function. + return new ParseFuncOrArgument( + nucleus.text, + true, nucleus); + } else if (symbols[this.mode][nucleus.text]) { + this.consume(); + // Otherwise if this is a no-argument function, find the type it + // corresponds to in the symbols map + return new ParseFuncOrArgument( + new ParseNode(symbols[this.mode][nucleus.text].group, + nucleus.text, this.mode, nucleus), + false, nucleus); + } else if (this.mode === "text" && cjkRegex.test(nucleus.text)) { + this.consume(); + return new ParseFuncOrArgument( + new ParseNode("textord", nucleus.text, this.mode, nucleus), + false, nucleus); + } else if (nucleus.text === "$") { + return new ParseFuncOrArgument( + nucleus.text, + false, nucleus); + } else { + return null; } } -}; - -/** - * Parse a single symbol out of the string. Here, we handle both the functions - * we have defined, as well as the single character symbols - * - * @return {?ParseFuncOrArgument} - */ -Parser.prototype.parseSymbol = function() { - const nucleus = this.nextToken; - - if (functions[nucleus.text]) { - this.consume(); - // If there exists a function with this name, we return the function and - // say that it is a function. - return new ParseFuncOrArgument( - nucleus.text, - true, nucleus); - } else if (symbols[this.mode][nucleus.text]) { - this.consume(); - // Otherwise if this is a no-argument function, find the type it - // corresponds to in the symbols map - return new ParseFuncOrArgument( - new ParseNode(symbols[this.mode][nucleus.text].group, - nucleus.text, this.mode, nucleus), - false, nucleus); - } else if (this.mode === "text" && cjkRegex.test(nucleus.text)) { - this.consume(); - return new ParseFuncOrArgument( - new ParseNode("textord", nucleus.text, this.mode, nucleus), - false, nucleus); - } else if (nucleus.text === "$") { - return new ParseFuncOrArgument( - nucleus.text, - false, nucleus); - } else { - return null; - } -}; +} Parser.prototype.ParseNode = ParseNode; diff --git a/src/Settings.js b/src/Settings.js index bd494886..26a93ee2 100644 --- a/src/Settings.js +++ b/src/Settings.js @@ -3,7 +3,7 @@ * default settings. */ -const utils = require("./utils"); +import utils from "./utils"; /** * The main Settings object @@ -15,14 +15,16 @@ const utils = require("./utils"); * math (true), meaning that the math starts in \displaystyle * and is placed in a block with vertical margin. */ -function Settings(options) { - // allow null options - options = options || {}; - this.displayMode = utils.deflt(options.displayMode, false); - this.throwOnError = utils.deflt(options.throwOnError, true); - this.errorColor = utils.deflt(options.errorColor, "#cc0000"); - this.macros = options.macros || {}; - this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false); +class Settings { + constructor(options) { + // allow null options + options = options || {}; + this.displayMode = utils.deflt(options.displayMode, false); + this.throwOnError = utils.deflt(options.throwOnError, true); + this.errorColor = utils.deflt(options.errorColor, "#cc0000"); + this.macros = options.macros || {}; + this.colorIsTextColor = utils.deflt(options.colorIsTextColor, false); + } } module.exports = Settings; diff --git a/src/Style.js b/src/Style.js index 7a5cd85b..50f30fb5 100644 --- a/src/Style.js +++ b/src/Style.js @@ -10,64 +10,66 @@ * The main style class. Contains a unique id for the style, a size (which is * the same for cramped and uncramped version of a style), and a cramped flag. */ -function Style(id, size, cramped) { - this.id = id; - this.size = size; - this.cramped = cramped; +class Style { + constructor(id, size, cramped) { + this.id = id; + this.size = size; + this.cramped = cramped; + } + + /** + * Get the style of a superscript given a base in the current style. + */ + sup() { + return styles[sup[this.id]]; + } + + /** + * Get the style of a subscript given a base in the current style. + */ + sub() { + return styles[sub[this.id]]; + } + + /** + * Get the style of a fraction numerator given the fraction in the current + * style. + */ + fracNum() { + return styles[fracNum[this.id]]; + } + + /** + * Get the style of a fraction denominator given the fraction in the current + * style. + */ + fracDen() { + return styles[fracDen[this.id]]; + } + + /** + * Get the cramped version of a style (in particular, cramping a cramped style + * doesn't change the style). + */ + cramp() { + return styles[cramp[this.id]]; + } + + /** + * Get a text or display version of this style. + */ + text() { + return styles[text[this.id]]; + } + + /** + * Return if this style is tightly spaced (scriptstyle/scriptscriptstyle) + */ + isTight() { + return this.size >= 2; + } } -/** - * Get the style of a superscript given a base in the current style. - */ -Style.prototype.sup = function() { - return styles[sup[this.id]]; -}; - -/** - * Get the style of a subscript given a base in the current style. - */ -Style.prototype.sub = function() { - return styles[sub[this.id]]; -}; - -/** - * Get the style of a fraction numerator given the fraction in the current - * style. - */ -Style.prototype.fracNum = function() { - return styles[fracNum[this.id]]; -}; - -/** - * Get the style of a fraction denominator given the fraction in the current - * style. - */ -Style.prototype.fracDen = function() { - return styles[fracDen[this.id]]; -}; - -/** - * Get the cramped version of a style (in particular, cramping a cramped style - * doesn't change the style). - */ -Style.prototype.cramp = function() { - return styles[cramp[this.id]]; -}; - -/** - * Get a text or display version of this style. - */ -Style.prototype.text = function() { - return styles[text[this.id]]; -}; - -/** - * Return if this style is tightly spaced (scriptstyle/scriptscriptstyle) - */ -Style.prototype.isTight = function() { - return this.size >= 2; -}; - // IDs of the different styles const D = 0; const Dc = 1; diff --git a/src/buildCommon.js b/src/buildCommon.js index d9a6a8c4..97319e92 100644 --- a/src/buildCommon.js +++ b/src/buildCommon.js @@ -4,10 +4,10 @@ * different kinds of domTree nodes in a consistent manner. */ -const domTree = require("./domTree"); -const fontMetrics = require("./fontMetrics"); -const symbols = require("./symbols"); -const utils = require("./utils"); +import domTree from "./domTree"; +import fontMetrics from "./fontMetrics"; +import symbols from "./symbols"; +import utils from "./utils"; // The following have to be loaded from Main-Italic font, using class mainit const mainitLetters = [ diff --git a/src/buildHTML.js b/src/buildHTML.js index 270a3ba6..a383197a 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -6,16 +6,14 @@ * called, to produce a final HTML tree. */ -const ParseError = require("./ParseError"); -const Style = require("./Style"); +import ParseError from "./ParseError"; +import Style from "./Style"; -const buildCommon = require("./buildCommon"); -const delimiter = require("./delimiter"); -const domTree = require("./domTree"); -const utils = require("./utils"); -const stretchy = require("./stretchy"); - -const makeSpan = buildCommon.makeSpan; +import buildCommon, { makeSpan } from "./buildCommon"; +import delimiter from "./delimiter"; +import domTree from "./domTree"; +import utils from "./utils"; +import stretchy from "./stretchy"; const isSpace = function(node) { return node instanceof domTree.span && node.classes[0] === "mspace"; @@ -24,7 +22,6 @@ const isSpace = function(node) { // Binary atoms (first class `mbin`) change into ordinary atoms (`mord`) // depending on their surroundings. See TeXbook pg. 442-446, Rules 5 and 6, // and the text before Rule 19. - const isBin = function(node) { return node && node.classes[0] === "mbin"; }; diff --git a/src/buildMathML.js b/src/buildMathML.js index f6ff5400..48f8507b 100644 --- a/src/buildMathML.js +++ b/src/buildMathML.js @@ -4,16 +4,13 @@ * parser. */ -const buildCommon = require("./buildCommon"); -const fontMetrics = require("./fontMetrics"); -const mathMLTree = require("./mathMLTree"); -const ParseError = require("./ParseError"); -const symbols = require("./symbols"); -const utils = require("./utils"); -const stretchy = require("./stretchy"); - -const makeSpan = buildCommon.makeSpan; -const fontMap = buildCommon.fontMap; +import buildCommon, { makeSpan, fontMap } from "./buildCommon"; +import fontMetrics from "./fontMetrics"; +import mathMLTree from "./mathMLTree"; +import ParseError from "./ParseError"; +import symbols from "./symbols"; +import utils from "./utils"; +import stretchy from "./stretchy"; /** * Takes a symbol and converts it into a MathML text node after performing diff --git a/src/buildTree.js b/src/buildTree.js index 6a3bec33..aa3e24eb 100644 --- a/src/buildTree.js +++ b/src/buildTree.js @@ -1,11 +1,9 @@ -const buildHTML = require("./buildHTML"); -const buildMathML = require("./buildMathML"); -const buildCommon = require("./buildCommon"); -const Options = require("./Options"); -const Settings = require("./Settings"); -const Style = require("./Style"); - -const makeSpan = buildCommon.makeSpan; +import buildHTML from "./buildHTML"; +import buildMathML from "./buildMathML"; +import { makeSpan } from "./buildCommon"; +import Options from "./Options"; +import Settings from "./Settings"; +import Style from "./Style"; const buildTree = function(tree, expression, settings) { settings = settings || new Settings({}); diff --git a/src/delimiter.js b/src/delimiter.js index 98a24e38..1d6e8fa8 100644 --- a/src/delimiter.js +++ b/src/delimiter.js @@ -20,15 +20,13 @@ * used in `\left` and `\right`. */ -const ParseError = require("./ParseError"); -const Style = require("./Style"); +import ParseError from "./ParseError"; +import Style from "./Style"; -const buildCommon = require("./buildCommon"); -const fontMetrics = require("./fontMetrics"); -const symbols = require("./symbols"); -const utils = require("./utils"); - -const makeSpan = buildCommon.makeSpan; +import buildCommon, { makeSpan } from "./buildCommon"; +import fontMetrics from "./fontMetrics"; +import symbols from "./symbols"; +import utils from "./utils"; /** * Get the metrics for a given symbol and font, after transformation (i.e. diff --git a/src/domTree.js b/src/domTree.js index a49b7667..c04f82bd 100644 --- a/src/domTree.js +++ b/src/domTree.js @@ -7,8 +7,8 @@ * * Similar functions for working with MathML nodes exist in mathMLTree.js. */ -const unicodeRegexes = require("./unicodeRegexes"); -const utils = require("./utils"); +import unicodeRegexes from "./unicodeRegexes"; +import utils from "./utils"; /** * Create an HTML className based on a list of classes. In addition to joining @@ -30,114 +30,116 @@ const createClass = function(classes) { * an inline style. It also contains information about its height, depth, and * maxFontSize. */ -function span(classes, children, options) { - this.classes = classes || []; - this.children = children || []; - this.height = 0; - this.depth = 0; - this.maxFontSize = 0; - this.style = {}; - this.attributes = {}; - if (options) { - if (options.style.isTight()) { - this.classes.push("mtight"); - } - if (options.getColor()) { - this.style.color = options.getColor(); - } - } -} - -/** - * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all - * browsers support attributes the same, and having too many custom attributes - * is probably bad. - */ -span.prototype.setAttribute = function(attribute, value) { - this.attributes[attribute] = value; -}; - -span.prototype.tryCombine = function(sibling) { - return false; -}; - -/** - * Convert the span into an HTML node - */ -span.prototype.toNode = function() { - const span = document.createElement("span"); - - // Apply the class - span.className = createClass(this.classes); - - // Apply inline styles - for (const style in this.style) { - if (Object.prototype.hasOwnProperty.call(this.style, style)) { - span.style[style] = this.style[style]; +class span { + constructor(classes, children, options) { + this.classes = classes || []; + this.children = children || []; + this.height = 0; + this.depth = 0; + this.maxFontSize = 0; + this.style = {}; + this.attributes = {}; + if (options) { + if (options.style.isTight()) { + this.classes.push("mtight"); + } + if (options.getColor()) { + this.style.color = options.getColor(); + } } } - // Apply attributes - for (const attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - span.setAttribute(attr, this.attributes[attr]); + /** + * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all + * browsers support attributes the same, and having too many custom attributes + * is probably bad. + */ + setAttribute(attribute, value) { + this.attributes[attribute] = value; + } + + tryCombine(sibling) { + return false; + } + + /** + * Convert the span into an HTML node + */ + toNode() { + const span = document.createElement("span"); + + // Apply the class + span.className = createClass(this.classes); + + // Apply inline styles + for (const style in this.style) { + if (Object.prototype.hasOwnProperty.call(this.style, style)) { + span.style[style] = this.style[style]; + } } - } - // Append the children, also as HTML nodes - for (let i = 0; i < this.children.length; i++) { - span.appendChild(this.children[i].toNode()); - } - - return span; -}; - -/** - * Convert the span into an HTML markup string - */ -span.prototype.toMarkup = function() { - let markup = " 0 + || createClass(this.classes) !== createClass(sibling.classes) + || this.skew !== sibling.skew + || this.maxFontSize !== sibling.maxFontSize) { + return false; + } + for (const style in this.style) { + if (this.style.hasOwnProperty(style) + && this.style[style] !== sibling.style[style]) { + return false; + } + } + for (const style in sibling.style) { + if (sibling.style.hasOwnProperty(style) + && this.style[style] !== sibling.style[style]) { + return false; + } + } + this.value += sibling.value; + this.height = Math.max(this.height, sibling.height); + this.depth = Math.max(this.depth, sibling.depth); + this.italic = sibling.italic; + return true; + } + + /** + * Creates a text node or span from a symbol node. Note that a span is only + * created if it is needed. + */ + toNode() { + const node = document.createTextNode(this.value); + let span = null; + + if (this.italic > 0) { + span = document.createElement("span"); + span.style.marginRight = this.italic + "em"; + } + + if (this.classes.length > 0) { + span = span || document.createElement("span"); + span.className = createClass(this.classes); + } + + for (const style in this.style) { + if (this.style.hasOwnProperty(style)) { + span = span || document.createElement("span"); + span.style[style] = this.style[style]; + } + } + + if (span) { + span.appendChild(node); + return span; + } else { + return node; + } + } + + /** + * Creates markup for a symbol node. + */ + toMarkup() { + // TODO(alpert): More duplication than I'd like from + // span.prototype.toMarkup and symbolNode.prototype.toNode... + let needsSpan = false; + + let markup = " 0) { + styles += "margin-right:" + this.italic + "em;"; + } + for (const style in this.style) { + if (this.style.hasOwnProperty(style)) { + styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; + } + } + + if (styles) { + needsSpan = true; + markup += " style=\"" + utils.escape(styles) + "\""; + } + + const escaped = utils.escape(this.value); + if (needsSpan) { + markup += ">"; + markup += escaped; + markup += ""; + return markup; + } else { + return escaped; + } } } -symbolNode.prototype.tryCombine = function(sibling) { - if (!sibling - || !(sibling instanceof symbolNode) - || this.italic > 0 - || createClass(this.classes) !== createClass(sibling.classes) - || this.skew !== sibling.skew - || this.maxFontSize !== sibling.maxFontSize) { - return false; - } - for (const style in this.style) { - if (this.style.hasOwnProperty(style) - && this.style[style] !== sibling.style[style]) { - return false; - } - } - for (const style in sibling.style) { - if (sibling.style.hasOwnProperty(style) - && this.style[style] !== sibling.style[style]) { - return false; - } - } - this.value += sibling.value; - this.height = Math.max(this.height, sibling.height); - this.depth = Math.max(this.depth, sibling.depth); - this.italic = sibling.italic; - return true; -}; - -/** - * Creates a text node or span from a symbol node. Note that a span is only - * created if it is needed. - */ -symbolNode.prototype.toNode = function() { - const node = document.createTextNode(this.value); - let span = null; - - if (this.italic > 0) { - span = document.createElement("span"); - span.style.marginRight = this.italic + "em"; - } - - if (this.classes.length > 0) { - span = span || document.createElement("span"); - span.className = createClass(this.classes); - } - - for (const style in this.style) { - if (this.style.hasOwnProperty(style)) { - span = span || document.createElement("span"); - span.style[style] = this.style[style]; - } - } - - if (span) { - span.appendChild(node); - return span; - } else { - return node; - } -}; - -/** - * Creates markup for a symbol node. - */ -symbolNode.prototype.toMarkup = function() { - // TODO(alpert): More duplication than I'd like from - // span.prototype.toMarkup and symbolNode.prototype.toNode... - let needsSpan = false; - - let markup = " 0) { - styles += "margin-right:" + this.italic + "em;"; - } - for (const style in this.style) { - if (this.style.hasOwnProperty(style)) { - styles += utils.hyphenate(style) + ":" + this.style[style] + ";"; - } - } - - if (styles) { - needsSpan = true; - markup += " style=\"" + utils.escape(styles) + "\""; - } - - const escaped = utils.escape(this.value); - if (needsSpan) { - markup += ">"; - markup += escaped; - markup += ""; - return markup; - } else { - return escaped; - } -}; - module.exports = { span: span, documentFragment: documentFragment, diff --git a/src/environments.js b/src/environments.js index 9ee204bc..0bc8a6d5 100644 --- a/src/environments.js +++ b/src/environments.js @@ -1,8 +1,6 @@ /* eslint no-constant-condition:0 */ -const parseData = require("./parseData"); -const ParseError = require("./ParseError"); - -const ParseNode = parseData.ParseNode; +import { ParseNode } from "./parseData"; +import ParseError from "./ParseError"; /** * Parse the body of the environment, with rows delimited by \\ and @@ -67,7 +65,6 @@ function parseArray(parser, result, style) { * - positions: the positions associated with these arguments from args. * The handler must return a ParseResult. */ - function defineEnvironment(names, props, handler) { if (typeof names === "string") { names = [names]; diff --git a/src/fontMetrics.js b/src/fontMetrics.js index 39ae0647..f8048c16 100644 --- a/src/fontMetrics.js +++ b/src/fontMetrics.js @@ -1,4 +1,4 @@ -const cjkRegex = require("./unicodeRegexes").cjkRegex; +import { cjkRegex } from "./unicodeRegexes"; /** * This file contains metrics regarding fonts and individual symbols. The sigma @@ -81,7 +81,7 @@ const sigmasAndXis = { // metrics, including height, depth, italic correction, and skew (kern from the // character to the corresponding \skewchar) // This map is generated via `make metrics`. It should not be changed manually. -const metricMap = require("./fontMetricsData"); +import metricMap from "./fontMetricsData"; // These are very rough approximations. We default to Times New Roman which // should have Latin-1 and Cyrillic characters, but may not depending on the diff --git a/src/functions.js b/src/functions.js index 773730cb..d54e0685 100644 --- a/src/functions.js +++ b/src/functions.js @@ -1,7 +1,6 @@ -const utils = require("./utils"); -const ParseError = require("./ParseError"); -const parseData = require("./parseData"); -const ParseNode = parseData.ParseNode; +import utils from "./utils"; +import ParseError from "./ParseError"; +import { ParseNode } from "./parseData"; /* This file contains a list of functions that we parse, identified by * the calls to defineFunction. diff --git a/src/mathMLTree.js b/src/mathMLTree.js index 36808148..49650be1 100644 --- a/src/mathMLTree.js +++ b/src/mathMLTree.js @@ -8,94 +8,98 @@ * domTree.js, creating namespaced DOM nodes and HTML text markup respectively. */ -const utils = require("./utils"); +import utils from "./utils"; /** * This node represents a general purpose MathML node of any type. The * constructor requires the type of node to create (for example, `"mo"` or * `"mspace"`, corresponding to `` and `` tags). */ -function MathNode(type, children) { - this.type = type; - this.attributes = {}; - this.children = children || []; +class MathNode { + constructor(type, children) { + this.type = type; + this.attributes = {}; + this.children = children || []; + } + + /** + * Sets an attribute on a MathML node. MathML depends on attributes to convey a + * semantic content, so this is used heavily. + */ + setAttribute(name, value) { + this.attributes[name] = value; + } + + /** + * Converts the math node into a MathML-namespaced DOM element. + */ + toNode() { + const node = document.createElementNS( + "http://www.w3.org/1998/Math/MathML", this.type); + + for (const attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + node.setAttribute(attr, this.attributes[attr]); + } + } + + for (let i = 0; i < this.children.length; i++) { + node.appendChild(this.children[i].toNode()); + } + + return node; + } + + /** + * Converts the math node into an HTML markup string. + */ + toMarkup() { + let markup = "<" + this.type; + + // Add the attributes + for (const attr in this.attributes) { + if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { + markup += " " + attr + "=\""; + markup += utils.escape(this.attributes[attr]); + markup += "\""; + } + } + + markup += ">"; + + for (let i = 0; i < this.children.length; i++) { + markup += this.children[i].toMarkup(); + } + + markup += ""; + + return markup; + } } -/** - * Sets an attribute on a MathML node. MathML depends on attributes to convey a - * semantic content, so this is used heavily. - */ -MathNode.prototype.setAttribute = function(name, value) { - this.attributes[name] = value; -}; - -/** - * Converts the math node into a MathML-namespaced DOM element. - */ -MathNode.prototype.toNode = function() { - const node = document.createElementNS( - "http://www.w3.org/1998/Math/MathML", this.type); - - for (const attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - node.setAttribute(attr, this.attributes[attr]); - } - } - - for (let i = 0; i < this.children.length; i++) { - node.appendChild(this.children[i].toNode()); - } - - return node; -}; - -/** - * Converts the math node into an HTML markup string. - */ -MathNode.prototype.toMarkup = function() { - let markup = "<" + this.type; - - // Add the attributes - for (const attr in this.attributes) { - if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) { - markup += " " + attr + "=\""; - markup += utils.escape(this.attributes[attr]); - markup += "\""; - } - } - - markup += ">"; - - for (let i = 0; i < this.children.length; i++) { - markup += this.children[i].toMarkup(); - } - - markup += ""; - - return markup; -}; - /** * This node represents a piece of text. */ -function TextNode(text) { - this.text = text; +class TextNode { + constructor(text) { + this.text = text; + } + + /** + * Converts the text node into a DOM text node. + */ + toNode() { + return document.createTextNode(this.text); + } + + /** + * Converts the text node into HTML markup (which is just the text itself). + */ + toMarkup() { + return utils.escape(this.text); + } } -/** - * Converts the text node into a DOM text node. - */ -TextNode.prototype.toNode = function() { - return document.createTextNode(this.text); -}; - -/** - * Converts the text node into HTML markup (which is just the text itself). - */ -TextNode.prototype.toMarkup = function() { - return utils.escape(this.text); -}; - module.exports = { MathNode: MathNode, TextNode: TextNode, diff --git a/src/parseData.js b/src/parseData.js index 61ebd069..5873bffd 100644 --- a/src/parseData.js +++ b/src/parseData.js @@ -15,14 +15,16 @@ * @param {Token=} lastToken last token of the input for this node, * will default to firstToken if unset */ -function ParseNode(type, value, mode, firstToken, lastToken) { - this.type = type; - this.value = value; - this.mode = mode; - if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) { - this.lexer = firstToken.lexer; - this.start = firstToken.start; - this.end = (lastToken || firstToken).end; +class ParseNode { + constructor(type, value, mode, firstToken, lastToken) { + this.type = type; + this.value = value; + this.mode = mode; + if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) { + this.lexer = firstToken.lexer; + this.start = firstToken.start; + this.end = (lastToken || firstToken).end; + } } } diff --git a/src/parseTree.js b/src/parseTree.js index a3b9121c..b0a95357 100644 --- a/src/parseTree.js +++ b/src/parseTree.js @@ -3,7 +3,7 @@ * TODO(emily): Remove this */ -const Parser = require("./Parser"); +import Parser from "./Parser"; /** * Parses an expression using a Parser, then returns the parsed result. diff --git a/test/errors-spec.js b/test/errors-spec.js index fa2818d3..a2853464 100644 --- a/test/errors-spec.js +++ b/test/errors-spec.js @@ -4,8 +4,8 @@ /* global it: false */ /* global describe: false */ -const parseTree = require("../src/parseTree"); -const Settings = require("../src/Settings"); +import parseTree from "../src/parseTree"; +import Settings from "../src/Settings"; const defaultSettings = new Settings({}); diff --git a/test/jasmine.json b/test/jasmine.json index f4c560d2..6b96cef7 100644 --- a/test/jasmine.json +++ b/test/jasmine.json @@ -5,6 +5,7 @@ "contrib/**/*[sS]pec.js" ], "helpers": [ - "helpers/**/*.js" + "helpers/**/*.js", + "node_modules/babel-core/register.js" ] } diff --git a/test/katex-spec.js b/test/katex-spec.js index bfff8804..7b218727 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -5,14 +5,14 @@ /* global it: false */ /* global describe: false */ -const buildMathML = require("../src/buildMathML"); -const buildTree = require("../src/buildTree"); -const katex = require("../katex"); -const ParseError = require("../src/ParseError"); -const parseTree = require("../src/parseTree"); -const Options = require("../src/Options"); -const Settings = require("../src/Settings"); -const Style = require("../src/Style"); +import buildMathML from "../src/buildMathML"; +import buildTree from "../src/buildTree"; +import katex from "../katex"; +import ParseError from "../src/ParseError"; +import parseTree from "../src/parseTree"; +import Options from "../src/Options"; +import Settings from "../src/Settings"; +import Style from "../src/Style"; const defaultSettings = new Settings({}); const defaultOptions = new Options({