You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
665 lines
22 KiB
665 lines
22 KiB
// CodeMirror, copyright (c) by Marijn Haverbeke and others |
|
// Distributed under an MIT license: https://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 paramData = { noEndTag: true, soyState: "param-def" }; |
|
var tags = { |
|
"alias": { noEndTag: true }, |
|
"delpackage": { noEndTag: true }, |
|
"namespace": { noEndTag: true, soyState: "namespace-def" }, |
|
"@attribute": paramData, |
|
"@attribute?": paramData, |
|
"@param": paramData, |
|
"@param?": paramData, |
|
"@inject": paramData, |
|
"@inject?": paramData, |
|
"@state": paramData, |
|
"template": { soyState: "templ-def", variableScope: true}, |
|
"extern": {soyState: "param-def"}, |
|
"export": {soyState: "export"}, |
|
"literal": { }, |
|
"msg": {}, |
|
"fallbackmsg": { noEndTag: true, reduceIndent: true}, |
|
"select": {}, |
|
"plural": {}, |
|
"let": { soyState: "var-def" }, |
|
"if": {}, |
|
"javaimpl": {}, |
|
"jsimpl": {}, |
|
"elseif": { noEndTag: true, reduceIndent: true}, |
|
"else": { noEndTag: true, reduceIndent: true}, |
|
"switch": {}, |
|
"case": { noEndTag: true, reduceIndent: true}, |
|
"default": { noEndTag: true, reduceIndent: true}, |
|
"foreach": { variableScope: true, soyState: "for-loop" }, |
|
"ifempty": { noEndTag: true, reduceIndent: true}, |
|
"for": { variableScope: true, soyState: "for-loop" }, |
|
"call": { soyState: "templ-ref" }, |
|
"param": { soyState: "param-ref"}, |
|
"print": { noEndTag: true }, |
|
"deltemplate": { soyState: "templ-def", variableScope: true}, |
|
"delcall": { soyState: "templ-ref" }, |
|
"log": {}, |
|
"element": { variableScope: true }, |
|
"velog": {}, |
|
"const": { soyState: "const-def"}, |
|
}; |
|
|
|
var indentingTags = Object.keys(tags).filter(function(tag) { |
|
return !tags[tag].noEndTag || tags[tag].reduceIndent; |
|
}); |
|
|
|
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, allowMissingTagName: true}), |
|
attributes: textMode, |
|
text: textMode, |
|
uri: textMode, |
|
trusted_resource_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 |
|
}; |
|
} |
|
|
|
function popcontext(state) { |
|
if (!state.context) return; |
|
if (state.context.scope) { |
|
state.variables = state.context.scope; |
|
} |
|
state.context = state.context.previousContext; |
|
} |
|
|
|
// 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"); |
|
} |
|
|
|
// Data for an open soy tag. |
|
function Context(previousContext, tag, scope) { |
|
this.previousContext = previousContext; |
|
this.tag = tag; |
|
this.kind = null; |
|
this.scope = scope; |
|
} |
|
|
|
function expression(stream, state) { |
|
var match; |
|
if (stream.match(/[[]/)) { |
|
state.soyState.push("list-literal"); |
|
state.context = new Context(state.context, "list-literal", state.variables); |
|
state.lookupVariables = false; |
|
return null; |
|
} else if (stream.match(/map\b/)) { |
|
state.soyState.push("map-literal"); |
|
return "keyword"; |
|
} else if (stream.match(/record\b/)) { |
|
state.soyState.push("record-literal"); |
|
return "keyword"; |
|
} else if (stream.match(/([\w]+)(?=\()/)) { |
|
return "variable callee"; |
|
} else if (match = stream.match(/^["']/)) { |
|
state.soyState.push("string"); |
|
state.quoteKind = match[0]; |
|
return "string"; |
|
} else if (stream.match(/^[(]/)) { |
|
state.soyState.push("open-parentheses"); |
|
return null; |
|
} else if (stream.match(/(null|true|false)(?!\w)/) || |
|
stream.match(/0x([0-9a-fA-F]{2,})/) || |
|
stream.match(/-?([0-9]*[.])?[0-9]+(e[0-9]*)?/)) { |
|
return "atom"; |
|
} else if (stream.match(/(\||[+\-*\/%]|[=!]=|\?:|[<>]=?)/)) { |
|
// Tokenize filter, binary, null propagator, and equality operators. |
|
return "operator"; |
|
} else if (match = stream.match(/^\$([\w]+)/)) { |
|
return ref(state.variables, match[1], !state.lookupVariables); |
|
} else if (match = stream.match(/^\w+/)) { |
|
return /^(?:as|and|or|not|in|if)$/.test(match[0]) ? "keyword" : null; |
|
} |
|
|
|
stream.next(); |
|
return null; |
|
} |
|
|
|
return { |
|
startState: function() { |
|
return { |
|
soyState: [], |
|
variables: prepend(null, 'ij'), |
|
scopes: null, |
|
indent: 0, |
|
quoteKind: null, |
|
context: null, |
|
lookupVariables: true, // Is unknown variables considered an error |
|
localStates: [{ |
|
mode: modes.html, |
|
state: CodeMirror.startState(modes.html) |
|
}] |
|
}; |
|
}, |
|
|
|
copyState: function(state) { |
|
return { |
|
tag: state.tag, // Last seen Soy tag. |
|
soyState: state.soyState.concat([]), |
|
variables: state.variables, |
|
context: state.context, |
|
indent: state.indent, // Indentation of the following line. |
|
quoteKind: state.quoteKind, |
|
lookupVariables: state.lookupVariables, |
|
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.context || !state.context.scope) { |
|
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 "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 (!state.soyState.length || last(state.soyState) != "literal") { |
|
if (stream.match(/^\/\*/)) { |
|
state.soyState.push("comment"); |
|
return "comment"; |
|
} else if (stream.match(stream.sol() ? /^\s*\/\/.*/ : /^\s+\/\/.*/)) { |
|
return "comment"; |
|
} |
|
} |
|
|
|
switch (last(state.soyState)) { |
|
case "templ-def": |
|
if (match = stream.match(/^\.?([\w]+(?!\.[\w]+)*)/)) { |
|
state.soyState.pop(); |
|
return "def"; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "templ-ref": |
|
if (match = stream.match(/(\.?[a-zA-Z_][a-zA-Z_0-9]+)+/)) { |
|
state.soyState.pop(); |
|
// If the first character is '.', it can only be a local template. |
|
if (match[0][0] == '.') { |
|
return "variable-2" |
|
} |
|
// Otherwise |
|
return "variable"; |
|
} |
|
if (match = stream.match(/^\$([\w]+)/)) { |
|
state.soyState.pop(); |
|
return ref(state.variables, match[1], !state.lookupVariables); |
|
} |
|
|
|
stream.next(); |
|
return null; |
|
|
|
case "namespace-def": |
|
if (match = stream.match(/^\.?([\w\.]+)/)) { |
|
state.soyState.pop(); |
|
return "variable"; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "param-def": |
|
if (match = stream.match(/^\*/)) { |
|
state.soyState.pop(); |
|
state.soyState.push("param-type"); |
|
return "type"; |
|
} |
|
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-ref": |
|
if (match = stream.match(/^\w+/)) { |
|
state.soyState.pop(); |
|
return "property"; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "open-parentheses": |
|
if (stream.match(/[)]/)) { |
|
state.soyState.pop(); |
|
return null; |
|
} |
|
return expression(stream, state); |
|
|
|
case "param-type": |
|
var peekChar = stream.peek(); |
|
if ("}]=>,".indexOf(peekChar) != -1) { |
|
state.soyState.pop(); |
|
return null; |
|
} else if (peekChar == "[") { |
|
state.soyState.push('param-type-record'); |
|
return null; |
|
} else if (peekChar == "(") { |
|
state.soyState.push('param-type-template'); |
|
return null; |
|
} else if (peekChar == "<") { |
|
state.soyState.push('param-type-parameter'); |
|
return null; |
|
} else if (match = stream.match(/^([\w]+|[?])/)) { |
|
return "type"; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "param-type-record": |
|
var peekChar = stream.peek(); |
|
if (peekChar == "]") { |
|
state.soyState.pop(); |
|
return null; |
|
} |
|
if (stream.match(/^\w+/)) { |
|
state.soyState.push('param-type'); |
|
return "property"; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "param-type-parameter": |
|
if (stream.match(/^[>]/)) { |
|
state.soyState.pop(); |
|
return null; |
|
} |
|
if (stream.match(/^[<,]/)) { |
|
state.soyState.push('param-type'); |
|
return null; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "param-type-template": |
|
if (stream.match(/[>]/)) { |
|
state.soyState.pop(); |
|
state.soyState.push('param-type'); |
|
return null; |
|
} |
|
if (stream.match(/^\w+/)) { |
|
state.soyState.push('param-type'); |
|
return "def"; |
|
} |
|
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 "for-loop": |
|
if (stream.match(/\bin\b/)) { |
|
state.soyState.pop(); |
|
return "keyword"; |
|
} |
|
if (stream.peek() == "$") { |
|
state.soyState.push('var-def'); |
|
return null; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "record-literal": |
|
if (stream.match(/^[)]/)) { |
|
state.soyState.pop(); |
|
return null; |
|
} |
|
if (stream.match(/[(,]/)) { |
|
state.soyState.push("map-value") |
|
state.soyState.push("record-key") |
|
return null; |
|
} |
|
stream.next() |
|
return null; |
|
|
|
case "map-literal": |
|
if (stream.match(/^[)]/)) { |
|
state.soyState.pop(); |
|
return null; |
|
} |
|
if (stream.match(/[(,]/)) { |
|
state.soyState.push("map-value") |
|
state.soyState.push("map-value") |
|
return null; |
|
} |
|
stream.next() |
|
return null; |
|
|
|
case "list-literal": |
|
if (stream.match(']')) { |
|
state.soyState.pop(); |
|
state.lookupVariables = true; |
|
popcontext(state); |
|
return null; |
|
} |
|
if (stream.match(/\bfor\b/)) { |
|
state.lookupVariables = true; |
|
state.soyState.push('for-loop'); |
|
return "keyword"; |
|
} |
|
return expression(stream, state); |
|
|
|
case "record-key": |
|
if (stream.match(/[\w]+/)) { |
|
return "property"; |
|
} |
|
if (stream.match(/^[:]/)) { |
|
state.soyState.pop(); |
|
return null; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "map-value": |
|
if (stream.peek() == ")" || stream.peek() == "," || stream.match(/^[:)]/)) { |
|
state.soyState.pop(); |
|
return null; |
|
} |
|
return expression(stream, state); |
|
|
|
case "import": |
|
if (stream.eat(";")) { |
|
state.soyState.pop(); |
|
state.indent -= 2 * config.indentUnit; |
|
return null; |
|
} |
|
if (stream.match(/\w+(?=\s+as)/)) { |
|
return "variable"; |
|
} |
|
if (match = stream.match(/\w+/)) { |
|
return /(from|as)/.test(match[0]) ? "keyword" : "def"; |
|
} |
|
if (match = stream.match(/^["']/)) { |
|
state.soyState.push("string"); |
|
state.quoteKind = match[0]; |
|
return "string"; |
|
} |
|
stream.next(); |
|
return null; |
|
|
|
case "tag": |
|
var endTag; |
|
var tagName; |
|
if (state.tag === undefined) { |
|
endTag = true; |
|
tagName = ''; |
|
} else { |
|
endTag = state.tag[0] == "/"; |
|
tagName = endTag ? state.tag.substring(1) : state.tag; |
|
} |
|
var tag = tags[tagName]; |
|
if (stream.match(/^\/?}/)) { |
|
var selfClosed = stream.current() == "/}"; |
|
if (selfClosed && !endTag) { |
|
popcontext(state); |
|
} |
|
if (state.tag == "/template" || state.tag == "/deltemplate") { |
|
state.variables = prepend(null, 'ij'); |
|
state.indent = 0; |
|
} else { |
|
state.indent -= config.indentUnit * |
|
(selfClosed || indentingTags.indexOf(state.tag) == -1 ? 2 : 1); |
|
} |
|
state.soyState.pop(); |
|
return "keyword"; |
|
} else if (stream.match(/^([\w?]+)(?==)/)) { |
|
if (state.context && state.context.tag == tagName && stream.current() == "kind" && (match = stream.match(/^="([^"]+)/, false))) { |
|
var kind = match[1]; |
|
state.context.kind = kind; |
|
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"; |
|
} |
|
return expression(stream, state); |
|
|
|
case "template-call-expression": |
|
if (stream.match(/^([\w-?]+)(?==)/)) { |
|
return "attribute"; |
|
} else if (stream.eat('>')) { |
|
state.soyState.pop(); |
|
return "keyword"; |
|
} else if (stream.eat('/>')) { |
|
state.soyState.pop(); |
|
return "keyword"; |
|
} |
|
return expression(stream, state); |
|
case "literal": |
|
if (stream.match('{/literal}', false)) { |
|
state.soyState.pop(); |
|
return this.token(stream, state); |
|
} |
|
return tokenUntil(stream, state, /\{\/literal}/); |
|
case "export": |
|
if (match = stream.match(/\w+/)) { |
|
state.soyState.pop(); |
|
if (match == "const") { |
|
state.soyState.push("const-def") |
|
return "keyword"; |
|
} else if (match == "extern") { |
|
state.soyState.push("param-def") |
|
return "keyword"; |
|
} |
|
} else { |
|
stream.next(); |
|
} |
|
return null; |
|
case "const-def": |
|
if (stream.match(/^\w+/)) { |
|
state.soyState.pop(); |
|
return "def"; |
|
} |
|
stream.next(); |
|
return null; |
|
} |
|
|
|
if (stream.match('{literal}')) { |
|
state.indent += config.indentUnit; |
|
state.soyState.push("literal"); |
|
state.context = new Context(state.context, "literal", state.variables); |
|
return "keyword"; |
|
|
|
// A tag-keyword must be followed by whitespace, comment or a closing tag. |
|
} else if (match = stream.match(/^\{([/@\\]?\w+\??)(?=$|[\s}]|\/[/*])/)) { |
|
var prevTag = state.tag; |
|
state.tag = match[1]; |
|
var endTag = state.tag[0] == "/"; |
|
var indentingTag = !!tags[state.tag]; |
|
var tagName = endTag ? state.tag.substring(1) : state.tag; |
|
var tag = tags[tagName]; |
|
if (state.tag != "/switch") |
|
state.indent += ((endTag || tag && tag.reduceIndent) && prevTag != "switch" ? 1 : 2) * config.indentUnit; |
|
|
|
state.soyState.push("tag"); |
|
var tagError = false; |
|
if (tag) { |
|
if (!endTag) { |
|
if (tag.soyState) state.soyState.push(tag.soyState); |
|
} |
|
// If a new tag, open a new context. |
|
if (!tag.noEndTag && (indentingTag || !endTag)) { |
|
state.context = new Context(state.context, state.tag, tag.variableScope ? state.variables : null); |
|
// Otherwise close the current context. |
|
} else if (endTag) { |
|
var isBalancedForExtern = tagName == 'extern' && (state.context && state.context.tag == 'export'); |
|
if (!state.context || ((state.context.tag != tagName) && !isBalancedForExtern)) { |
|
tagError = true; |
|
} else if (state.context) { |
|
if (state.context.kind) { |
|
state.localStates.pop(); |
|
var localState = last(state.localStates); |
|
if (localState.mode.indent) { |
|
state.indent -= localState.mode.indent(localState.state, "", ""); |
|
} |
|
} |
|
popcontext(state); |
|
} |
|
} |
|
} else if (endTag) { |
|
// Assume all tags with a closing tag are defined in the config. |
|
tagError = true; |
|
} |
|
return (tagError ? "error " : "") + "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"; |
|
} else if (!state.context && stream.match(/\bimport\b/)) { |
|
state.soyState.push("import"); |
|
state.indent += 2 * config.indentUnit; |
|
return "keyword"; |
|
} else if (match = stream.match('<{')) { |
|
state.soyState.push("template-call-expression"); |
|
state.indent += 2 * config.indentUnit; |
|
state.soyState.push("tag"); |
|
return "keyword"; |
|
} else if (match = stream.match('</>')) { |
|
state.indent -= 1 * config.indentUnit; |
|
return "keyword"; |
|
} |
|
|
|
return tokenUntil(stream, state, /\{|\s+\/\/|\/\*/); |
|
}, |
|
|
|
indent: function(state, textAfter, line) { |
|
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, line); |
|
} |
|
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("wordChars", "soy", /[\w$]/); |
|
|
|
CodeMirror.registerHelper("hintWords", "soy", Object.keys(tags).concat( |
|
["css", "debugger"])); |
|
|
|
CodeMirror.defineMIME("text/x-soy", "soy"); |
|
});
|
|
|