5
if (typeof exports == "object" && typeof module == "object")
6
mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"));
7
else if (typeof define == "function" && define.amd)
8
define(["../../lib/codemirror", "../htmlmixed/htmlmixed"], mod);
11
})(function(CodeMirror) {
14
var paramData = { noEndTag: true, soyState: "param-def" };
16
"alias": { noEndTag: true },
17
"delpackage": { noEndTag: true },
18
"namespace": { noEndTag: true, soyState: "namespace-def" },
19
"@attribute": paramData,
20
"@attribute?": paramData,
24
"@inject?": paramData,
26
"template": { soyState: "templ-def", variableScope: true},
27
"extern": {soyState: "param-def"},
28
"export": {soyState: "export"},
31
"fallbackmsg": { noEndTag: true, reduceIndent: true},
34
"let": { soyState: "var-def" },
38
"elseif": { noEndTag: true, reduceIndent: true},
39
"else": { noEndTag: true, reduceIndent: true},
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" },
52
"element": { variableScope: true },
54
"const": { soyState: "const-def"},
57
var indentingTags = Object.keys(tags).filter(function(tag) {
58
return !tags[tag].noEndTag || tags[tag].reduceIndent;
61
CodeMirror.defineMode("soy", function(config) {
62
var textMode = CodeMirror.getMode(config, "text/plain");
64
html: CodeMirror.getMode(config, {name: "text/html", multilineTagIndentFactor: 2, multilineTagIndentPastTag: false, allowMissingTagName: true}),
68
trusted_resource_uri: textMode,
69
css: CodeMirror.getMode(config, "text/css"),
70
js: CodeMirror.getMode(config, {name: "text/javascript", statementIndent: 2 * config.indentUnit})
73
function last(array) {
74
return array[array.length - 1];
77
function tokenUntil(stream, state, untilRegExp) {
79
for (var indent = 0; indent < state.indent; indent++) {
80
if (!stream.eat(/\s/)) break;
82
if (indent) return null;
84
var oldString = stream.string;
85
var match = untilRegExp.exec(oldString.substr(stream.pos));
89
stream.string = oldString.substr(0, stream.pos + match.index);
91
var result = stream.hideFirstChars(state.indent, function() {
92
var localState = last(state.localStates);
93
return localState.mode.token(stream, localState.state);
95
stream.string = oldString;
99
function contains(list, element) {
101
if (list.element === element) return true;
107
function prepend(list, element) {
114
function popcontext(state) {
115
if (!state.context) return;
116
if (state.context.scope) {
117
state.variables = state.context.scope;
119
state.context = state.context.previousContext;
124
function ref(list, name, loose) {
125
return contains(list, name) ? "variable-2" : (loose ? "variable" : "variable-2 error");
129
function Context(previousContext, tag, scope) {
130
this.previousContext = previousContext;
136
function expression(stream, state) {
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;
143
} else if (stream.match(/\bmap(?=\()/)) {
144
state.soyState.push("map-literal");
146
} else if (stream.match(/\brecord(?=\()/)) {
147
state.soyState.push("record-literal");
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];
155
} else if (stream.match(/^[(]/)) {
156
state.soyState.push("open-parentheses");
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]*)?/)) {
162
} else if (stream.match(/(\||[+\-*\/%]|[=!]=|\?:|[<>]=?)/)) {
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;
176
startState: function() {
179
variables: prepend(null, 'ij'),
184
lookupVariables: true,
187
state: CodeMirror.startState(modes.html)
192
copyState: function(state) {
195
soyState: state.soyState.concat([]),
196
variables: state.variables,
197
context: state.context,
198
indent: state.indent,
199
quoteKind: state.quoteKind,
200
lookupVariables: state.lookupVariables,
201
localStates: state.localStates.map(function(localState) {
203
mode: localState.mode,
204
state: CodeMirror.copyState(localState.mode, localState.state)
210
token: function(stream, state) {
213
switch (last(state.soyState)) {
215
if (stream.match(/^.*?\*\//)) {
216
state.soyState.pop();
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]);
230
var match = stream.match(/^.*?(["']|\\[\s\S])/);
233
} else if (match[1] == state.quoteKind) {
234
state.quoteKind = null;
235
state.soyState.pop();
240
if (!state.soyState.length || last(state.soyState) != "literal") {
241
if (stream.match(/^\/\*/)) {
242
state.soyState.push("comment");
244
} else if (stream.match(stream.sol() ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) {
249
switch (last(state.soyState)) {
251
if (match = stream.match(/^\.?([\w]+(?!\.[\w]+)*)/)) {
252
state.soyState.pop();
259
if (match = stream.match(/(\.?[a-zA-Z_][a-zA-Z_0-9]+)+/)) {
260
state.soyState.pop();
262
if (match[0][0] == '.') {
268
if (match = stream.match(/^\$([\w]+)/)) {
269
state.soyState.pop();
270
return ref(state.variables, match[1], !state.lookupVariables);
276
case "namespace-def":
277
if (match = stream.match(/^\.?([\w\.]+)/)) {
278
state.soyState.pop();
285
if (match = stream.match(/^\*/)) {
286
state.soyState.pop();
287
state.soyState.push("param-type");
290
if (match = stream.match(/^\w+/)) {
291
state.variables = prepend(state.variables, match[0]);
292
state.soyState.pop();
293
state.soyState.push("param-type");
300
if (match = stream.match(/^\w+/)) {
301
state.soyState.pop();
307
case "open-parentheses":
308
if (stream.match(/[)]/)) {
309
state.soyState.pop();
312
return expression(stream, state);
315
var peekChar = stream.peek();
316
if ("}]=>,".indexOf(peekChar) != -1) {
317
state.soyState.pop();
319
} else if (peekChar == "[") {
320
state.soyState.push('param-type-record');
322
} else if (peekChar == "(") {
323
state.soyState.push('param-type-template');
325
} else if (peekChar == "<") {
326
state.soyState.push('param-type-parameter');
328
} else if (match = stream.match(/^([\w]+|[?])/)) {
334
case "param-type-record":
335
var peekChar = stream.peek();
336
if (peekChar == "]") {
337
state.soyState.pop();
340
if (stream.match(/^\w+/)) {
341
state.soyState.push('param-type');
347
case "param-type-parameter":
348
if (stream.match(/^[>]/)) {
349
state.soyState.pop();
352
if (stream.match(/^[<,]/)) {
353
state.soyState.push('param-type');
359
case "param-type-template":
360
if (stream.match(/[>]/)) {
361
state.soyState.pop();
362
state.soyState.push('param-type');
365
if (stream.match(/^\w+/)) {
366
state.soyState.push('param-type');
373
if (match = stream.match(/^\$([\w]+)/)) {
374
state.variables = prepend(state.variables, match[1]);
375
state.soyState.pop();
382
if (stream.match(/\bin\b/)) {
383
state.soyState.pop();
386
if (stream.peek() == "$") {
387
state.soyState.push('var-def');
393
case "record-literal":
394
if (stream.match(/^[)]/)) {
395
state.soyState.pop();
398
if (stream.match(/[(,]/)) {
399
state.soyState.push("map-value")
400
state.soyState.push("record-key")
407
if (stream.match(/^[)]/)) {
408
state.soyState.pop();
411
if (stream.match(/[(,]/)) {
412
state.soyState.push("map-value")
413
state.soyState.push("map-value")
420
if (stream.match(']')) {
421
state.soyState.pop();
422
state.lookupVariables = true;
426
if (stream.match(/\bfor\b/)) {
427
state.lookupVariables = true;
428
state.soyState.push('for-loop');
431
return expression(stream, state);
434
if (stream.match(/[\w]+/)) {
437
if (stream.match(/^[:]/)) {
438
state.soyState.pop();
445
if (stream.peek() == ")" || stream.peek() == "," || stream.match(/^[:)]/)) {
446
state.soyState.pop();
449
return expression(stream, state);
452
if (stream.eat(";")) {
453
state.soyState.pop();
454
state.indent -= 2 * config.indentUnit;
457
if (stream.match(/\w+(?=\s+as\b)/)) {
460
if (match = stream.match(/\w+/)) {
461
return /\b(from|as)\b/.test(match[0]) ? "keyword" : "def";
463
if (match = stream.match(/^["']/)) {
464
state.soyState.push("string");
465
state.quoteKind = match[0];
474
if (state.tag === undefined) {
478
endTag = state.tag[0] == "/";
479
tagName = endTag ? state.tag.substring(1) : state.tag;
481
var tag = tags[tagName];
482
if (stream.match(/^\/?}/)) {
483
var selfClosed = stream.current() == "/}";
484
if (selfClosed && !endTag) {
487
if (state.tag == "/template" || state.tag == "/deltemplate") {
488
state.variables = prepend(null, 'ij');
491
state.indent -= config.indentUnit *
492
(selfClosed || indentingTags.indexOf(state.tag) == -1 ? 2 : 1);
494
state.soyState.pop();
496
} else if (stream.match(/^([\w?]+)(?==)/)) {
497
if (state.context && state.context.tag == tagName && stream.current() == "kind" && (match = stream.match(/^="([^"]+)/, false))) {
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, "", "");
505
state.localStates.push({
507
state: CodeMirror.startState(mode)
512
return expression(stream, state);
514
case "template-call-expression":
515
if (stream.match(/^([\w-?]+)(?==)/)) {
517
} else if (stream.eat('>')) {
518
state.soyState.pop();
520
} else if (stream.eat('/>')) {
521
state.soyState.pop();
524
return expression(stream, state);
526
if (stream.match('{/literal}', false)) {
527
state.soyState.pop();
528
return this.token(stream, state);
530
return tokenUntil(stream, state, /\{\/literal}/);
532
if (match = stream.match(/\w+/)) {
533
state.soyState.pop();
534
if (match == "const") {
535
state.soyState.push("const-def")
537
} else if (match == "extern") {
538
state.soyState.push("param-def")
546
if (stream.match(/^\w+/)) {
547
state.soyState.pop();
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);
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;
571
state.soyState.push("tag");
572
var tagError = false;
575
if (tag.soyState) state.soyState.push(tag.soyState);
578
if (!tag.noEndTag && (indentingTag || !endTag)) {
579
state.context = new Context(state.context, state.tag, tag.variableScope ? state.variables : null);
582
var isBalancedForExtern = tagName == 'extern' && (state.context && state.context.tag == 'export');
583
if (!state.context || ((state.context.tag != tagName) && !isBalancedForExtern)) {
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, "", "");
600
return (tagError ? "error " : "") + "keyword";
603
} else if (stream.eat('{')) {
605
state.indent += 2 * config.indentUnit;
606
state.soyState.push("tag");
608
} else if (!state.context && stream.sol() && stream.match(/import\b/)) {
609
state.soyState.push("import");
610
state.indent += 2 * config.indentUnit;
612
} else if (match = stream.match('<{')) {
613
state.soyState.push("template-call-expression");
614
state.indent += 2 * config.indentUnit;
615
state.soyState.push("tag");
617
} else if (match = stream.match('</>')) {
618
state.indent -= 1 * config.indentUnit;
622
return tokenUntil(stream, state, /\{|\s+\/\/|\/\*/);
625
indent: function(state, textAfter, line) {
626
var indent = state.indent, top = last(state.soyState);
627
if (top == "comment") return CodeMirror.Pass;
629
if (top == "literal") {
630
if (/^\{\/literal}/.test(textAfter)) indent -= config.indentUnit;
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;
637
var localState = last(state.localStates);
638
if (indent && localState.mode.indent) {
639
indent += localState.mode.indent(localState.state, textAfter, line);
644
innerMode: function(state) {
645
if (state.soyState.length && last(state.soyState) != "literal") return null;
646
else return last(state.localStates);
649
electricInput: /^\s*\{(\/|\/template|\/deltemplate|\/switch|fallbackmsg|elseif|else|case|default|ifempty|\/literal\})$/,
651
blockCommentStart: "/*",
652
blockCommentEnd: "*/",
653
blockCommentContinue: " * ",
654
useInnerComments: false,
659
CodeMirror.registerHelper("wordChars", "soy", /[\w$]/);
661
CodeMirror.registerHelper("hintWords", "soy", Object.keys(tags).concat(
662
["css", "debugger"]));
664
CodeMirror.defineMIME("text/x-soy", "soy");