From beb975f7e76c734044fa35a2c9a64b97f9eb5c94 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Radek=20Pi=C3=B3rkowski?= <radek25c@gmail.com>
Date: Tue, 15 Apr 2014 21:13:31 +0200
Subject: [PATCH] [php mode] Add support for interpolated variables in
 double-quoted strings.

---
 mode/php/index.html |   6 +-
 mode/php/php.js     |  97 ++++++++++++++++++++++++++++-
 mode/php/test.js    | 145 ++++++++++++++++++++++++++++++++++++++++++++
 test/index.html     |   3 +
 4 files changed, 249 insertions(+), 2 deletions(-)
 create mode 100644 mode/php/test.js

diff --git a/mode/php/index.html b/mode/php/index.html
index 1fb7435bb..dd25a5e78 100644
--- a/mode/php/index.html
+++ b/mode/php/index.html
@@ -32,8 +32,12 @@
 <h2>PHP mode</h2>
 <form><textarea id="code" name="code">
 <?php
+$a = array('a' => 1, 'b' => 2, 3 => 'c');
+
+echo "$a[a] ${a[3] /* } comment */} {$a[b]} \$a[a]";
+
 function hello($who) {
-	return "Hello " . $who;
+	return "Hello $who!";
 }
 ?>
 <p>The program says <?= hello("World") ?>.</p>
diff --git a/mode/php/php.js b/mode/php/php.js
index bcec3ebb2..184293b62 100644
--- a/mode/php/php.js
+++ b/mode/php/php.js
@@ -21,6 +21,82 @@
     };
   }
 
+  // Two helper functions for encapsList
+  function matchFirst(list) {
+    return function (stream) {
+      for (var i = 0; i < list.length; ++i)
+        if (stream.match(list[i][0]))
+          return list[i][1];
+      return false;
+    };
+  }
+  function matchSequence(list) {
+    if (list.length == 0) return encapsList;
+    return function (stream, state) {
+      var result = list[0](stream, state);
+      if (result !== false) {
+        state.tokenize = matchSequence(list.slice(1));
+        return result;
+      }
+      else {
+        state.tokenize = encapsList;
+        return "string";
+      }
+    };
+  }
+  function encapsList(stream, state) {
+    var escaped = false, next, end = false;
+
+    if (stream.current() == '"') return "string";
+
+    // "Complex" syntax
+    if (stream.match("${", false) || stream.match("{$", false)) {
+      state.tokenize = null;
+      return "string";
+    }
+
+    // Simple syntax
+    if (stream.match(/\$[a-zA-Z_][a-zA-Z0-9_]*/)) {
+      // After the variable name there may appear array or object operator.
+      if (stream.match("[", false)) {
+        // Match array operator
+        state.tokenize = matchSequence([
+          matchFirst([["[", null]]),
+          matchFirst([
+            [/\d[\w\.]*/, "number"],
+            [/\$[a-zA-Z_][a-zA-Z0-9_]*/, "variable-2"],
+            [/[\w\$]+/, "variable"]
+          ]),
+          matchFirst([["]", null]])
+        ]);
+      }
+      if (stream.match(/\-\>\w/, false)) {
+        // Match object operator
+        state.tokenize = matchSequence([
+          matchFirst([["->", null]]),
+          matchFirst([[/[\w]+/, "variable"]])
+        ]);
+      }
+      return "variable-2";
+    }
+
+    // Normal string
+    while (
+      !stream.eol() &&
+      (!stream.match("{$", false)) &&
+      (!stream.match(/(\$[a-zA-Z_][a-zA-Z0-9_]*|\$\{)/, false) || escaped)
+    ) {
+      next = stream.next();
+      if (!escaped && next == '"') { end = true; break; }
+      escaped = !escaped && next == "\\";
+    }
+    if (end) {
+      state.tokenize = null;
+      state.phpEncapsStack.pop();
+    }
+    return "string";
+  }
+
   var phpKeywords = "abstract and array as break case catch class clone const continue declare default " +
     "do else elseif enddeclare endfor endforeach endif endswitch endwhile extends final " +
     "for foreach function global goto if implements interface instanceof namespace " +
@@ -62,6 +138,24 @@
           return "comment";
         }
         return false;
+      },
+      '"': function(stream, state) {
+        if (!state.phpEncapsStack)
+          state.phpEncapsStack = [];
+        state.phpEncapsStack.push(0);
+        state.tokenize = encapsList;
+        return state.tokenize(stream, state);
+      },
+      "{": function(_stream, state) {
+        if (state.phpEncapsStack && state.phpEncapsStack.length > 0)
+          state.phpEncapsStack[state.phpEncapsStack.length - 1]++;
+        return false;
+      },
+      "}": function(_stream, state) {
+        if (state.phpEncapsStack && state.phpEncapsStack.length > 0)
+          if (--state.phpEncapsStack[state.phpEncapsStack.length - 1] == 0)
+            state.tokenize = encapsList;
+        return false;
       }
     }
   };
