Skip to content
Snippets Groups Projects
Commit 65c74d59 authored by Ben Keen's avatar Ben Keen Committed by Marijn Haverbeke
Browse files

[smarty mode] Rewrite, Smarty 3 support added

Issue #956
parent d07c5471
No related branches found
No related tags found
No related merge requests found
......@@ -12,6 +12,7 @@
<body>
<h1>CodeMirror: Smarty mode</h1>
<h3>Default settings (Smarty 2, <b>{</b> and <b>}</b> delimiters)</h3>
<form><textarea id="code" name="code">
{extends file="parent.tpl"}
{include file="template.tpl"}
......@@ -43,6 +44,7 @@
<br />
<h3>Smarty 2, custom delimiters</h3>
<form><textarea id="code2" name="code2">
{--extends file="parent.tpl"--}
{--include file="template.tpl"--}
......@@ -76,7 +78,48 @@
});
</script>
<p>A plain text/Smarty mode which allows for custom delimiter tags (defaults to <b>{</b> and <b>}</b>).</p>
<br />
<h3>Smarty 3</h3>
<textarea id="code3" name="code3">
Nested tags {$foo={counter one=1 two={inception}}+3} are now valid in Smarty 3.
<script>
function test() {
console.log("Smarty 3 permits single curly braces followed by whitespace to NOT slip into Smarty mode.");
}
</script>
{assign var=foo value=[1,2,3]}
{assign var=foo value=['y'=>'yellow','b'=>'blue']}
{assign var=foo value=[1,[9,8],3]}
{$foo=$bar+2} {* a comment *}
{$foo.bar=1} {* another comment *}
{$foo = myfunct(($x+$y)*3)}
{$foo = strlen($bar)}
{$foo.bar.baz=1}, {$foo[]=1}
Smarty "dot" syntax (note: embedded {} are used to address ambiguities):
{$foo.a.b.c} => $foo['a']['b']['c']
{$foo.a.$b.c} => $foo['a'][$b]['c']
{$foo.a.{$b+4}.c} => $foo['a'][$b+4]['c']
{$foo.a.{$b.c}} => $foo['a'][$b['c']]
{$object->method1($x)->method2($y)}</textarea>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code3"), {
lineNumbers: true,
mode: "smarty",
smartyVersion: 3
});
</script>
<p>A plain text/Smarty version 2 or 3 mode, which allows for custom delimiter tags.</p>
<p><strong>MIME types defined:</strong> <code>text/x-smarty</code></p>
</body>
......
/**
* Smarty 2 and 3 mode.
*/
CodeMirror.defineMode("smarty", function(config) {
var keyFuncs = ["debug", "extends", "function", "include", "literal"];
"use strict";
// our default settings; check to see if they're overridden
var settings = {
rightDelimiter: '}',
leftDelimiter: '{',
smartyVersion: 2 // for backward compatibility
};
if (config.hasOwnProperty("leftDelimiter")) {
settings.leftDelimiter = config.leftDelimiter;
}
if (config.hasOwnProperty("rightDelimiter")) {
settings.rightDelimiter = config.rightDelimiter;
}
if (config.hasOwnProperty("smartyVersion") && config.smartyVersion === 3) {
settings.smartyVersion = 3;
}
var keyFunctions = ["debug", "extends", "function", "include", "literal"];
var last;
var regs = {
operatorChars: /[+\-*&%=<>!?]/,
validIdentifier: /[a-zA-Z0-9\_]/,
stringChar: /[\'\"]/
validIdentifier: /[a-zA-Z0-9_]/,
stringChar: /['"]/
};
var leftDelim = (typeof config.mode.leftDelimiter != 'undefined') ? config.mode.leftDelimiter : "{";
var rightDelim = (typeof config.mode.rightDelimiter != 'undefined') ? config.mode.rightDelimiter : "}";
function ret(style, lst) { last = lst; return style; }
function tokenizer(stream, state) {
function chain(parser) {
var helpers = {
continue: function(style, lastType) {
last = lastType;
return style;
},
chain: function(stream, state, parser) {
state.tokenize = parser;
return parser(stream, state);
}
};
if (stream.match(leftDelim, true)) {
if (stream.eat("*")) {
return chain(inBlock("comment", "*" + rightDelim));
}
else {
state.tokenize = inSmarty;
return "tag";
}
}
else {
// I'd like to do an eatWhile() here, but I can't get it to eat only up to the rightDelim string/char
stream.next();
return null;
}
}
function inSmarty(stream, state) {
if (stream.match(rightDelim, true)) {
state.tokenize = tokenizer;
return ret("tag", null);
}
// our various parsers
var parsers = {
var ch = stream.next();
if (ch == "$") {
stream.eatWhile(regs.validIdentifier);
return ret("variable-2", "variable");
}
else if (ch == ".") {
return ret("operator", "property");
}
else if (regs.stringChar.test(ch)) {
state.tokenize = inAttribute(ch);
return ret("string", "string");
}
else if (regs.operatorChars.test(ch)) {
stream.eatWhile(regs.operatorChars);
return ret("operator", "operator");
}
else if (ch == "[" || ch == "]") {
return ret("bracket", "bracket");
}
else if (/\d/.test(ch)) {
stream.eatWhile(/\d/);
return ret("number", "number");
}
else {
if (state.last == "variable") {
if (ch == "@") {
stream.eatWhile(regs.validIdentifier);
return ret("property", "property");
// the main tokenizer
tokenizer: function(stream, state) {
if (stream.match(settings.leftDelimiter, true)) {
if (stream.eat("*")) {
return helpers.chain(stream, state, parsers.inBlock("comment", "*" + settings.rightDelimiter));
} else {
// Smarty 3 allows { and } surrounded by whitespace to NOT slip into Smarty mode
state.depth++;
var isEol = stream.eol();
var isFollowedByWhitespace = /\s/.test(stream.peek());
if (settings.smartyVersion === 3 && settings.leftDelimiter === "{" && (isEol || isFollowedByWhitespace)) {
state.depth--;
return null;
} else {
state.tokenize = parsers.smarty;
last = "startTag";
return "tag";
}
}
else if (ch == "|") {
stream.eatWhile(regs.validIdentifier);
return ret("qualifier", "modifier");
}
}
else if (state.last == "whitespace") {
stream.eatWhile(regs.validIdentifier);
return ret("attribute", "modifier");
}
else if (state.last == "property") {
stream.eatWhile(regs.validIdentifier);
return ret("property", null);
}
else if (/\s/.test(ch)) {
last = "whitespace";
} else {
stream.next();
return null;
}
},
var str = "";
if (ch != "/") {
str += ch;
}
var c = "";
while ((c = stream.eat(regs.validIdentifier))) {
str += c;
}
var i, j;
for (i=0, j=keyFuncs.length; i<j; i++) {
if (keyFuncs[i] == str) {
return ret("keyword", "keyword");
// parsing Smarty content
smarty: function(stream, state) {
if (stream.match(settings.rightDelimiter, true)) {
if (settings.smartyVersion === 3) {
state.depth--;
if (state.depth <= 0) {
state.tokenize = parsers.tokenizer;
}
} else {
state.tokenize = parsers.tokenizer;
}
return helpers.continue("tag", null);
}
if (/\s/.test(ch)) {
return null;
if (stream.match(settings.leftDelimiter, true)) {
state.depth++;
return helpers.continue("tag", "startTag");
}
return ret("tag", "tag");
}
}
function inAttribute(quote) {
return function(stream, state) {
while (!stream.eol()) {
if (stream.next() == quote) {
state.tokenize = inSmarty;
break;
var ch = stream.next();
if (ch == "$") {
stream.eatWhile(regs.validIdentifier);
return helpers.continue("variable-2", "variable");
} else if (ch == ".") {
return helpers.continue("operator", "property");
} else if (regs.stringChar.test(ch)) {
state.tokenize = parsers.inAttribute(ch);
return helpers.continue("string", "string");
} else if (regs.operatorChars.test(ch)) {
stream.eatWhile(regs.operatorChars);
return helpers.continue("operator", "operator");
} else if (ch == "[" || ch == "]") {
return helpers.continue("bracket", "bracket");
} else if (/\d/.test(ch)) {
stream.eatWhile(/\d/);
return helpers.continue("number", "number");
} else {
if (state.last == "variable") {
if (ch == "@") {
stream.eatWhile(regs.validIdentifier);
return helpers.continue("property", "property");
} else if (ch == "|") {
stream.eatWhile(regs.validIdentifier);
return helpers.continue("qualifier", "modifier");
}
} else if (state.last == "whitespace") {
stream.eatWhile(regs.validIdentifier);
return helpers.continue("attribute", "modifier");
} if (state.last == "property") {
stream.eatWhile(regs.validIdentifier);
return helpers.continue("property", null);
} else if (/\s/.test(ch)) {
last = "whitespace";
return null;
}
}
return "string";
};
}
function inBlock(style, terminator) {
return function(stream, state) {
while (!stream.eol()) {
if (stream.match(terminator)) {
state.tokenize = tokenizer;
break;
var str = "";
if (ch != "/") {
str += ch;
}
stream.next();
var c = null;
while (c = stream.eat(regs.validIdentifier)) {
str += c;
}
for (var i=0, j=keyFunctions.length; i<j; i++) {
if (keyFunctions[i] == str) {
return helpers.continue("keyword", "keyword");
}
}
if (/\s/.test(ch)) {
return null;
}
return helpers.continue("tag", "tag");
}
return style;
};
}
},
inAttribute: function(quote) {
return function(stream, state) {
while (!stream.eol()) {
if (stream.next() == quote) {
state.tokenize = parsers.smarty;
break;
}
}
return "string";
};
},
inBlock: function(style, terminator) {
return function(stream, state) {
while (!stream.eol()) {
if (stream.match(terminator)) {
state.tokenize = parsers.tokenizer;
break;
}
stream.next();
}
return style;
};
}
};
// the public API for CodeMirror
return {
startState: function() {
return { tokenize: tokenizer, mode: "smarty", last: null };
return {
tokenize: parsers.tokenizer,
mode: "smarty",
last: null,
depth: 0
};
},
token: function(stream, state) {
var style = state.tokenize(stream, state);
......@@ -145,4 +191,4 @@ CodeMirror.defineMode("smarty", function(config) {
};
});
CodeMirror.defineMIME("text/x-smarty", "smarty");
CodeMirror.defineMIME("text/x-smarty", "smarty");
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment