diff --git a/mode/smarty/index.html b/mode/smarty/index.html index 6b7debedc4be652867116f46998f0bd25098f096..9e41733807f99b0a91424dc616fe518d5b76d86d 100644 --- a/mode/smarty/index.html +++ b/mode/smarty/index.html @@ -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> diff --git a/mode/smarty/smarty.js b/mode/smarty/smarty.js index 7d7e62f86e4318a0646c242c7cdc09219fd13caf..00c1df5aa85bb5e2e7d2d1b5065f39f252d011da 100644 --- a/mode/smarty/smarty.js +++ b/mode/smarty/smarty.js @@ -1,140 +1,186 @@ +/** + * 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