1 // TODO actually recognize syntax of TypeScript constructs 2 3 CodeMirror.defineMode("javascript", function(config, parserConfig) { 4 var indentUnit = config.indentUnit; 5 var statementIndent = parserConfig.statementIndent; 6 var jsonMode = parserConfig.json; 7 var isTS = parserConfig.typescript; 8 9 // Tokenizer 10 11 var keywords = function(){ 12 function kw(type) {return {type: type, style: "keyword"};} 13 var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); 14 var operator = kw("operator"), atom = {type: "atom", style: "atom"}; 15 16 var jsKeywords = { 17 "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, 18 "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, 19 "var": kw("var"), "const": kw("var"), "let": kw("var"), 20 "function": kw("function"), "catch": kw("catch"), 21 "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), 22 "in": operator, "typeof": operator, "instanceof": operator, 23 "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, 24 "this": kw("this"), "module": kw("module"), "class": kw("class"), "super": kw("atom"), 25 "yield": C, "export": kw("export"), "import": kw("import"), "extends": C 26 }; 27 28 // Extend the 'normal' keywords with the TypeScript language extensions 29 if (isTS) { 30 var type = {type: "variable", style: "variable-3"}; 31 var tsKeywords = { 32 // object-like things 33 "interface": kw("interface"), 34 "extends": kw("extends"), 35 "constructor": kw("constructor"), 36 37 // scope modifiers 38 "public": kw("public"), 39 "private": kw("private"), 40 "protected": kw("protected"), 41 "static": kw("static"), 42 43 // types 44 "string": type, "number": type, "bool": type, "any": type 45 }; 46 47 for (var attr in tsKeywords) { 48 jsKeywords[attr] = tsKeywords[attr]; 49 } 50 } 51 52 return jsKeywords; 53 }(); 54 55 var isOperatorChar = /[+\-*&%=<>!?|~^]/; 56 57 function nextUntilUnescaped(stream, end) { 58 var escaped = false, next; 59 while ((next = stream.next()) != null) { 60 if (next == end && !escaped) 61 return false; 62 escaped = !escaped && next == "\\"; 63 } 64 return escaped; 65 } 66 67 // Used as scratch variables to communicate multiple values without 68 // consing up tons of objects. 69 var type, content; 70 function ret(tp, style, cont) { 71 type = tp; content = cont; 72 return style; 73 } 74 function tokenBase(stream, state) { 75 var ch = stream.next(); 76 if (ch == '"' || ch == "'") { 77 state.tokenize = tokenString(ch); 78 return state.tokenize(stream, state); 79 } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) { 80 return ret("number", "number"); 81 } else if (ch == "." && stream.match("..")) { 82 return ret("spread", "meta"); 83 } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { 84 return ret(ch); 85 } else if (ch == "=" && stream.eat(">")) { 86 return ret("=>"); 87 } else if (ch == "0" && stream.eat(/x/i)) { 88 stream.eatWhile(/[\da-f]/i); 89 return ret("number", "number"); 90 } else if (/\d/.test(ch)) { 91 stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); 92 return ret("number", "number"); 93 } else if (ch == "/") { 94 if (stream.eat("*")) { 95 state.tokenize = tokenComment; 96 return tokenComment(stream, state); 97 } else if (stream.eat("/")) { 98 stream.skipToEnd(); 99 return ret("comment", "comment"); 100 } else if (state.lastType == "operator" || state.lastType == "keyword c" || 101 state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) { 102 nextUntilUnescaped(stream, "/"); 103 stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla 104 return ret("regexp", "string-2"); 105 } else { 106 stream.eatWhile(isOperatorChar); 107 return ret("operator", null, stream.current()); 108 } 109 } else if (ch == "`") { 110 state.tokenize = tokenQuasi; 111 return tokenQuasi(stream, state); 112 } else if (ch == "#") { 113 stream.skipToEnd(); 114 return ret("error", "error"); 115 } else if (isOperatorChar.test(ch)) { 116 stream.eatWhile(isOperatorChar); 117 return ret("operator", null, stream.current()); 118 } else { 119 stream.eatWhile(/[\w\$_]/); 120 var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; 121 return (known && state.lastType != ".") ? ret(known.type, known.style, word) : 122 ret("variable", "variable", word); 123 } 124 } 125 126 function tokenString(quote) { 127 return function(stream, state) { 128 if (!nextUntilUnescaped(stream, quote)) 129 state.tokenize = tokenBase; 130 return ret("string", "string"); 131 }; 132 } 133 134 function tokenComment(stream, state) { 135 var maybeEnd = false, ch; 136 while (ch = stream.next()) { 137 if (ch == "/" && maybeEnd) { 138 state.tokenize = tokenBase; 139 break; 140 } 141 maybeEnd = (ch == "*"); 142 } 143 return ret("comment", "comment"); 144 } 145 146 function tokenQuasi(stream, state) { 147 var escaped = false, next; 148 while ((next = stream.next()) != null) { 149 if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { 150 state.tokenize = tokenBase; 151 break; 152 } 153 escaped = !escaped && next == "\\"; 154 } 155 return ret("quasi", "string-2", stream.current()); 156 } 157 158 var brackets = "([{}])"; 159 // This is a crude lookahead trick to try and notice that we're 160 // parsing the argument patterns for a fat-arrow function before we 161 // actually hit the arrow token. It only works if the arrow is on 162 // the same line as the arguments and there's no strange noise 163 // (comments) in between. Fallback is to only notice when we hit the 164 // arrow, and not declare the arguments as locals for the arrow 165 // body. 166 function findFatArrow(stream, state) { 167 if (state.fatArrowAt) state.fatArrowAt = null; 168 var arrow = stream.string.indexOf("=>", stream.start); 169 if (arrow < 0) return; 170 171 var depth = 0, sawSomething = false; 172 for (var pos = arrow - 1; pos >= 0; --pos) { 173 var ch = stream.string.charAt(pos); 174 var bracket = brackets.indexOf(ch); 175 if (bracket >= 0 && bracket < 3) { 176 if (!depth) { ++pos; break; } 177 if (--depth == 0) break; 178 } else if (bracket >= 3 && bracket < 6) { 179 ++depth; 180 } else if (/[$\w]/.test(ch)) { 181 sawSomething = true; 182 } else if (sawSomething && !depth) { 183 ++pos; 184 break; 185 } 186 } 187 if (sawSomething && !depth) state.fatArrowAt = pos; 188 } 189 190 // Parser 191 192 var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true}; 193 194 function JSLexical(indented, column, type, align, prev, info) { 195 this.indented = indented; 196 this.column = column; 197 this.type = type; 198 this.prev = prev; 199 this.info = info; 200 if (align != null) this.align = align; 201 } 202 203 function inScope(state, varname) { 204 for (var v = state.localVars; v; v = v.next) 205 if (v.name == varname) return true; 206 for (var cx = state.context; cx; cx = cx.prev) { 207 for (var v = cx.vars; v; v = v.next) 208 if (v.name == varname) return true; 209 } 210 } 211 212 function parseJS(state, style, type, content, stream) { 213 var cc = state.cc; 214 // Communicate our context to the combinators. 215 // (Less wasteful than consing up a hundred closures on every call.) 216 cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; 217 218 if (!state.lexical.hasOwnProperty("align")) 219 state.lexical.align = true; 220 221 while(true) { 222 var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; 223 if (combinator(type, content)) { 224 while(cc.length && cc[cc.length - 1].lex) 225 cc.pop()(); 226 if (cx.marked) return cx.marked; 227 if (type == "variable" && inScope(state, content)) return "variable-2"; 228 return style; 229 } 230 } 231 } 232 233 // Combinator utils 234 235 var cx = {state: null, column: null, marked: null, cc: null}; 236 function pass() { 237 for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); 238 } 239 function cont() { 240 pass.apply(null, arguments); 241 return true; 242 } 243 function register(varname) { 244 function inList(list) { 245 for (var v = list; v; v = v.next) 246 if (v.name == varname) return true; 247 return false; 248 } 249 var state = cx.state; 250 if (state.context) { 251 cx.marked = "def"; 252 if (inList(state.localVars)) return; 253 state.localVars = {name: varname, next: state.localVars}; 254 } else { 255 if (inList(state.globalVars)) return; 256 if (parserConfig.globalVars) 257 state.globalVars = {name: varname, next: state.globalVars}; 258 } 259 } 260 261 // Combinators 262 263 var defaultVars = {name: "this", next: {name: "arguments"}}; 264 function pushcontext() { 265 cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; 266 cx.state.localVars = defaultVars; 267 } 268 function popcontext() { 269 cx.state.localVars = cx.state.context.vars; 270 cx.state.context = cx.state.context.prev; 271 } 272 function pushlex(type, info) { 273 var result = function() { 274 var state = cx.state, indent = state.indented; 275 if (state.lexical.type == "stat") indent = state.lexical.indented; 276 state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); 277 }; 278 result.lex = true; 279 return result; 280 } 281 function poplex() { 282 var state = cx.state; 283 if (state.lexical.prev) { 284 if (state.lexical.type == ")") 285 state.indented = state.lexical.indented; 286 state.lexical = state.lexical.prev; 287 } 288 } 289 poplex.lex = true; 290 291 function expect(wanted) { 292 return function(type) { 293 if (type == wanted) return cont(); 294 else if (wanted == ";") return pass(); 295 else return cont(arguments.callee); 296 }; 297 } 298 299 function statement(type, value) { 300 if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex); 301 if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex); 302 if (type == "keyword b") return cont(pushlex("form"), statement, poplex); 303 if (type == "{") return cont(pushlex("}"), block, poplex); 304 if (type == ";") return cont(); 305 if (type == "if") return cont(pushlex("form"), expression, statement, poplex, maybeelse); 306 if (type == "function") return cont(functiondef); 307 if (type == "for") return cont(pushlex("form"), forspec, poplex, statement, poplex); 308 if (type == "variable") return cont(pushlex("stat"), maybelabel); 309 if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), 310 block, poplex, poplex); 311 if (type == "case") return cont(expression, expect(":")); 312 if (type == "default") return cont(expect(":")); 313 if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), 314 statement, poplex, popcontext); 315 if (type == "module") return cont(pushlex("form"), pushcontext, afterModule, popcontext, poplex); 316 if (type == "class") return cont(pushlex("form"), className, objlit, poplex); 317 if (type == "export") return cont(pushlex("form"), afterExport, poplex); 318 if (type == "import") return cont(pushlex("form"), afterImport, poplex); 319 return pass(pushlex("stat"), expression, expect(";"), poplex); 320 } 321 function expression(type) { 322 return expressionInner(type, false); 323 } 324 function expressionNoComma(type) { 325 return expressionInner(type, true); 326 } 327 function expressionInner(type, noComma) { 328 if (cx.state.fatArrowAt == cx.stream.start) { 329 var body = noComma ? arrowBodyNoComma : arrowBody; 330 if (type == "(") return cont(pushcontext, commasep(pattern, ")"), expect("=>"), body, popcontext); 331 else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); 332 } 333 334 var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; 335 if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); 336 if (type == "function") return cont(functiondef); 337 if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression); 338 if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop); 339 if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); 340 if (type == "[") return cont(pushlex("]"), expressionNoComma, maybeArrayComprehension, poplex, maybeop); 341 if (type == "{") return cont(commasep(objprop, "}"), maybeop); 342 return cont(); 343 } 344 function maybeexpression(type) { 345 if (type.match(/[;\}\)\],]/)) return pass(); 346 return pass(expression); 347 } 348 function maybeexpressionNoComma(type) { 349 if (type.match(/[;\}\)\],]/)) return pass(); 350 return pass(expressionNoComma); 351 } 352 353 function maybeoperatorComma(type, value) { 354 if (type == ",") return cont(expression); 355 return maybeoperatorNoComma(type, value, false); 356 } 357 function maybeoperatorNoComma(type, value, noComma) { 358 var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; 359 var expr = noComma == false ? expression : expressionNoComma; 360 if (value == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); 361 if (type == "operator") { 362 if (/\+\+|--/.test(value)) return cont(me); 363 if (value == "?") return cont(expression, expect(":"), expr); 364 return cont(expr); 365 } 366 if (type == "quasi") { cx.cc.push(me); return quasi(value); } 367 if (type == ";") return; 368 if (type == "(") return cont(commasep(expressionNoComma, ")", "call"), me); 369 if (type == ".") return cont(property, me); 370 if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); 371 } 372 function quasi(value) { 373 if (!value) debugger; 374 if (value.slice(value.length - 2) != "${") return cont(); 375 return cont(expression, continueQuasi); 376 } 377 function continueQuasi(type) { 378 if (type == "}") { 379 cx.marked = "string-2"; 380 cx.state.tokenize = tokenQuasi; 381 return cont(); 382 } 383 } 384 function arrowBody(type) { 385 findFatArrow(cx.stream, cx.state); 386 if (type == "{") return pass(statement); 387 return pass(expression); 388 } 389 function arrowBodyNoComma(type) { 390 findFatArrow(cx.stream, cx.state); 391 if (type == "{") return pass(statement); 392 return pass(expressionNoComma); 393 } 394 function maybelabel(type) { 395 if (type == ":") return cont(poplex, statement); 396 return pass(maybeoperatorComma, expect(";"), poplex); 397 } 398 function property(type) { 399 if (type == "variable") {cx.marked = "property"; return cont();} 400 } 401 function objprop(type, value) { 402 if (type == "variable") { 403 cx.marked = "property"; 404 if (value == "get" || value == "set") return cont(getterSetter); 405 } else if (type == "number" || type == "string") { 406 cx.marked = type + " property"; 407 } else if (type == "[") { 408 return cont(expression, expect("]"), afterprop); 409 } 410 if (atomicTypes.hasOwnProperty(type)) return cont(afterprop); 411 } 412 function getterSetter(type) { 413 if (type != "variable") return pass(afterprop); 414 cx.marked = "property"; 415 return cont(functiondef); 416 } 417 function afterprop(type) { 418 if (type == ":") return cont(expressionNoComma); 419 if (type == "(") return pass(functiondef); 420 } 421 function commasep(what, end, info) { 422 function proceed(type) { 423 if (type == ",") { 424 var lex = cx.state.lexical; 425 if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; 426 return cont(what, proceed); 427 } 428 if (type == end) return cont(); 429 return cont(expect(end)); 430 } 431 return function(type) { 432 if (type == end) return cont(); 433 if (info === false) return pass(what, proceed); 434 return pass(pushlex(end, info), what, proceed, poplex); 435 }; 436 } 437 function block(type) { 438 if (type == "}") return cont(); 439 return pass(statement, block); 440 } 441 function maybetype(type) { 442 if (isTS && type == ":") return cont(typedef); 443 } 444 function typedef(type) { 445 if (type == "variable"){cx.marked = "variable-3"; return cont();} 446 } 447 function vardef() { 448 return pass(pattern, maybetype, maybeAssign, vardefCont); 449 } 450 function pattern(type, value) { 451 if (type == "variable") { register(value); return cont(); } 452 if (type == "[") return cont(commasep(pattern, "]")); 453 if (type == "{") return cont(commasep(proppattern, "}")); 454 } 455 function proppattern(type, value) { 456 if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { 457 register(value); 458 return cont(maybeAssign); 459 } 460 if (type == "variable") cx.marked = "property"; 461 return cont(expect(":"), pattern, maybeAssign); 462 } 463 function maybeAssign(_type, value) { 464 if (value == "=") return cont(expressionNoComma); 465 } 466 function vardefCont(type) { 467 if (type == ",") return cont(vardef); 468 } 469 function maybeelse(type, value) { 470 if (type == "keyword b" && value == "else") return cont(pushlex("form"), statement, poplex); 471 } 472 function forspec(type) { 473 if (type == "(") return cont(pushlex(")"), forspec1, expect(")")); 474 } 475 function forspec1(type) { 476 if (type == "var") return cont(vardef, expect(";"), forspec2); 477 if (type == ";") return cont(forspec2); 478 if (type == "variable") return cont(formaybeinof); 479 return pass(expression, expect(";"), forspec2); 480 } 481 function formaybeinof(_type, value) { 482 if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } 483 return cont(maybeoperatorComma, forspec2); 484 } 485 function forspec2(type, value) { 486 if (type == ";") return cont(forspec3); 487 if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } 488 return pass(expression, expect(";"), forspec3); 489 } 490 function forspec3(type) { 491 if (type != ")") cont(expression); 492 } 493 function functiondef(type, value) { 494 if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} 495 if (type == "variable") {register(value); return cont(functiondef);} 496 if (type == "(") return cont(pushcontext, commasep(funarg, ")"), statement, popcontext); 497 } 498 function funarg(type) { 499 if (type == "spread") return cont(funarg); 500 return pass(pattern, maybetype); 501 } 502 function className(type, value) { 503 if (type == "variable") {register(value); return cont(classNameAfter);} 504 } 505 function classNameAfter(_type, value) { 506 if (value == "extends") return cont(expression); 507 } 508 function objlit(type) { 509 if (type == "{") return cont(commasep(objprop, "}")); 510 } 511 function afterModule(type, value) { 512 if (type == "string") return cont(statement); 513 if (type == "variable") { register(value); return cont(maybeFrom); } 514 } 515 function afterExport(_type, value) { 516 if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } 517 if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } 518 return pass(statement); 519 } 520 function afterImport(type) { 521 if (type == "string") return cont(); 522 return pass(importSpec, maybeFrom); 523 } 524 function importSpec(type, value) { 525 if (type == "{") return cont(commasep(importSpec, "}")); 526 if (type == "variable") register(value); 527 return cont(); 528 } 529 function maybeFrom(_type, value) { 530 if (value == "from") { cx.marked = "keyword"; return cont(expression); } 531 } 532 function maybeArrayComprehension(type) { 533 if (type == "for") return pass(comprehension); 534 if (type == ",") return cont(commasep(expressionNoComma, "]", false)); 535 return pass(commasep(expressionNoComma, "]", false)); 536 } 537 function comprehension(type) { 538 if (type == "for") return cont(forspec, comprehension); 539 if (type == "if") return cont(expression, comprehension); 540 } 541 542 // Interface 543 544 return { 545 startState: function(basecolumn) { 546 var state = { 547 tokenize: tokenBase, 548 lastType: "sof", 549 cc: [], 550 lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), 551 localVars: parserConfig.localVars, 552 context: parserConfig.localVars && {vars: parserConfig.localVars}, 553 indented: 0 554 }; 555 if (parserConfig.globalVars) state.globalVars = parserConfig.globalVars; 556 return state; 557 }, 558 559 token: function(stream, state) { 560 if (stream.sol()) { 561 if (!state.lexical.hasOwnProperty("align")) 562 state.lexical.align = false; 563 state.indented = stream.indentation(); 564 findFatArrow(stream, state); 565 } 566 if (state.tokenize != tokenComment && stream.eatSpace()) return null; 567 var style = state.tokenize(stream, state); 568 if (type == "comment") return style; 569 state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; 570 return parseJS(state, style, type, content, stream); 571 }, 572 573 indent: function(state, textAfter) { 574 if (state.tokenize == tokenComment) return CodeMirror.Pass; 575 if (state.tokenize != tokenBase) return 0; 576 var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical; 577 // Kludge to prevent 'maybelse' from blocking lexical scope pops 578 for (var i = state.cc.length - 1; i >= 0; --i) { 579 var c = state.cc[i]; 580 if (c == poplex) lexical = lexical.prev; 581 else if (c != maybeelse) break; 582 } 583 if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev; 584 if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") 585 lexical = lexical.prev; 586 var type = lexical.type, closing = firstChar == type; 587 588 if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0); 589 else if (type == "form" && firstChar == "{") return lexical.indented; 590 else if (type == "form") return lexical.indented + indentUnit; 591 else if (type == "stat") 592 return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? statementIndent || indentUnit : 0); 593 else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) 594 return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); 595 else if (lexical.align) return lexical.column + (closing ? 0 : 1); 596 else return lexical.indented + (closing ? 0 : indentUnit); 597 }, 598 599 electricChars: ":{}", 600 blockCommentStart: jsonMode ? null : "/*", 601 blockCommentEnd: jsonMode ? null : "*/", 602 lineComment: jsonMode ? null : "//", 603 fold: "brace", 604 605 helperType: jsonMode ? "json" : "javascript", 606 jsonMode: jsonMode 607 }; 608 }); 609 610 CodeMirror.defineMIME("text/javascript", "javascript"); 611 CodeMirror.defineMIME("text/ecmascript", "javascript"); 612 CodeMirror.defineMIME("application/javascript", "javascript"); 613 CodeMirror.defineMIME("application/ecmascript", "javascript"); 614 CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); 615 CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); 616 CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); 617 CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); 618