Web Inspector: ES6: JavaScript syntax highlighting and recognition of for..of
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / External / CodeMirror / javascript.js
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 });