From 4122fa2b99d57b9b43a0cec580d5783bcb8589a1 Mon Sep 17 00:00:00 2001 From: Emily Eisenberg Date: Fri, 12 Jul 2013 23:16:30 -0700 Subject: [PATCH] Rewrite the parser Summary: Make our own parser that doesn't use jison, so that we can handle funny TeX syntax, and to make it smaller. Test Plan: Make sure the tests pass with the new parser. Reviewers: alpert Reviewed By: alpert Differential Revision: http://phabricator.khanacademy.org/D3029 --- .gitignore | 1 - Lexer.js | 88 +++++++++++++++ Makefile | 12 +- Parser.js | 263 ++++++++++++++++++++++++++++++++++++++++++++ jisonify.js | 31 ------ katex.js | 72 ++++++------ lexer.js | 99 ----------------- parseTree.js | 9 +- parser.jison | 161 --------------------------- server.js | 4 - static/index.html | 2 +- test/katex-tests.js | 12 ++ 12 files changed, 414 insertions(+), 340 deletions(-) create mode 100644 Lexer.js create mode 100644 Parser.js delete mode 100644 jisonify.js delete mode 100644 lexer.js delete mode 100644 parser.jison diff --git a/.gitignore b/.gitignore index 1770453b9..e3fbd9833 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ build node_modules -parser.js diff --git a/Lexer.js b/Lexer.js new file mode 100644 index 000000000..02fea64e0 --- /dev/null +++ b/Lexer.js @@ -0,0 +1,88 @@ +// The main lexer class +function Lexer(input) { + this._input = input; +}; + +// The result of a single lex +function LexResult(type, text, position) { + this.type = type; + this.text = text; + this.position = position; +} + +// "normal" types of tokens +var normals = [ + [/^[/|@."`0-9]/, 'textord'], + [/^[a-zA-Z]/, 'mathord'], + [/^[*+-]/, 'bin'], + [/^[=<>]/, 'rel'], + [/^[,;]/, 'punct'], + [/^\^/, '^'], + [/^_/, '_'], + [/^{/, '{'], + [/^}/, '}'], + [/^[(\[]/, 'open'], + [/^[)\]?!]/, 'close'] +]; + +// Different functions +var funcs = [ + // Bin symbols + 'cdot', 'pm', 'div', + // Rel symbols + 'leq', 'geq', 'neq', 'nleq', 'ngeq', + // Open/close symbols + 'lvert', 'rvert', + // Punct symbols + 'colon', + // Spacing symbols + 'qquad', 'quad', ' ', 'space', ',', ':', ';', + // Colors + 'blue', 'orange', 'pink', 'red', 'green', 'gray', 'purple', + // Mathy functions + "arcsin", "arccos", "arctan", "arg", "cos", "cosh", "cot", "coth", "csc", + "deg", "dim", "exp", "hom", "ker", "lg", "ln", "log", "sec", "sin", "sinh", + "tan", "tanh", + // Other functions + 'dfrac', 'llap', 'rlap' +]; +// Build a regex to easily parse the functions +var anyFunc = new RegExp("^\\\\(" + funcs.join("|") + ")(?![a-zA-Z])"); + +// Lex a single token +Lexer.prototype.lex = function(pos) { + var input = this._input.slice(pos); + + // Get rid of whitespace + var whitespace = input.match(/^\s*/)[0]; + pos += whitespace.length; + input = input.slice(whitespace.length); + + // If there's no more input to parse, return an EOF token + if (input.length === 0) { + return new LexResult('EOF', null, pos); + } + + var match; + if ((match = input.match(anyFunc))) { + // If we match one of the tokens, extract the type + return new LexResult(match[1], match[0], pos + match[0].length); + } else { + // Otherwise, we look through the normal token regexes and see if it's + // one of them. + for (var i = 0; i < normals.length; i++) { + var normal = normals[i]; + + if ((match = input.match(normal[0]))) { + // If it is, return it + return new LexResult( + normal[1], match[0], pos + match[0].length); + } + } + } + + // We didn't match any of the tokens, so throw an error. + throw "Unexpected character: '" + input[0] + "' at position " + this._pos; +}; + +module.exports = Lexer; diff --git a/Makefile b/Makefile index dd994bd3c..40c20b0ff 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,16 @@ .PHONY: build copy serve clean build: build/katex.js -build/katex.js: katex.js parser.jison lexer.js - ./node_modules/.bin/browserify $< --standalone katex -t ./jisonify > $@ +compress: build/katex.min.js + @echo -n "Minified, gzipped size: " + @gzip -c $^ | wc -c + +build/katex.js: katex.js Parser.js Lexer.js + ./node_modules/.bin/browserify $< --standalone katex > $@ + +build/katex.min.js: build/katex.js + uglifyjs --mangle < $< > $@ + copy: build cp build/katex.js ../exercises/utils/katex.js diff --git a/Parser.js b/Parser.js new file mode 100644 index 000000000..9eea1ae49 --- /dev/null +++ b/Parser.js @@ -0,0 +1,263 @@ +var Lexer = require("./Lexer"); + +// Main Parser class +function Parser(options) { + this.options = options; +}; + +// Returned by the Parser.parse... functions. Stores the current results and +// the new lexer position. +function ParseResult(result, newPosition) { + this.result = result; + this.position = newPosition; +} + +// The resulting parse tree nodes of the parse tree. +function ParseNode(type, value) { + this.type = type; + this.value = value; +} + +// Checks a result to make sure it has the right type, and throws an +// appropriate error otherwise. +var expect = function(result, type) { + if (result.type !== type) { + throw "Failed parsing: Expected '" + type + "', got '" + result.type + "'"; + } +}; + +// Main parsing function, which parses an entire input. Returns either a list +// of parseNodes or null if the parse fails. +Parser.prototype.parse = function(input) { + // Make a new lexer + this.lexer = new Lexer(input); + + // Try to parse the input + var parse = this.parseInput(0); + return parse.result; +}; + +// Parses an entire input tree +Parser.prototype.parseInput = function(pos) { + // Parse an expression + var expression = this.parseExpression(pos); + // If we succeeded, make sure there's an EOF at the end + var EOF = this.lexer.lex(expression.position); + expect(EOF, 'EOF'); + return expression; +}; + +// Parses an "expression", which is a list of atoms +Parser.prototype.parseExpression = function(pos) { + // Start with a list of nodes + var expression = []; + while (true) { + // Try to parse atoms + var parse = this.parseAtom(pos); + if (parse) { + // Copy them into the list + expression.push(parse.result); + pos = parse.position; + } else { + break; + } + } + return new ParseResult(expression, pos); +}; + +// Parses a superscript expression, like "^3" +Parser.prototype.parseSuperscript = function(pos) { + // Try to parse a "^" character + var sup = this.lexer.lex(pos); + if (sup.type === "^") { + // If we got one, parse the corresponding group + var group = this.parseGroup(sup.position); + if (group) { + return group; + } else { + // Throw an error if we didn't find a group + throw "Parse error: Couldn't find group after '^'"; + } + } else { + return null; + } +}; + +// Parses a subscript expression, like "_3" +Parser.prototype.parseSubscript = function(pos) { + // Try to parse a "_" character + var sub = this.lexer.lex(pos); + if (sub.type === "_") { + // If we got one, parse the corresponding group + var group = this.parseGroup(sub.position); + if (group) { + return group; + } else { + // Throw an error if we didn't find a group + throw "Parse error: Couldn't find group after '_'"; + } + } else { + return null; + } +}; + +// Parses an atom, which consists of a nucleus, and an optional superscript and +// subscript +Parser.prototype.parseAtom = function(pos) { + // Parse the nucleus + var nucleus = this.parseGroup(pos); + if (nucleus) { + // Now, we try to parse a subscript or a superscript. If one of those + // succeeds, we then try to parse the opposite one, and depending on + // whether that succeeds, we return the correct type. + var sup, sub; + if (sup = this.parseSuperscript(nucleus.position)) { + if (sub = this.parseSubscript(sup.position)) { + return new ParseResult( + new ParseNode("supsub", + {base: nucleus.result, sup: sup.result, + sub: sub.result}), + sub.position); + } else { + return new ParseResult( + new ParseNode("sup", + {base: nucleus.result, sup: sup.result}), + sup.position); + } + } else if (sub = this.parseSubscript(nucleus.position)) { + if (sup = this.parseSuperscript(sub.position)) { + return new ParseResult( + new ParseNode("supsub", + {base: nucleus.result, sup: sup.result, + sub: sub.result}), + sup.position); + } else { + return new ParseResult( + new ParseNode("sub", + {base: nucleus.result, sub: sub.result}), + sub.position); + } + } else { + return nucleus; + } + } else { + return null; + } +} + +// Parses a group, which is either a single nucleus (like "x") or an expression +// in braces (like "{x+y}") +Parser.prototype.parseGroup = function(pos) { + var start = this.lexer.lex(pos); + // Try to parse an open brace + if (start.type === "{") { + // If we get a brace, parse an expression + var expression = this.parseExpression(start.position); + // Make sure we get a close brace + var closeBrace = this.lexer.lex(expression.position); + expect(closeBrace, "}"); + return new ParseResult( + new ParseNode("ordgroup", expression.result), + closeBrace.position); + } else { + // Otherwise, just return a nucleus + return this.parseNucleus(pos); + } +}; + +// Tests whether an element is in a list +function contains(list, elem) { + return list.indexOf(elem) !== -1; +} + +// A list of 1-argument color functions +var colorFuncs = [ + "blue", "orange", "pink", "red", "green", "gray", "purple" +]; + +// A map of elements that don't have arguments, and should simply be placed +// into a group depending on their type. The keys are the groups that items can +// be placed in, and the values are lists of element types that should be +// placed in those groups. +// +// For example, if the lexer returns something of type "colon", we should +// return a node of type "punct" +var copyFuncs = { + "textord": ["textord"], + "mathord": ["mathord"], + "bin": ["bin", "pm", "div", "cdot"], + "open": ["open", "lvert"], + "close": ["close", "rvert"], + "rel": ["rel", "leq", "geq", "neq", "nleq", "ngeq"], + "spacing": ["qquad", "quad", "space", " ", ",", ":", ";"], + "punct": ["punct", "colon"], + "namedfn": ["arcsin", "arccos", "arctan", "arg", "cos", "cosh", "cot", + "coth", "csc", "deg", "dim", "exp", "hom", "ker", "lg", "ln", "log", + "sec", "sin", "sinh", "tan", "tanh"] +}; + +// Build a list of all of the different functions in the copyFuncs list, to +// quickly check if the function should be interpreted by the map. +var funcToType = {}; +for (var type in copyFuncs) { + for (var i = 0; i < copyFuncs[type].length; i++) { + var func = copyFuncs[type][i]; + funcToType[func] = type; + } +} + +// Parses a "nucleus", which is either a single token from the tokenizer or a +// function and its arguments +Parser.prototype.parseNucleus = function(pos) { + var nucleus = this.lexer.lex(pos); + + if (contains(colorFuncs, nucleus.type)) { + // If this is a color function, parse its argument and return + var group = this.parseGroup(nucleus.position); + if (group) { + return new ParseResult( + new ParseNode("color", + {color: nucleus.type, value: group.result}), + group.position); + } else { + throw "Parse error: Expected group after '" + nucleus.text + "'"; + } + } else if (nucleus.type === "llap" || nucleus.type === "rlap") { + // If this is an llap or rlap, parse its argument and return + var group = this.parseGroup(nucleus.position); + if (group) { + return new ParseResult( + new ParseNode(nucleus.type, nucleus.text), + group.position); + } else { + throw "Parse error: Expected group after '" + nucleus.text + "'"; + } + } else if (nucleus.type === "dfrac") { + // If this is a dfrac, parse its two arguments and return + var numer = this.parseGroup(nucleus.position); + if (numer) { + var denom = this.parseGroup(numer.position); + if (denom) { + return new ParseResult( + new ParseNode("dfrac", + {numer: numer.result, denom: denom.result}), + denom.position); + } else { + throw "Parse error: Expected denominator after '\\dfrac'"; + } + } else { + throw "Parse error: Expected numerator after '\\dfrac'" + } + } else if (funcToType[nucleus.type]) { + // Otherwise if this is a no-argument function, find the type it + // corresponds to in the map and return + return new ParseResult( + new ParseNode(funcToType[nucleus.type], nucleus.text), + nucleus.position); + } else { + // Otherwise, we couldn't parse it + return null; + } +}; + +module.exports = Parser; diff --git a/jisonify.js b/jisonify.js deleted file mode 100644 index f3c3c7ff3..000000000 --- a/jisonify.js +++ /dev/null @@ -1,31 +0,0 @@ -var ebnfParser = require("ebnf-parser"); -var jison = require("jison"); -var through = require("through"); - -module.exports = function(file) { - if (!(/\.jison$/).test(file)) { - return through(); - } - - var data = ''; - return through(write, end); - - function write(buf) { - data += buf; - } - - function end() { - try { - var grammar = ebnfParser.parse(data); - var parser = new jison.Parser(grammar); - var js = parser.generate({moduleType: "js"}); - js += "\nmodule.exports = parser;"; - - this.queue(js); - this.queue(null); - } catch (e) { - // TODO(alpert): Does this do anything? (Is it useful?) - this.emit("error", e); - } - } -}; diff --git a/katex.js b/katex.js index 48bb1aee9..c9114250d 100644 --- a/katex.js +++ b/katex.js @@ -12,12 +12,10 @@ var makeSpan = function(className, children) { var span = document.createElement("span"); span.className = className || ""; - if (_.isArray(children)) { - _.each(children, function(v) { - span.appendChild(v); - }); - } else if (children) { - span.appendChild(children); + if (children) { + for (var i = 0; i < children.length; i++) { + span.appendChild(children[i]); + } } return span; @@ -25,46 +23,46 @@ var makeSpan = function(className, children) { var buildGroup = function(group, prev) { if (group.type === "mathord") { - return makeSpan("mord", mathit(group.value)); + return makeSpan("mord", [mathit(group.value)]); } else if (group.type === "textord") { - return makeSpan("mord", textit(group.value)); + return makeSpan("mord", [textit(group.value)]); } else if (group.type === "bin") { var className = "mbin"; if (prev == null || _.contains(["bin", "open", "rel"], prev.type)) { group.type = "ord"; className = "mord"; } - return makeSpan(className, textit(group.value)); + return makeSpan(className, [textit(group.value)]); } else if (group.type === "rel") { - return makeSpan("mrel", textit(group.value)); + return makeSpan("mrel", [textit(group.value)]); } else if (group.type === "sup") { - var sup = makeSpan("msup", buildExpression(group.value.sup)); - return makeSpan("mord", buildExpression(group.value.base).concat(sup)); + var sup = makeSpan("msup", [buildGroup(group.value.sup)]); + return makeSpan("mord", [buildGroup(group.value.base), sup]); } else if (group.type === "sub") { - var sub = makeSpan("msub", buildExpression(group.value.sub)); - return makeSpan("mord", buildExpression(group.value.base).concat(sub)); + var sub = makeSpan("msub", [buildGroup(group.value.sub)]); + return makeSpan("mord", [buildGroup(group.value.base), sub]); } else if (group.type === "supsub") { - var sup = makeSpan("msup", buildExpression(group.value.sup)); - var sub = makeSpan("msub", buildExpression(group.value.sub)); + var sup = makeSpan("msup", [buildGroup(group.value.sup)]); + var sub = makeSpan("msub", [buildGroup(group.value.sub)]); var supsub = makeSpan("msupsub", [sup, sub]); - return makeSpan("mord", buildExpression(group.value.base).concat(supsub)); + return makeSpan("mord", [buildGroup(group.value.base), supsub]); } else if (group.type === "open") { - return makeSpan("mopen", textit(group.value)); + return makeSpan("mopen", [textit(group.value)]); } else if (group.type === "close") { - return makeSpan("mclose", textit(group.value)); + return makeSpan("mclose", [textit(group.value)]); } else if (group.type === "dfrac") { - var numer = makeSpan("mfracnum", makeSpan("", buildExpression(group.value.numer))); - var mid = makeSpan("mfracmid", makeSpan()); - var denom = makeSpan("mfracden", buildExpression(group.value.denom)); + var numer = makeSpan("mfracnum", [makeSpan("", [buildGroup(group.value.numer)])]); + var mid = makeSpan("mfracmid", [makeSpan()]); + var denom = makeSpan("mfracden", [buildGroup(group.value.denom)]); return makeSpan("minner mfrac", [numer, mid, denom]); } else if (group.type === "color") { - return makeSpan("mord " + group.value.color, buildExpression(group.value.value)); + return makeSpan("mord " + group.value.color, [buildGroup(group.value.value)]); } else if (group.type === "spacing") { if (group.value === "\\ " || group.value === "\\space") { - return makeSpan("mord mspace", textit(group.value)); + return makeSpan("mord mspace", [textit(group.value)]); } else { var spacingClassMap = { "\\qquad": "qquad", @@ -78,18 +76,18 @@ var buildGroup = function(group, prev) { } } else if (group.type === "llap") { var inner = makeSpan("", buildExpression(group.value)); - return makeSpan("llap", inner); + return makeSpan("llap", [inner]); } else if (group.type === "rlap") { var inner = makeSpan("", buildExpression(group.value)); - return makeSpan("rlap", inner); + return makeSpan("rlap", [inner]); } else if (group.type === "punct") { - return makeSpan("mpunct", textit(group.value)); + return makeSpan("mpunct", [textit(group.value)]); } else if (group.type === "ordgroup") { return makeSpan("mord", buildExpression(group.value)); } else if (group.type === "namedfn") { - return makeSpan("mop", textit(group.value.slice(1))); + return makeSpan("mop", [textit(group.value.slice(1))]); } else { - console.log("Unknown type:", group.type); + throw "Lex error: Got group of unknown type: '" + group.type + "'"; } }; @@ -120,7 +118,7 @@ var textit = function(value) { }; var mathit = function(value) { - return makeSpan("mathit", textit(value)); + return makeSpan("mathit", [textit(value)]); }; var clearNode = function(node) { @@ -133,10 +131,16 @@ var clearNode = function(node) { var process = function(toParse, baseElem) { var tree = parseTree(toParse); - clearNode(baseElem); - _.each(buildExpression(tree), function(elem) { - baseElem.appendChild(elem); - }); + if (tree) { + clearNode(baseElem); + var expression = buildExpression(tree); + for (var i = 0; i < expression.length; i++) { + baseElem.appendChild(expression[i]); + } + return true; + } else { + return false; + } }; module.exports = { diff --git a/lexer.js b/lexer.js deleted file mode 100644 index 3e5b9eb92..000000000 --- a/lexer.js +++ /dev/null @@ -1,99 +0,0 @@ -function Lexer() { -}; - -var normals = [ - [/^[/|@."`0-9]/, 'TEXTORD'], - [/^[a-zA-Z]/, 'MATHORD'], - [/^[*+-]/, 'BIN'], - [/^[=<>]/, 'REL'], - [/^[,;]/, 'PUNCT'], - [/^\^/, '^'], - [/^_/, '_'], - [/^{/, '{'], - [/^}/, '}'], - [/^[(\[]/, 'OPEN'], - [/^[)\]?!]/, 'CLOSE'] -]; - -var funcs = [ - // Bin symbols - 'cdot', 'pm', 'div', - // Rel symbols - 'leq', 'geq', 'neq', 'nleq', 'ngeq', - // Open/close symbols - 'lvert', 'rvert', - // Punct symbols - 'colon', - // Spacing symbols - 'qquad', 'quad', ' ', 'space', ',', ':', ';', - // Colors - 'blue', 'orange', 'pink', 'red', 'green', 'gray', 'purple', - // Mathy functions - "arcsin", "arccos", "arctan", "arg", "cos", "cosh", "cot", "coth", "csc", - "deg", "dim", "exp", "hom", "ker", "lg", "ln", "log", "sec", "sin", "sinh", - "tan", "tanh", - // Other functions - 'dfrac', 'llap', 'rlap' -]; -var anyFunc = new RegExp("^\\\\(" + funcs.join("|") + ")(?![a-zA-Z])"); - -Lexer.prototype.doMatch = function(match) { - this.yytext = match; - this.yyleng = match.length; - - this.yylloc.first_column = this._pos; - this.yylloc.last_column = this._pos + match.length; - - this._pos += match.length; - this._input = this._input.slice(match.length); -}; - -Lexer.prototype.lex = function() { - // Get rid of whitespace - var whitespace = this._input.match(/^\s*/)[0]; - this._pos += whitespace.length; - this._input = this._input.slice(whitespace.length); - - if (this._input.length === 0) { - return 'EOF'; - } - - var match; - - if ((match = this._input.match(anyFunc))) { - this.doMatch(match[0]); - - if (match[1] === " ") { - return "space"; - } - return match[1]; - } else { - for (var i = 0; i < normals.length; i++) { - var normal = normals[i]; - - if ((match = this._input.match(normal[0]))) { - this.doMatch(match[0]); - return normal[1]; - } - } - } - - throw "Unexpected character: '" + this._input[0] + "' at position " + this._pos; -}; - -Lexer.prototype.setInput = function(input) { - this._input = input; - this._pos = 0; - - this.yyleng = 0; - this.yytext = ""; - this.yylineno = 0; - this.yylloc = { - first_line: 1, - first_column: 0, - last_line: 1, - last_column: 0 - }; -}; - -module.exports = new Lexer(); diff --git a/parseTree.js b/parseTree.js index 6167cd8fd..c9e36c0f9 100644 --- a/parseTree.js +++ b/parseTree.js @@ -1,10 +1,5 @@ -var parser = require("./parser.jison"); -parser.lexer = require("./lexer"); -parser.yy = { - parseError: function(str) { - throw new Error(str); - } -}; +var Parser = require("./Parser"); +var parser = new Parser({verbose: true}); var parseTree = function(toParse) { return parser.parse(toParse); diff --git a/parser.jison b/parser.jison deleted file mode 100644 index fefcc2cd7..000000000 --- a/parser.jison +++ /dev/null @@ -1,161 +0,0 @@ -/* description: Parses end executes mathematical expressions. */ - -/* operator associations and precedence */ - -%left '^' -%left '_' -%left 'ORD' -%left 'BIN' -%left SUPSUB - -%start expression - -%% /* language grammar */ - -expression - : ex 'EOF' - {return $1;} - ; - -ex - : - {$$ = [];} - | group ex - {$$ = $1.concat($2);} - | group '^' group ex - {$$ = [{type: 'sup', value: {base: $1, sup: $3}}].concat($4);} - | group '_' group ex - {$$ = [{type: 'sub', value: {base: $1, sub: $3}}].concat($4);} - | group '^' group '_' group ex %prec SUPSUB - {$$ = [{type: 'supsub', value: {base: $1, sup: $3, sub: $5}}].concat($6);} - | group '_' group '^' group ex %prec SUPSUB - {$$ = [{type: 'supsub', value: {base: $1, sup: $5, sub: $3}}].concat($6);} - ; - -group - : atom - {$$ = $1;} - | '{' ex '}' - {$$ = [{type: 'ordgroup', value: $2}];} - | func - {$$ = $1;} - ; - -func - : 'cdot' - {$$ = [{type: 'bin', value: yytext}];} - | 'pm' - {$$ = [{type: 'bin', value: yytext}];} - | 'div' - {$$ = [{type: 'bin', value: yytext}];} - | 'lvert' - {$$ = [{type: 'open', value: yytext}];} - | 'rvert' - {$$ = [{type: 'close', value: yytext}];} - | 'leq' - {$$ = [{type: 'rel', value: yytext}];} - | 'geq' - {$$ = [{type: 'rel', value: yytext}];} - | 'neq' - {$$ = [{type: 'rel', value: yytext}];} - | 'nleq' - {$$ = [{type: 'rel', value: yytext}];} - | 'ngeq' - {$$ = [{type: 'rel', value: yytext}];} - | 'qquad' - {$$ = [{type: 'spacing', value: yytext}];} - | 'quad' - {$$ = [{type: 'spacing', value: yytext}];} - | 'space' - {$$ = [{type: 'spacing', value: yytext}];} - | ',' - {$$ = [{type: 'spacing', value: yytext}];} - | ':' - {$$ = [{type: 'spacing', value: yytext}];} - | ';' - {$$ = [{type: 'spacing', value: yytext}];} - | 'colon' - {$$ = [{type: 'punct', value: yytext}];} - | 'blue' group - {$$ = [{type: 'color', value: {color: 'blue', value: $2}}];} - | 'orange' group - {$$ = [{type: 'color', value: {color: 'orange', value: $2}}];} - | 'pink' group - {$$ = [{type: 'color', value: {color: 'pink', value: $2}}];} - | 'red' group - {$$ = [{type: 'color', value: {color: 'red', value: $2}}];} - | 'green' group - {$$ = [{type: 'color', value: {color: 'green', value: $2}}];} - | 'gray' group - {$$ = [{type: 'color', value: {color: 'gray', value: $2}}];} - | 'purple' group - {$$ = [{type: 'color', value: {color: 'purple', value: $2}}];} - | 'dfrac' group group - {$$ = [{type: 'dfrac', value: {numer: $2, denom: $3}}];} - | 'llap' group - {$$ = [{type: 'llap', value: $2}];} - | 'rlap' group - {$$ = [{type: 'rlap', value: $2}];} - | 'arcsin' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'arccos' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'arctan' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'arg' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'cos' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'cosh' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'cot' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'coth' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'csc' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'deg' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'dim' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'exp' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'hom' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'ker' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'lg' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'ln' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'log' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'sec' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'sin' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'sinh' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'tan' - {$$ = [{type: 'namedfn', value: yytext}];} - | 'tanh' - {$$ = [{type: 'namedfn', value: yytext}];} - ; - -atom - : 'TEXTORD' - {$$ = [{type: 'textord', value: yytext}];} - | 'MATHORD' - {$$ = [{type: 'mathord', value: yytext}];} - | 'BIN' - {$$ = [{type: 'bin', value: yytext}];} - | 'REL' - {$$ = [{type: 'rel', value: yytext}];} - | 'PUNCT' - {$$ = [{type: 'punct', value: yytext}];} - | 'OPEN' - {$$ = [{type: 'open', value: yytext}];} - | 'CLOSE' - {$$ = [{type: 'close', value: yytext}];} - ; - diff --git a/server.js b/server.js index e53320a4e..a7ed3b29c 100644 --- a/server.js +++ b/server.js @@ -3,8 +3,6 @@ var path = require("path"); var browserify = require("browserify"); var express = require("express"); -var jisonify = require("./jisonify"); - var app = express(); app.use(express.logger()); @@ -12,7 +10,6 @@ app.use(express.logger()); app.get("/katex.js", function(req, res, next) { var b = browserify(); b.add("./katex"); - b.transform(jisonify); var stream = b.bundle({standalone: "katex"}); @@ -28,7 +25,6 @@ app.get("/katex.js", function(req, res, next) { app.get("/test/katex-tests.js", function(req, res, next) { var b = browserify(); b.add("./test/katex-tests"); - b.transform(jisonify); var stream = b.bundle({}); diff --git a/static/index.html b/static/index.html index d72fefd02..ea278133e 100644 --- a/static/index.html +++ b/static/index.html @@ -9,7 +9,7 @@ - +
diff --git a/test/katex-tests.js b/test/katex-tests.js index 158fe3112..66e0a5175 100644 --- a/test/katex-tests.js +++ b/test/katex-tests.js @@ -207,6 +207,18 @@ describe("A subscript and superscript parser", function() { expect(parseA).toEqual(parseB); }); + it("should not parse x^x^x", function() { + expect(function() { + parseTree("x^x^x"); + }).toThrow(); + }); + + it("should not parse x_x_x", function() { + expect(function() { + parseTruee("x_x_x"); + }).toThrow(); + }); + it("should work correctly with {}s", function() { expect(function() { parseTree("x^{2+3}");