Mercurial
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 }); |