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