comparison .cms/lib/codemirror/mode/soy/soy.js @ 0:78edf6b517a0 draft

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