// CodeMirror, copyright (c) by Marijn Haverbeke and others // Distributed under an MIT license: http://codemirror.net/LICENSE (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed")); else if (typeof define == "function" && define.amd) // AMD define(["../../lib/codemirror", "../htmlmixed/htmlmixed"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; var indentingTags = ["template", "literal", "msg", "fallbackmsg", "let", "if", "elseif", "else", "switch", "case", "default", "foreach", "ifempty", "for", "call", "param", "deltemplate", "delcall", "log"]; CodeMirror.defineMode("soy", function(config) { var textMode = CodeMirror.getMode(config, "text/plain"); var modes = { html: CodeMirror.getMode(config, {name: "text/html", multilineTagIndentFactor: 2, multilineTagIndentPastTag: false}), attributes: textMode, text: textMode, uri: textMode, css: CodeMirror.getMode(config, "text/css"), js: CodeMirror.getMode(config, {name: "text/javascript", statementIndent: 2 * config.indentUnit}) }; function last(array) { return array[array.length - 1]; } function tokenUntil(stream, state, untilRegExp) { if (stream.sol()) { for (var indent = 0; indent < state.indent; indent++) { if (!stream.eat(/\s/)) break; } if (indent) return null; } var oldString = stream.string; var match = untilRegExp.exec(oldString.substr(stream.pos)); if (match) { // We don't use backUp because it backs up just the position, not the state. // This uses an undocumented API. stream.string = oldString.substr(0, stream.pos + match.index); } var result = stream.hideFirstChars(state.indent, function() { var localState = last(state.localStates); return localState.mode.token(stream, localState.state); }); stream.string = oldString; return result; } function contains(list, element) { while (list) { if (list.element === element) return true; list = list.next; } return false; } function prepend(list, element) { return { element: element, next: list }; } // Reference a variable `name` in `list`. // Let `loose` be truthy to ignore missing identifiers. function ref(list, name, loose) { return contains(list, name) ? "variable-2" : (loose ? "variable" : "variable-2 error"); } function popscope(state) { if (state.scopes) { state.variables = state.scopes.element; state.scopes = state.scopes.next; } } return { startState: function() { return { kind: [], kindTag: [], soyState: [], templates: null, variables: prepend(null, 'ij'), scopes: null, indent: 0, quoteKind: null, localStates: [{ mode: modes.html, state: CodeMirror.startState(modes.html) }] }; }, copyState: function(state) { return { tag: state.tag, // Last seen Soy tag. kind: state.kind.concat([]), // Values of kind="" attributes. kindTag: state.kindTag.concat([]), // Opened tags with kind="" attributes. soyState: state.soyState.concat([]), templates: state.templates, variables: state.variables, scopes: state.scopes, indent: state.indent, // Indentation of the following line. quoteKind: state.quoteKind, localStates: state.localStates.map(function(localState) { return { mode: localState.mode, state: CodeMirror.copyState(localState.mode, localState.state) }; }) }; }, token: function(stream, state) { var match; switch (last(state.soyState)) { case "comment": if (stream.match(/^.*?\*\//)) { state.soyState.pop(); } else { stream.skipToEnd(); } if (!state.scopes) { var paramRe = /@param\??\s+(\S+)/g; var current = stream.current(); for (var match; (match = paramRe.exec(current)); ) { state.variables = prepend(state.variables, match[1]); } } return "comment"; case "templ-def": if (match = stream.match(/^\.?([\w]+(?!\.[\w]+)*)/)) { state.templates = prepend(state.templates, match[1]); state.scopes = prepend(state.scopes, state.variables); state.soyState.pop(); return "def"; } stream.next(); return null; case "templ-ref": if (match = stream.match(/^\.?([\w]+)/)) { state.soyState.pop(); // If the first character is '.', try to match against a local template name. if (match[0][0] == '.') { return ref(state.templates, match[1], true); } // Otherwise return "variable"; } stream.next(); return null; case "param-def": if (match = stream.match(/^\w+/)) { state.variables = prepend(state.variables, match[0]); state.soyState.pop(); state.soyState.push("param-type"); return "def"; } stream.next(); return null; case "param-type": if (stream.peek() == "}") { state.soyState.pop(); return null; } if (stream.eatWhile(/^[\w]+/)) { return "variable-3"; } stream.next(); return null; case "var-def": if (match = stream.match(/^\$([\w]+)/)) { state.variables = prepend(state.variables, match[1]); state.soyState.pop(); return "def"; } stream.next(); return null; case "tag": if (stream.match(/^\/?}/)) { if (state.tag == "/template" || state.tag == "/deltemplate") { popscope(state); state.variables = prepend(null, 'ij'); state.indent = 0; } else { if (state.tag == "/for" || state.tag == "/foreach") { popscope(state); } state.indent -= config.indentUnit * (stream.current() == "/}" || indentingTags.indexOf(state.tag) == -1 ? 2 : 1); } state.soyState.pop(); return "keyword"; } else if (stream.match(/^([\w?]+)(?==)/)) { if (stream.current() == "kind" && (match = stream.match(/^="([^"]+)/, false))) { var kind = match[1]; state.kind.push(kind); state.kindTag.push(state.tag); var mode = modes[kind] || modes.html; var localState = last(state.localStates); if (localState.mode.indent) { state.indent += localState.mode.indent(localState.state, ""); } state.localStates.push({ mode: mode, state: CodeMirror.startState(mode) }); } return "attribute"; } else if (match = stream.match(/^["']/)) { state.soyState.push("string"); state.quoteKind = match; return "string"; } if (match = stream.match(/^\$([\w]+)/)) { return ref(state.variables, match[1]); } if (match = stream.match(/^\w+/)) { return /^(?:as|and|or|not|in)$/.test(match[0]) ? "keyword" : null; } stream.next(); return null; case "literal": if (stream.match(/^(?=\{\/literal})/)) { state.indent -= config.indentUnit; state.soyState.pop(); return this.token(stream, state); } return tokenUntil(stream, state, /\{\/literal}/); case "string": var match = stream.match(/^.*?(["']|\\[\s\S])/); if (!match) { stream.skipToEnd(); } else if (match[1] == state.quoteKind) { state.quoteKind = null; state.soyState.pop(); } return "string"; } if (stream.match(/^\/\*/)) { state.soyState.push("comment"); if (!state.scopes) { state.variables = prepend(null, 'ij'); } return "comment"; } else if (stream.match(stream.sol() ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) { if (!state.scopes) { state.variables = prepend(null, 'ij'); } return "comment"; } else if (stream.match(/^\{literal}/)) { state.indent += config.indentUnit; state.soyState.push("literal"); return "keyword"; // A tag-keyword must be followed by whitespace or a closing tag. } else if (match = stream.match(/^\{([\/@\\]?\w+\??)(?=[\s\}])/)) { if (match[1] != "/switch") state.indent += (/^(\/|(else|elseif|ifempty|case|fallbackmsg|default)$)/.test(match[1]) && state.tag != "switch" ? 1 : 2) * config.indentUnit; state.tag = match[1]; if (state.tag == "/" + last(state.kindTag)) { // We found the tag that opened the current kind="". state.kind.pop(); state.kindTag.pop(); state.localStates.pop(); var localState = last(state.localStates); if (localState.mode.indent) { state.indent -= localState.mode.indent(localState.state, ""); } } state.soyState.push("tag"); if (state.tag == "template" || state.tag == "deltemplate") { state.soyState.push("templ-def"); } else if (state.tag == "call" || state.tag == "delcall") { state.soyState.push("templ-ref"); } else if (state.tag == "let") { state.soyState.push("var-def"); } else if (state.tag == "for" || state.tag == "foreach") { state.scopes = prepend(state.scopes, state.variables); state.soyState.push("var-def"); } else if (state.tag == "namespace") { if (!state.scopes) { state.variables = prepend(null, 'ij'); } } else if (state.tag.match(/^@(?:param\??|inject)/)) { state.soyState.push("param-def"); } return "keyword"; // Not a tag-keyword; it's an implicit print tag. } else if (stream.eat('{')) { state.tag = "print"; state.indent += 2 * config.indentUnit; state.soyState.push("tag"); return "keyword"; } return tokenUntil(stream, state, /\{|\s+\/\/|\/\*/); }, indent: function(state, textAfter) { var indent = state.indent, top = last(state.soyState); if (top == "comment") return CodeMirror.Pass; if (top == "literal") { if (/^\{\/literal}/.test(textAfter)) indent -= config.indentUnit; } else { if (/^\s*\{\/(template|deltemplate)\b/.test(textAfter)) return 0; if (/^\{(\/|(fallbackmsg|elseif|else|ifempty)\b)/.test(textAfter)) indent -= config.indentUnit; if (state.tag != "switch" && /^\{(case|default)\b/.test(textAfter)) indent -= config.indentUnit; if (/^\{\/switch\b/.test(textAfter)) indent -= config.indentUnit; } var localState = last(state.localStates); if (indent && localState.mode.indent) { indent += localState.mode.indent(localState.state, textAfter); } return indent; }, innerMode: function(state) { if (state.soyState.length && last(state.soyState) != "literal") return null; else return last(state.localStates); }, electricInput: /^\s*\{(\/|\/template|\/deltemplate|\/switch|fallbackmsg|elseif|else|case|default|ifempty|\/literal\})$/, lineComment: "//", blockCommentStart: "/*", blockCommentEnd: "*/", blockCommentContinue: " * ", useInnerComments: false, fold: "indent" }; }, "htmlmixed"); CodeMirror.registerHelper("hintWords", "soy", indentingTags.concat( ["delpackage", "namespace", "alias", "print", "css", "debugger"])); CodeMirror.defineMIME("text/x-soy", "soy"); });