openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

soy.js (12868B)


      1 // CodeMirror, copyright (c) by Marijn Haverbeke and others
      2 // Distributed under an MIT license: http://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 indentingTags = ["template", "literal", "msg", "fallbackmsg", "let", "if", "elseif",
     15                        "else", "switch", "case", "default", "foreach", "ifempty", "for",
     16                        "call", "param", "deltemplate", "delcall", "log"];
     17 
     18   CodeMirror.defineMode("soy", function(config) {
     19     var textMode = CodeMirror.getMode(config, "text/plain");
     20     var modes = {
     21       html: CodeMirror.getMode(config, {name: "text/html", multilineTagIndentFactor: 2, multilineTagIndentPastTag: false}),
     22       attributes: textMode,
     23       text: textMode,
     24       uri: textMode,
     25       css: CodeMirror.getMode(config, "text/css"),
     26       js: CodeMirror.getMode(config, {name: "text/javascript", statementIndent: 2 * config.indentUnit})
     27     };
     28 
     29     function last(array) {
     30       return array[array.length - 1];
     31     }
     32 
     33     function tokenUntil(stream, state, untilRegExp) {
     34       if (stream.sol()) {
     35         for (var indent = 0; indent < state.indent; indent++) {
     36           if (!stream.eat(/\s/)) break;
     37         }
     38         if (indent) return null;
     39       }
     40       var oldString = stream.string;
     41       var match = untilRegExp.exec(oldString.substr(stream.pos));
     42       if (match) {
     43         // We don't use backUp because it backs up just the position, not the state.
     44         // This uses an undocumented API.
     45         stream.string = oldString.substr(0, stream.pos + match.index);
     46       }
     47       var result = stream.hideFirstChars(state.indent, function() {
     48         var localState = last(state.localStates);
     49         return localState.mode.token(stream, localState.state);
     50       });
     51       stream.string = oldString;
     52       return result;
     53     }
     54 
     55     function contains(list, element) {
     56       while (list) {
     57         if (list.element === element) return true;
     58         list = list.next;
     59       }
     60       return false;
     61     }
     62 
     63     function prepend(list, element) {
     64       return {
     65         element: element,
     66         next: list
     67       };
     68     }
     69 
     70     // Reference a variable `name` in `list`.
     71     // Let `loose` be truthy to ignore missing identifiers.
     72     function ref(list, name, loose) {
     73       return contains(list, name) ? "variable-2" : (loose ? "variable" : "variable-2 error");
     74     }
     75 
     76     function popscope(state) {
     77       if (state.scopes) {
     78         state.variables = state.scopes.element;
     79         state.scopes = state.scopes.next;
     80       }
     81     }
     82 
     83     return {
     84       startState: function() {
     85         return {
     86           kind: [],
     87           kindTag: [],
     88           soyState: [],
     89           templates: null,
     90           variables: prepend(null, 'ij'),
     91           scopes: null,
     92           indent: 0,
     93           quoteKind: null,
     94           localStates: [{
     95             mode: modes.html,
     96             state: CodeMirror.startState(modes.html)
     97           }]
     98         };
     99       },
    100 
    101       copyState: function(state) {
    102         return {
    103           tag: state.tag, // Last seen Soy tag.
    104           kind: state.kind.concat([]), // Values of kind="" attributes.
    105           kindTag: state.kindTag.concat([]), // Opened tags with kind="" attributes.
    106           soyState: state.soyState.concat([]),
    107           templates: state.templates,
    108           variables: state.variables,
    109           scopes: state.scopes,
    110           indent: state.indent, // Indentation of the following line.
    111           quoteKind: state.quoteKind,
    112           localStates: state.localStates.map(function(localState) {
    113             return {
    114               mode: localState.mode,
    115               state: CodeMirror.copyState(localState.mode, localState.state)
    116             };
    117           })
    118         };
    119       },
    120 
    121       token: function(stream, state) {
    122         var match;
    123 
    124         switch (last(state.soyState)) {
    125           case "comment":
    126             if (stream.match(/^.*?\*\//)) {
    127               state.soyState.pop();
    128             } else {
    129               stream.skipToEnd();
    130             }
    131             if (!state.scopes) {
    132               var paramRe = /@param\??\s+(\S+)/g;
    133               var current = stream.current();
    134               for (var match; (match = paramRe.exec(current)); ) {
    135                 state.variables = prepend(state.variables, match[1]);
    136               }
    137             }
    138             return "comment";
    139 
    140           case "string":
    141             var match = stream.match(/^.*?(["']|\\[\s\S])/);
    142             if (!match) {
    143               stream.skipToEnd();
    144             } else if (match[1] == state.quoteKind) {
    145               state.quoteKind = null;
    146               state.soyState.pop();
    147             }
    148             return "string";
    149         }
    150 
    151         if (stream.match(/^\/\*/)) {
    152           state.soyState.push("comment");
    153           return "comment";
    154         } else if (stream.match(stream.sol() || (state.soyState.length && last(state.soyState) != "literal") ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) {
    155           return "comment";
    156         }
    157 
    158         switch (last(state.soyState)) {
    159           case "templ-def":
    160             if (match = stream.match(/^\.?([\w]+(?!\.[\w]+)*)/)) {
    161               state.templates = prepend(state.templates, match[1]);
    162               state.scopes = prepend(state.scopes, state.variables);
    163               state.soyState.pop();
    164               return "def";
    165             }
    166             stream.next();
    167             return null;
    168 
    169           case "templ-ref":
    170             if (match = stream.match(/^\.?([\w]+)/)) {
    171               state.soyState.pop();
    172               // If the first character is '.', try to match against a local template name.
    173               if (match[0][0] == '.') {
    174                 return ref(state.templates, match[1], true);
    175               }
    176               // Otherwise
    177               return "variable";
    178             }
    179             stream.next();
    180             return null;
    181 
    182           case "param-def":
    183             if (match = stream.match(/^\w+/)) {
    184               state.variables = prepend(state.variables, match[0]);
    185               state.soyState.pop();
    186               state.soyState.push("param-type");
    187               return "def";
    188             }
    189             stream.next();
    190             return null;
    191 
    192           case "param-type":
    193             if (stream.peek() == "}") {
    194               state.soyState.pop();
    195               return null;
    196             }
    197             if (stream.eatWhile(/^[\w]+/)) {
    198               return "variable-3";
    199             }
    200             stream.next();
    201             return null;
    202 
    203           case "var-def":
    204             if (match = stream.match(/^\$([\w]+)/)) {
    205               state.variables = prepend(state.variables, match[1]);
    206               state.soyState.pop();
    207               return "def";
    208             }
    209             stream.next();
    210             return null;
    211 
    212           case "tag":
    213             if (stream.match(/^\/?}/)) {
    214               if (state.tag == "/template" || state.tag == "/deltemplate") {
    215                 popscope(state);
    216                 state.variables = prepend(null, 'ij');
    217                 state.indent = 0;
    218               } else {
    219                 if (state.tag == "/for" || state.tag == "/foreach") {
    220                   popscope(state);
    221                 }
    222                 state.indent -= config.indentUnit *
    223                     (stream.current() == "/}" || indentingTags.indexOf(state.tag) == -1 ? 2 : 1);
    224               }
    225               state.soyState.pop();
    226               return "keyword";
    227             } else if (stream.match(/^([\w?]+)(?==)/)) {
    228               if (stream.current() == "kind" && (match = stream.match(/^="([^"]+)/, false))) {
    229                 var kind = match[1];
    230                 state.kind.push(kind);
    231                 state.kindTag.push(state.tag);
    232                 var mode = modes[kind] || modes.html;
    233                 var localState = last(state.localStates);
    234                 if (localState.mode.indent) {
    235                   state.indent += localState.mode.indent(localState.state, "");
    236                 }
    237                 state.localStates.push({
    238                   mode: mode,
    239                   state: CodeMirror.startState(mode)
    240                 });
    241               }
    242               return "attribute";
    243             } else if (match = stream.match(/^["']/)) {
    244               state.soyState.push("string");
    245               state.quoteKind = match;
    246               return "string";
    247             }
    248             if (match = stream.match(/^\$([\w]+)/)) {
    249               return ref(state.variables, match[1]);
    250             }
    251             if (match = stream.match(/^\w+/)) {
    252               return /^(?:as|and|or|not|in)$/.test(match[0]) ? "keyword" : null;
    253             }
    254             stream.next();
    255             return null;
    256 
    257           case "literal":
    258             if (stream.match(/^(?=\{\/literal})/)) {
    259               state.indent -= config.indentUnit;
    260               state.soyState.pop();
    261               return this.token(stream, state);
    262             }
    263             return tokenUntil(stream, state, /\{\/literal}/);
    264         }
    265 
    266         if (stream.match(/^\{literal}/)) {
    267           state.indent += config.indentUnit;
    268           state.soyState.push("literal");
    269           return "keyword";
    270 
    271         // A tag-keyword must be followed by whitespace, comment or a closing tag.
    272         } else if (match = stream.match(/^\{([\/@\\]?\w+\??)(?=[\s\}]|\/[/*])/)) {
    273           if (match[1] != "/switch")
    274             state.indent += (/^(\/|(else|elseif|ifempty|case|fallbackmsg|default)$)/.test(match[1]) && state.tag != "switch" ? 1 : 2) * config.indentUnit;
    275           state.tag = match[1];
    276           if (state.tag == "/" + last(state.kindTag)) {
    277             // We found the tag that opened the current kind="".
    278             state.kind.pop();
    279             state.kindTag.pop();
    280             state.localStates.pop();
    281             var localState = last(state.localStates);
    282             if (localState.mode.indent) {
    283               state.indent -= localState.mode.indent(localState.state, "");
    284             }
    285           }
    286           state.soyState.push("tag");
    287           if (state.tag == "template" || state.tag == "deltemplate") {
    288             state.soyState.push("templ-def");
    289           } else if (state.tag == "call" || state.tag == "delcall") {
    290             state.soyState.push("templ-ref");
    291           } else if (state.tag == "let") {
    292             state.soyState.push("var-def");
    293           } else if (state.tag == "for" || state.tag == "foreach") {
    294             state.scopes = prepend(state.scopes, state.variables);
    295             state.soyState.push("var-def");
    296           } else if (state.tag == "namespace") {
    297             if (!state.scopes) {
    298               state.variables = prepend(null, 'ij');
    299             }
    300           } else if (state.tag.match(/^@(?:param\??|inject)/)) {
    301             state.soyState.push("param-def");
    302           }
    303           return "keyword";
    304 
    305         // Not a tag-keyword; it's an implicit print tag.
    306         } else if (stream.eat('{')) {
    307           state.tag = "print";
    308           state.indent += 2 * config.indentUnit;
    309           state.soyState.push("tag");
    310           return "keyword";
    311         }
    312 
    313         return tokenUntil(stream, state, /\{|\s+\/\/|\/\*/);
    314       },
    315 
    316       indent: function(state, textAfter) {
    317         var indent = state.indent, top = last(state.soyState);
    318         if (top == "comment") return CodeMirror.Pass;
    319 
    320         if (top == "literal") {
    321           if (/^\{\/literal}/.test(textAfter)) indent -= config.indentUnit;
    322         } else {
    323           if (/^\s*\{\/(template|deltemplate)\b/.test(textAfter)) return 0;
    324           if (/^\{(\/|(fallbackmsg|elseif|else|ifempty)\b)/.test(textAfter)) indent -= config.indentUnit;
    325           if (state.tag != "switch" && /^\{(case|default)\b/.test(textAfter)) indent -= config.indentUnit;
    326           if (/^\{\/switch\b/.test(textAfter)) indent -= config.indentUnit;
    327         }
    328         var localState = last(state.localStates);
    329         if (indent && localState.mode.indent) {
    330           indent += localState.mode.indent(localState.state, textAfter);
    331         }
    332         return indent;
    333       },
    334 
    335       innerMode: function(state) {
    336         if (state.soyState.length && last(state.soyState) != "literal") return null;
    337         else return last(state.localStates);
    338       },
    339 
    340       electricInput: /^\s*\{(\/|\/template|\/deltemplate|\/switch|fallbackmsg|elseif|else|case|default|ifempty|\/literal\})$/,
    341       lineComment: "//",
    342       blockCommentStart: "/*",
    343       blockCommentEnd: "*/",
    344       blockCommentContinue: " * ",
    345       useInnerComments: false,
    346       fold: "indent"
    347     };
    348   }, "htmlmixed");
    349 
    350   CodeMirror.registerHelper("hintWords", "soy", indentingTags.concat(
    351       ["delpackage", "namespace", "alias", "print", "css", "debugger"]));
    352 
    353   CodeMirror.defineMIME("text/x-soy", "soy");
    354 });