@@ -101,7 +195,8 @@
         state.curState = state.html;
         return "meta";
       } else {
-        return phpMode.token(stream, state.curState);
+        var result = phpMode.token(stream, state.curState);
+        return (stream.pos <= stream.start) ? phpMode.token(stream, state.curState) : result;
       }
     }
 
diff --git a/mode/php/test.js b/mode/php/test.js
new file mode 100644
index 000000000..db68d75de
--- /dev/null
+++ b/mode/php/test.js
@@ -0,0 +1,145 @@
+(function() {
+  var mode = CodeMirror.getMode({indentUnit: 2}, "php");
+  function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
+
+  MT('simple_test',
+     '[meta <?php] ' +
+     '[keyword echo] [string "aaa"]; ' +
+     '[meta ?>]');
+
+  MT('variable_interpolation_non_alphanumeric',
+     '[meta <?php]',
+     '[keyword echo] [string "aaa$~$!$@$#$$$%$^$&$*$($)$.$<$>$/$\\$}$\\\"$:$;$?$|$[[$]]$+$=aaa"]',
+     '[meta ?>]');
+
+  MT('variable_interpolation_digits',
+     '[meta <?php]',
+     '[keyword echo] [string "aaa$1$2$3$4$5$6$7$8$9$0aaa"]',
+     '[meta ?>]');
+
+  MT('variable_interpolation_simple_syntax_1',
+     '[meta <?php]',
+     '[keyword echo] [string "aaa][variable-2 $aaa][string .aaa"];',
+     '[meta ?>]');
+
+  MT('variable_interpolation_simple_syntax_2',
+     '[meta <?php]',
+     '[keyword echo] [string "][variable-2 $aaaa][[','[number 2]',         ']][string aa"];',
+     '[keyword echo] [string "][variable-2 $aaaa][[','[number 2345]',      ']][string aa"];',
+     '[keyword echo] [string "][variable-2 $aaaa][[','[number 2.3]',       ']][string aa"];',
+     '[keyword echo] [string "][variable-2 $aaaa][[','[variable aaaaa]',   ']][string aa"];',
+     '[keyword echo] [string "][variable-2 $aaaa][[','[variable-2 $aaaaa]',']][string aa"];',
+
+     '[keyword echo] [string "1aaa][variable-2 $aaaa][[','[number 2]',         ']][string aa"];',
+     '[keyword echo] [string "aaa][variable-2 $aaaa][[','[number 2345]',      ']][string aa"];',
+     '[keyword echo] [string "aaa][variable-2 $aaaa][[','[number 2.3]',       ']][string aa"];',
+     '[keyword echo] [string "aaa][variable-2 $aaaa][[','[variable aaaaa]',   ']][string aa"];',
+     '[keyword echo] [string "aaa][variable-2 $aaaa][[','[variable-2 $aaaaa]',']][string aa"];',
+     '[meta ?>]');
+
+  MT('variable_interpolation_simple_syntax_3',
+     '[meta <?php]',
+     '[keyword echo] [string "aaa][variable-2 $aaaa]->[variable aaaaa][string .aaaaaa"];',
+     '[keyword echo] [string "aaa][variable-2 $aaaa][string ->][variable-2 $aaaaa][string .aaaaaa"];',
+     '[keyword echo] [string "aaa][variable-2 $aaaa]->[variable aaaaa][string [[2]].aaaaaa"];',
+     '[keyword echo] [string "aaa][variable-2 $aaaa]->[variable aaaaa][string ->aaaa2.aaaaaa"];',
+     '[meta ?>]');
+
+  MT('variable_interpolation_escaping',
+     '[meta <?php] [comment /* Escaping */]',
+     '[keyword echo] [string "aaa\\$aaaa->aaa.aaa"];',
+     '[keyword echo] [string "aaa\\$aaaa[[2]]aaa.aaa"];',
+     '[keyword echo] [string "aaa\\$aaaa[[asd]]aaa.aaa"];',
+     '[keyword echo] [string "aaa{\\$aaaa->aaa.aaa"];',
+     '[keyword echo] [string "aaa{\\$aaaa[[2]]aaa.aaa"];',
+     '[keyword echo] [string "aaa{\\aaaaa[[asd]]aaa.aaa"];',
+     '[keyword echo] [string "aaa\\${aaaa->aaa.aaa"];',
+     '[keyword echo] [string "aaa\\${aaaa[[2]]aaa.aaa"];',
+     '[keyword echo] [string "aaa\\${aaaa[[asd]]aaa.aaa"];',
+     '[meta ?>]');
+
+  MT('variable_interpolation_complex_syntax_1',
+     '[meta <?php]',
+     '[keyword echo] [string "aaa][variable-2 $]{[variable aaaa]}[string ->aaa.aaa"];',
+     '[keyword echo] [string "aaa][variable-2 $]{[variable-2 $aaaa]}[string ->aaa.aaa"];',
+     '[keyword echo] [string "aaa][variable-2 $]{[variable-2 $aaaa][[','  [number 42]',']]}[string ->aaa.aaa"];',
+     '[keyword echo] [string "aaa][variable-2 $]{[variable aaaa][meta ?>]aaaaaa');
+
+  MT('variable_interpolation_complex_syntax_2',
+     '[meta <?php] [comment /* Monsters */]',
+     '[keyword echo] [string "][variable-2 $]{[variable aaa][comment /*}?>} $aaa<?php } */]}[string ->aaa.aaa"];',
+     '[keyword echo] [string "][variable-2 $]{[variable aaa][comment /*}?>*/][[','  [string "aaa][variable-2 $aaa][string {}][variable-2 $]{[variable aaa]}[string "]',']]}[string ->aaa.aaa"];',
+     '[keyword echo] [string "][variable-2 $]{[variable aaa][comment /*} } $aaa } */]}[string ->aaa.aaa"];');
+
+
+  function build_recursive_monsters(nt, t, n){
+    var monsters = [t];
+    for (var i = 1; i <= n; ++i)
+      monsters[i] = nt.join(monsters[i - 1]);
+    return monsters;
+  }
+
+  var m1 = build_recursive_monsters(
+    ['[string "][variable-2 $]{[variable aaa] [operator +] ', '}[string "]'],
+    '[comment /* }?>} */] [string "aaa][variable-2 $aaa][string .aaa"]',
+    10
+  );
+
+  MT('variable_interpolation_complex_syntax_3_1',
+     '[meta <?php] [comment /* Recursive monsters */]',
+     '[keyword echo] ' + m1[4] + ';',
+     '[keyword echo] ' + m1[7] + ';',
+     '[keyword echo] ' + m1[8] + ';',
+     '[keyword echo] ' + m1[5] + ';',
+     '[keyword echo] ' + m1[1] + ';',
+     '[keyword echo] ' + m1[6] + ';',
+     '[keyword echo] ' + m1[9] + ';',
+     '[keyword echo] ' + m1[0] + ';',
+     '[keyword echo] ' + m1[10] + ';',
+     '[keyword echo] ' + m1[2] + ';',
+     '[keyword echo] ' + m1[3] + ';',
+     '[keyword echo] [string "end"];',
+     '[meta ?>]');
+
+  var m2 = build_recursive_monsters(
+    ['[string "a][variable-2 $]{[variable aaa] [operator +] ', ' [operator +] ', '}[string .a"]'],
+    '[comment /* }?>{{ */] [string "a?>}{{aa][variable-2 $aaa][string .a}a?>a"]',
+    5
+  );
+
+  MT('variable_interpolation_complex_syntax_3_2',
+     '[meta <?php] [comment /* Recursive monsters 2 */]',
+     '[keyword echo] ' + m2[0] + ';',
+     '[keyword echo] ' + m2[1] + ';',
+     '[keyword echo] ' + m2[5] + ';',
+     '[keyword echo] ' + m2[4] + ';',
+     '[keyword echo] ' + m2[2] + ';',
+     '[keyword echo] ' + m2[3] + ';',
+     '[keyword echo] [string "end"];',
+     '[meta ?>]');
+
+  function build_recursive_monsters_2(mf1, mf2, nt, t, n){
+    var monsters = [t];
+    for (var i = 1; i <= n; ++i)
+      monsters[i] = nt[0] + mf1[i - 1] + nt[1] + mf2[i - 1] + nt[2] + monsters[i - 1] + nt[3];
+    return monsters;
+  }
+
+  var m3 = build_recursive_monsters_2(
+    m1,
+    m2,
+    ['[string "a][variable-2 $]{[variable aaa] [operator +] ', ' [operator +] ', ' [operator +] ', '}[string .a"]'],
+    '[comment /* }?>{{ */] [string "a?>}{{aa][variable-2 $aaa][string .a}a?>a"]',
+    4
+  );
+
+  MT('variable_interpolation_complex_syntax_3_3',
+     '[meta <?php] [comment /* Recursive monsters 2 */]',
+     '[keyword echo] ' + m3[4] + ';',
+     '[keyword echo] ' + m3[0] + ';',
+     '[keyword echo] ' + m3[3] + ';',
+     '[keyword echo] ' + m3[1] + ';',
+     '[keyword echo] ' + m3[2] + ';',
+     '[keyword echo] [string "end"];',
+     '[meta ?>]');
+})();
diff --git a/test/index.html b/test/index.html
index 76fd835f2..752971fe4 100644
--- a/test/index.html
+++ b/test/index.html
@@ -15,6 +15,8 @@
 <script src="../addon/edit/matchbrackets.js"></script>
 <script src="../addon/comment/comment.js"></script>
 <script src="../mode/javascript/javascript.js"></script>
+<script src="../mode/clike/clike.js"></script>
+<script src="../mode/php/php.js"></script>
 <script src="../mode/xml/xml.js"></script>
 <script src="../keymap/vim.js"></script>
 <script src="../keymap/emacs.js"></script>
@@ -78,6 +80,7 @@
     <script src="search_test.js"></script>
     <script src="mode_test.js"></script>
     <script src="../mode/javascript/test.js"></script>
+    <script src="../mode/php/test.js"></script>
     <script src="../mode/css/css.js"></script>
     <script src="../mode/css/test.js"></script>
     <script src="../mode/css/scss_test.js"></script>
-- 
GitLab