UNPKG

18.9 kBJavaScriptView Raw
1// CodeMirror, copyright (c) by Marijn Haverbeke and others
2// Distributed under an MIT license: https://codemirror.net/LICENSE
3
4(function(mod) {
5 if (typeof exports == "object" && typeof module == "object") // CommonJS
6 mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"));
7 else if (typeof define == "function" && define.amd) // AMD
8 define(["../../lib/codemirror", "../htmlmixed/htmlmixed"], mod);
9 else // Plain browser env
10 mod(CodeMirror);
11})(function(CodeMirror) {
12 "use strict";
13
14 var paramData = { noEndTag: true, soyState: "param-def" };
15 var tags = {
16 "alias": { noEndTag: true },
17 "delpackage": { noEndTag: true },
18 "namespace": { noEndTag: true, soyState: "namespace-def" },
19 "@param": paramData,
20 "@param?": paramData,
21 "@inject": paramData,
22 "@inject?": paramData,
23 "@state": paramData,
24 "template": { soyState: "templ-def", variableScope: true},
25 "literal": { },
26 "msg": {},
27 "fallbackmsg": { noEndTag: true, reduceIndent: true},
28 "select": {},
29 "plural": {},
30 "let": { soyState: "var-def" },
31 "if": {},
32 "elseif": { noEndTag: true, reduceIndent: true},
33 "else": { noEndTag: true, reduceIndent: true},
34 "switch": {},
35 "case": { noEndTag: true, reduceIndent: true},
36 "default": { noEndTag: true, reduceIndent: true},
37 "foreach": { variableScope: true, soyState: "var-def" },
38 "ifempty": { noEndTag: true, reduceIndent: true},
39 "for": { variableScope: true, soyState: "var-def" },
40 "call": { soyState: "templ-ref" },
41 "param": { soyState: "param-ref"},
42 "print": { noEndTag: true },
43 "deltemplate": { soyState: "templ-def", variableScope: true},
44 "delcall": { soyState: "templ-ref" },
45 "log": {},
46 "element": { variableScope: true },
47 };
48
49 var indentingTags = Object.keys(tags).filter(function(tag) {
50 return !tags[tag].noEndTag || tags[tag].reduceIndent;
51 });
52
53 CodeMirror.defineMode("soy", function(config) {
54 var textMode = CodeMirror.getMode(config, "text/plain");
55 var modes = {
56 html: CodeMirror.getMode(config, {name: "text/html", multilineTagIndentFactor: 2, multilineTagIndentPastTag: false}),
57 attributes: textMode,
58 text: textMode,
59 uri: textMode,
60 trusted_resource_uri: textMode,
61 css: CodeMirror.getMode(config, "text/css"),
62 js: CodeMirror.getMode(config, {name: "text/javascript", statementIndent: 2 * config.indentUnit})
63 };
64
65 function last(array) {
66 return array[array.length - 1];
67 }
68
69 function tokenUntil(stream, state, untilRegExp) {
70 if (stream.sol()) {
71 for (var indent = 0; indent < state.indent; indent++) {
72 if (!stream.eat(/\s/)) break;
73 }
74 if (indent) return null;
75 }
76 var oldString = stream.string;
77 var match = untilRegExp.exec(oldString.substr(stream.pos));
78 if (match) {
79 // We don't use backUp because it backs up just the position, not the state.
80 // This uses an undocumented API.
81 stream.string = oldString.substr(0, stream.pos + match.index);
82 }
83 var result = stream.hideFirstChars(state.indent, function() {
84 var localState = last(state.localStates);
85 return localState.mode.token(stream, localState.state);
86 });
87 stream.string = oldString;
88 return result;
89 }
90
91 function contains(list, element) {
92 while (list) {
93 if (list.element === element) return true;
94 list = list.next;
95 }
96 return false;
97 }
98
99 function prepend(list, element) {
100 return {
101 element: element,
102 next: list
103 };
104 }
105
106 function popcontext(state) {
107 if (!state.context) return;
108 if (state.context.scope) {
109 state.variables = state.context.scope;
110 }
111 state.context = state.context.previousContext;
112 }
113
114 // Reference a variable `name` in `list`.
115 // Let `loose` be truthy to ignore missing identifiers.
116 function ref(list, name, loose) {
117 return contains(list, name) ? "variable-2" : (loose ? "variable" : "variable-2 error");
118 }
119
120 // Data for an open soy tag.
121 function Context(previousContext, tag, scope) {
122 this.previousContext = previousContext;
123 this.tag = tag;
124 this.kind = null;
125 this.scope = scope;
126 }
127
128 function expression(stream, state) {
129 var match;
130 if (stream.match(/[[]/)) {
131 state.soyState.push("list-literal");
132 state.lookupVariables = false;
133 return null;
134 } else if (stream.match(/map\b/)) {
135 state.soyState.push("map-literal");
136 return "keyword";
137 } else if (stream.match(/record\b/)) {
138 state.soyState.push("record-literal");
139 return "keyword";
140 } else if (stream.match(/([\w]+)(?=\()/)) {
141 return "variable callee";
142 } else if (match = stream.match(/^["']/)) {
143 state.soyState.push("string");
144 state.quoteKind = match[0];
145 return "string";
146 } else if (stream.match(/^[(]/)) {
147 state.soyState.push("open-parentheses");
148 return null;
149 } else if (stream.match(/(null|true|false)(?!\w)/) ||
150 stream.match(/0x([0-9a-fA-F]{2,})/) ||
151 stream.match(/-?([0-9]*[.])?[0-9]+(e[0-9]*)?/)) {
152 return "atom";
153 } else if (stream.match(/(\||[+\-*\/%]|[=!]=|\?:|[<>]=?)/)) {
154 // Tokenize filter, binary, null propagator, and equality operators.
155 return "operator";
156 } else if (match = stream.match(/^\$([\w]+)/)) {
157 return ref(state.variables, match[1], !state.lookupVariables);
158 } else if (match = stream.match(/^\w+/)) {
159 return /^(?:as|and|or|not|in|if)$/.test(match[0]) ? "keyword" : null;
160 }
161
162 stream.next();
163 return null;
164 }
165
166 return {
167 startState: function() {
168 return {
169 soyState: [],
170 variables: prepend(null, 'ij'),
171 scopes: null,
172 indent: 0,
173 quoteKind: null,
174 context: null,
175 lookupVariables: true, // Is unknown variables considered an error
176 localStates: [{
177 mode: modes.html,
178 state: CodeMirror.startState(modes.html)
179 }]
180 };
181 },
182
183 copyState: function(state) {
184 return {
185 tag: state.tag, // Last seen Soy tag.
186 soyState: state.soyState.concat([]),
187 variables: state.variables,
188 context: state.context,
189 indent: state.indent, // Indentation of the following line.
190 quoteKind: state.quoteKind,
191 lookupVariables: state.lookupVariables,
192 localStates: state.localStates.map(function(localState) {
193 return {
194 mode: localState.mode,
195 state: CodeMirror.copyState(localState.mode, localState.state)
196 };
197 })
198 };
199 },
200
201 token: function(stream, state) {
202 var match;
203
204 switch (last(state.soyState)) {
205 case "comment":
206 if (stream.match(/^.*?\*\//)) {
207 state.soyState.pop();
208 } else {
209 stream.skipToEnd();
210 }
211 if (!state.context || !state.context.scope) {
212 var paramRe = /@param\??\s+(\S+)/g;
213 var current = stream.current();
214 for (var match; (match = paramRe.exec(current)); ) {
215 state.variables = prepend(state.variables, match[1]);
216 }
217 }
218 return "comment";
219
220 case "string":
221 var match = stream.match(/^.*?(["']|\\[\s\S])/);
222 if (!match) {
223 stream.skipToEnd();
224 } else if (match[1] == state.quoteKind) {
225 state.quoteKind = null;
226 state.soyState.pop();
227 }
228 return "string";
229 }
230
231 if (!state.soyState.length || last(state.soyState) != "literal") {
232 if (stream.match(/^\/\*/)) {
233 state.soyState.push("comment");
234 return "comment";
235 } else if (stream.match(stream.sol() ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) {
236 return "comment";
237 }
238 }
239
240 switch (last(state.soyState)) {
241 case "templ-def":
242 if (match = stream.match(/^\.?([\w]+(?!\.[\w]+)*)/)) {
243 state.soyState.pop();
244 return "def";
245 }
246 stream.next();
247 return null;
248
249 case "templ-ref":
250 if (match = stream.match(/(\.?[a-zA-Z_][a-zA-Z_0-9]+)+/)) {
251 state.soyState.pop();
252 // If the first character is '.', it can only be a local template.
253 if (match[0][0] == '.') {
254 return "variable-2"
255 }
256 // Otherwise
257 return "variable";
258 }
259 stream.next();
260 return null;
261
262 case "namespace-def":
263 if (match = stream.match(/^\.?([\w\.]+)/)) {
264 state.soyState.pop();
265 return "variable";
266 }
267 stream.next();
268 return null;
269
270 case "param-def":
271 if (match = stream.match(/^\w+/)) {
272 state.variables = prepend(state.variables, match[0]);
273 state.soyState.pop();
274 state.soyState.push("param-type");
275 return "def";
276 }
277 stream.next();
278 return null;
279
280 case "param-ref":
281 if (match = stream.match(/^\w+/)) {
282 state.soyState.pop();
283 return "property";
284 }
285 stream.next();
286 return null;
287
288 case "open-parentheses":
289 if (stream.match(/[)]/)) {
290 state.soyState.pop();
291 return null;
292 }
293 return expression(stream, state);
294
295 case "param-type":
296 var peekChar = stream.peek();
297 if ("}]=>,".indexOf(peekChar) != -1) {
298 state.soyState.pop();
299 return null;
300 } else if (peekChar == "[") {
301 state.soyState.push('param-type-record');
302 return null;
303 } else if (peekChar == "<") {
304 state.soyState.push('param-type-parameter');
305 return null;
306 } else if (match = stream.match(/^([\w]+|[?])/)) {
307 return "type";
308 }
309 stream.next();
310 return null;
311
312 case "param-type-record":
313 var peekChar = stream.peek();
314 if (peekChar == "]") {
315 state.soyState.pop();
316 return null;
317 }
318 if (stream.match(/^\w+/)) {
319 state.soyState.push('param-type');
320 return "property";
321 }
322 stream.next();
323 return null;
324
325 case "param-type-parameter":
326 if (stream.match(/^[>]/)) {
327 state.soyState.pop();
328 return null;
329 }
330 if (stream.match(/^[<,]/)) {
331 state.soyState.push('param-type');
332 return null;
333 }
334 stream.next();
335 return null;
336
337 case "var-def":
338 if (match = stream.match(/^\$([\w]+)/)) {
339 state.variables = prepend(state.variables, match[1]);
340 state.soyState.pop();
341 return "def";
342 }
343 stream.next();
344 return null;
345
346 case "record-literal":
347 if (stream.match(/^[)]/)) {
348 state.soyState.pop();
349 return null;
350 }
351 if (stream.match(/[(,]/)) {
352 state.soyState.push("map-value")
353 state.soyState.push("record-key")
354 return null;
355 }
356 stream.next()
357 return null;
358
359 case "map-literal":
360 if (stream.match(/^[)]/)) {
361 state.soyState.pop();
362 return null;
363 }
364 if (stream.match(/[(,]/)) {
365 state.soyState.push("map-value")
366 state.soyState.push("map-value")
367 return null;
368 }
369 stream.next()
370 return null;
371
372 case "list-literal":
373 if (stream.match(/\]/)) {
374 state.soyState.pop();
375 state.lookupVariables = true;
376 return null;
377 }
378 if (stream.match(/for\b/)) {
379 state.soyState.push("var-def")
380 return "keyword";
381 } else if (stream.match(/in\b/)) {
382 state.lookupVariables = true;
383 return "keyword";
384 }
385 return expression(stream, state);
386
387 case "record-key":
388 if (stream.match(/[\w]+/)) {
389 return "property";
390 }
391 if (stream.match(/^[:]/)) {
392 state.soyState.pop();
393 return null;
394 }
395 stream.next();
396 return null;
397
398 case "map-value":
399 if (stream.peek() == ")" || stream.peek() == "," || stream.match(/^[:)]/)) {
400 state.soyState.pop();
401 return null;
402 }
403 return expression(stream, state);
404
405 case "tag":
406 var endTag = state.tag[0] == "/";
407 var tagName = endTag ? state.tag.substring(1) : state.tag;
408 var tag = tags[tagName];
409 if (stream.match(/^\/?}/)) {
410 var selfClosed = stream.current() == "/}";
411 if (selfClosed && !endTag) {
412 popcontext(state);
413 }
414 if (state.tag == "/template" || state.tag == "/deltemplate") {
415 state.variables = prepend(null, 'ij');
416 state.indent = 0;
417 } else {
418 state.indent -= config.indentUnit *
419 (selfClosed || indentingTags.indexOf(state.tag) == -1 ? 2 : 1);
420 }
421 state.soyState.pop();
422 return "keyword";
423 } else if (stream.match(/^([\w?]+)(?==)/)) {
424 if (state.context && state.context.tag == tagName && stream.current() == "kind" && (match = stream.match(/^="([^"]+)/, false))) {
425 var kind = match[1];
426 state.context.kind = kind;
427 var mode = modes[kind] || modes.html;
428 var localState = last(state.localStates);
429 if (localState.mode.indent) {
430 state.indent += localState.mode.indent(localState.state, "", "");
431 }
432 state.localStates.push({
433 mode: mode,
434 state: CodeMirror.startState(mode)
435 });
436 }
437 return "attribute";
438 }
439 return expression(stream, state);
440
441 case "literal":
442 if (stream.match(/^(?=\{\/literal})/)) {
443 state.soyState.pop();
444 return this.token(stream, state);
445 }
446 return tokenUntil(stream, state, /\{\/literal}/);
447 }
448
449 if (stream.match(/^\{literal}/)) {
450 state.indent += config.indentUnit;
451 state.soyState.push("literal");
452 state.context = new Context(state.context, "literal", state.variables);
453 return "keyword";
454
455 // A tag-keyword must be followed by whitespace, comment or a closing tag.
456 } else if (match = stream.match(/^\{([/@\\]?\w+\??)(?=$|[\s}]|\/[/*])/)) {
457 var prevTag = state.tag;
458 state.tag = match[1];
459 var endTag = state.tag[0] == "/";
460 var indentingTag = !!tags[state.tag];
461 var tagName = endTag ? state.tag.substring(1) : state.tag;
462 var tag = tags[tagName];
463 if (state.tag != "/switch")
464 state.indent += ((endTag || tag && tag.reduceIndent) && prevTag != "switch" ? 1 : 2) * config.indentUnit;
465
466 state.soyState.push("tag");
467 var tagError = false;
468 if (tag) {
469 if (!endTag) {
470 if (tag.soyState) state.soyState.push(tag.soyState);
471 }
472 // If a new tag, open a new context.
473 if (!tag.noEndTag && (indentingTag || !endTag)) {
474 state.context = new Context(state.context, state.tag, tag.variableScope ? state.variables : null);
475 // Otherwise close the current context.
476 } else if (endTag) {
477 if (!state.context || state.context.tag != tagName) {
478 tagError = true;
479 } else if (state.context) {
480 if (state.context.kind) {
481 state.localStates.pop();
482 var localState = last(state.localStates);
483 if (localState.mode.indent) {
484 state.indent -= localState.mode.indent(localState.state, "", "");
485 }
486 }
487 popcontext(state);
488 }
489 }
490 } else if (endTag) {
491 // Assume all tags with a closing tag are defined in the config.
492 tagError = true;
493 }
494 return (tagError ? "error " : "") + "keyword";
495
496 // Not a tag-keyword; it's an implicit print tag.
497 } else if (stream.eat('{')) {
498 state.tag = "print";
499 state.indent += 2 * config.indentUnit;
500 state.soyState.push("tag");
501 return "keyword";
502 }
503
504 return tokenUntil(stream, state, /\{|\s+\/\/|\/\*/);
505 },
506
507 indent: function(state, textAfter, line) {
508 var indent = state.indent, top = last(state.soyState);
509 if (top == "comment") return CodeMirror.Pass;
510
511 if (top == "literal") {
512 if (/^\{\/literal}/.test(textAfter)) indent -= config.indentUnit;
513 } else {
514 if (/^\s*\{\/(template|deltemplate)\b/.test(textAfter)) return 0;
515 if (/^\{(\/|(fallbackmsg|elseif|else|ifempty)\b)/.test(textAfter)) indent -= config.indentUnit;
516 if (state.tag != "switch" && /^\{(case|default)\b/.test(textAfter)) indent -= config.indentUnit;
517 if (/^\{\/switch\b/.test(textAfter)) indent -= config.indentUnit;
518 }
519 var localState = last(state.localStates);
520 if (indent && localState.mode.indent) {
521 indent += localState.mode.indent(localState.state, textAfter, line);
522 }
523 return indent;
524 },
525
526 innerMode: function(state) {
527 if (state.soyState.length && last(state.soyState) != "literal") return null;
528 else return last(state.localStates);
529 },
530
531 electricInput: /^\s*\{(\/|\/template|\/deltemplate|\/switch|fallbackmsg|elseif|else|case|default|ifempty|\/literal\})$/,
532 lineComment: "//",
533 blockCommentStart: "/*",
534 blockCommentEnd: "*/",
535 blockCommentContinue: " * ",
536 useInnerComments: false,
537 fold: "indent"
538 };
539 }, "htmlmixed");
540
541 CodeMirror.registerHelper("wordChars", "soy", /[\w$]/);
542
543 CodeMirror.registerHelper("hintWords", "soy", Object.keys(tags).concat(
544 ["css", "debugger"]));
545
546 CodeMirror.defineMIME("text/x-soy", "soy");
547});