Commit 5d7223c996f114bc4631d86430a4132b344b2503

Golmote 2018-03-03T21:39:51

PHP: Add support for Heredoc and Nowdoc strings

diff --git a/components/prism-php.js b/components/prism-php.js
index 79fe976..2c194f4 100644
--- a/components/prism-php.js
+++ b/components/prism-php.js
@@ -10,7 +10,7 @@
  * Adds the following new token classes:
  * 		constant, delimiter, variable, function, package
  */
-
+(function (Prism) {
 Prism.languages.php = Prism.languages.extend('clike', {
 	'keyword': /\b(?:and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|private|protected|parent|throw|null|echo|print|trait|namespace|final|yield|goto|instanceof|finally|try|catch)\b/i,
 	'constant': /\b[A-Z0-9_]{2,}\b/,
@@ -54,6 +54,35 @@ Prism.languages.insertBefore('php', 'operator', {
 });
 
 Prism.languages.insertBefore('php', 'string', {
+	'nowdoc-string': {
+		pattern: /<<<'([^']+)'(?:\r\n?|\n)(?:.*(?:\r\n?|\n))*?\1;/,
+		greedy: true,
+		alias: 'string',
+		inside: {
+			'delimiter': {
+				pattern: /^<<<'[^']+'|[a-z_]\w*;$/i,
+				alias: 'symbol',
+				inside: {
+					'punctuation': /^<<<'?|[';]$/
+				}
+			}
+		}
+	},
+	'heredoc-string': {
+		pattern: /<<<(?:"([^"]+)"(?:\r\n?|\n)(?:.*(?:\r\n?|\n))*?\1;|([a-z_]\w*)(?:\r\n?|\n)(?:.*(?:\r\n?|\n))*?\2;)/i,
+		greedy: true,
+		alias: 'string',
+		inside: {
+			'delimiter': {
+				pattern: /^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,
+				alias: 'symbol',
+				inside: {
+					'punctuation': /^<<<"?|[";]$/
+				}
+			},
+			'interpolation': null // See below
+		}
+	},
 	'single-quoted-string': {
 		pattern: /'(?:\\[\s\S]|[^\\'])*'/,
 		greedy: true,
@@ -64,18 +93,23 @@ Prism.languages.insertBefore('php', 'string', {
 		greedy: true,
 		alias: 'string',
 		inside: {
-			interpolation: {
-				pattern: /{\$(?:{(?:{[^{}]+}|[^{}]+)}|[^{}])+}|(^|[^\\{])\$+(?:\w+(?:\[.+?]|->\w+)*)/,
-				lookbehind: true,
-				inside: {
-					rest: Prism.languages.php
-				}
-			}
+			'interpolation': null // See below
 		}
 	}
 });
+// The different types of PHP strings "replace" the C-like standard string
 delete Prism.languages.php['string'];
 
+var string_interpolation = {
+	pattern: /{\$(?:{(?:{[^{}]+}|[^{}]+)}|[^{}])+}|(^|[^\\{])\$+(?:\w+(?:\[.+?]|->\w+)*)/,
+	lookbehind: true,
+	inside: {
+		rest: Prism.languages.php
+	}
+};
+Prism.languages.php['heredoc-string'].inside['interpolation'] = string_interpolation;
+Prism.languages.php['double-quoted-string'].inside['interpolation'] = string_interpolation;
+
 // Add HTML support if the markup language exists
 if (Prism.languages.markup) {
 
@@ -136,3 +170,4 @@ if (Prism.languages.markup) {
 		env.element.innerHTML = env.highlightedCode;
 	});
 }
+}(Prism));
\ No newline at end of file
diff --git a/components/prism-php.min.js b/components/prism-php.min.js
index 13e2cf4..57c347b 100644
--- a/components/prism-php.min.js
+++ b/components/prism-php.min.js
@@ -1 +1 @@
-Prism.languages.php=Prism.languages.extend("clike",{keyword:/\b(?:and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|private|protected|parent|throw|null|echo|print|trait|namespace|final|yield|goto|instanceof|finally|try|catch)\b/i,constant:/\b[A-Z0-9_]{2,}\b/,comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0}}),Prism.languages.insertBefore("php","class-name",{"shell-comment":{pattern:/(^|[^\\])#.*/,lookbehind:!0,alias:"comment"}}),Prism.languages.insertBefore("php","keyword",{delimiter:{pattern:/\?>|<\?(?:php|=)?/i,alias:"important"},variable:/\$+(?:\w+\b|(?={))/i,"package":{pattern:/(\\|namespace\s+|use\s+)[\w\\]+/,lookbehind:!0,inside:{punctuation:/\\/}}}),Prism.languages.insertBefore("php","operator",{property:{pattern:/(->)[\w]+/,lookbehind:!0}}),Prism.languages.insertBefore("php","string",{"single-quoted-string":{pattern:/'(?:\\[\s\S]|[^\\'])*'/,greedy:!0,alias:"string"},"double-quoted-string":{pattern:/"(?:\\[\s\S]|[^\\"])*"/,greedy:!0,alias:"string",inside:{interpolation:{pattern:/{\$(?:{(?:{[^{}]+}|[^{}]+)}|[^{}])+}|(^|[^\\{])\$+(?:\w+(?:\[.+?]|->\w+)*)/,lookbehind:!0,inside:{rest:Prism.languages.php}}}}}),delete Prism.languages.php.string,Prism.languages.markup&&(Prism.hooks.add("before-highlight",function(e){"php"===e.language&&/(?:<\?php|<\?)/gi.test(e.code)&&(e.tokenStack=[],e.backupCode=e.code,e.code=e.code.replace(/(?:<\?php|<\?)[\s\S]*?(?:\?>|$)/gi,function(a){for(var n=e.tokenStack.length;-1!==e.backupCode.indexOf("___PHP"+n+"___");)++n;return e.tokenStack[n]=a,"___PHP"+n+"___"}),e.grammar=Prism.languages.markup)}),Prism.hooks.add("before-insert",function(e){"php"===e.language&&e.backupCode&&(e.code=e.backupCode,delete e.backupCode)}),Prism.hooks.add("after-highlight",function(e){if("php"===e.language&&e.tokenStack){e.grammar=Prism.languages.php;for(var a=0,n=Object.keys(e.tokenStack);a<n.length;++a){var t=n[a],i=e.tokenStack[t];e.highlightedCode=e.highlightedCode.replace("___PHP"+t+"___",'<span class="token php language-php">'+Prism.highlight(i,e.grammar,"php").replace(/\$/g,"$$$$")+"</span>")}e.element.innerHTML=e.highlightedCode}}));
\ No newline at end of file
+!function(e){e.languages.php=e.languages.extend("clike",{keyword:/\b(?:and|or|xor|array|as|break|case|cfunction|class|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|for|foreach|function|include|include_once|global|if|new|return|static|switch|use|require|require_once|var|while|abstract|interface|public|implements|private|protected|parent|throw|null|echo|print|trait|namespace|final|yield|goto|instanceof|finally|try|catch)\b/i,constant:/\b[A-Z0-9_]{2,}\b/,comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0}}),e.languages.insertBefore("php","class-name",{"shell-comment":{pattern:/(^|[^\\])#.*/,lookbehind:!0,alias:"comment"}}),e.languages.insertBefore("php","keyword",{delimiter:{pattern:/\?>|<\?(?:php|=)?/i,alias:"important"},variable:/\$+(?:\w+\b|(?={))/i,"package":{pattern:/(\\|namespace\s+|use\s+)[\w\\]+/,lookbehind:!0,inside:{punctuation:/\\/}}}),e.languages.insertBefore("php","operator",{property:{pattern:/(->)[\w]+/,lookbehind:!0}}),e.languages.insertBefore("php","string",{"nowdoc-string":{pattern:/<<<'([^']+)'(?:\r\n?|\n)(?:.*(?:\r\n?|\n))*?\1;/,greedy:!0,alias:"string",inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<'?|[';]$/}}}},"heredoc-string":{pattern:/<<<(?:"([^"]+)"(?:\r\n?|\n)(?:.*(?:\r\n?|\n))*?\1;|([a-z_]\w*)(?:\r\n?|\n)(?:.*(?:\r\n?|\n))*?\2;)/i,greedy:!0,alias:"string",inside:{delimiter:{pattern:/^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<"?|[";]$/}},interpolation:null}},"single-quoted-string":{pattern:/'(?:\\[\s\S]|[^\\'])*'/,greedy:!0,alias:"string"},"double-quoted-string":{pattern:/"(?:\\[\s\S]|[^\\"])*"/,greedy:!0,alias:"string",inside:{interpolation:null}}}),delete e.languages.php.string;var n={pattern:/{\$(?:{(?:{[^{}]+}|[^{}]+)}|[^{}])+}|(^|[^\\{])\$+(?:\w+(?:\[.+?]|->\w+)*)/,lookbehind:!0,inside:{rest:e.languages.php}};e.languages.php["heredoc-string"].inside.interpolation=n,e.languages.php["double-quoted-string"].inside.interpolation=n,e.languages.markup&&(e.hooks.add("before-highlight",function(n){"php"===n.language&&/(?:<\?php|<\?)/gi.test(n.code)&&(n.tokenStack=[],n.backupCode=n.code,n.code=n.code.replace(/(?:<\?php|<\?)[\s\S]*?(?:\?>|$)/gi,function(e){for(var a=n.tokenStack.length;-1!==n.backupCode.indexOf("___PHP"+a+"___");)++a;return n.tokenStack[a]=e,"___PHP"+a+"___"}),n.grammar=e.languages.markup)}),e.hooks.add("before-insert",function(e){"php"===e.language&&e.backupCode&&(e.code=e.backupCode,delete e.backupCode)}),e.hooks.add("after-highlight",function(n){if("php"===n.language&&n.tokenStack){n.grammar=e.languages.php;for(var a=0,t=Object.keys(n.tokenStack);a<t.length;++a){var i=t[a],r=n.tokenStack[i];n.highlightedCode=n.highlightedCode.replace("___PHP"+i+"___",'<span class="token php language-php">'+e.highlight(r,n.grammar,"php").replace(/\$/g,"$$$$")+"</span>")}n.element.innerHTML=n.highlightedCode}}))}(Prism);
\ No newline at end of file
diff --git a/examples/prism-php.html b/examples/prism-php.html
index 3938a71..5021807 100644
--- a/examples/prism-php.html
+++ b/examples/prism-php.html
@@ -10,7 +10,13 @@ comment */
 <h2>Strings</h2>
 <pre><code>'foo \'bar\' baz'
 "foo \"bar\" baz"
-"a string # containing an hash"</code></pre>
+"a string # containing an hash"
+$foo = &lt;&lt;&lt;FOO
+    Heredoc strings are supported too!
+FOO;
+$bar = &lt;&lt;&lt;'BAR'
+    And also Nowdoc strings
+BAR;</code></pre>
 
 <h2>Variables</h2>
 <pre><code>$some_var = 5;
@@ -55,7 +61,13 @@ function gen_one_to_three() {
 
 <h2>String interpolation</h2>
 <pre><code>$str = "This is $great!";
-$foobar = "{${$foo->bar()}}";</code></pre>
+$foobar = "Another example: {${$foo->bar()}}";
+$a = &lt;&lt;&lt;FOO
+    Hello $world!
+FOO;
+$b = &lt;&lt;&lt;"FOOBAR"
+    Interpolation inside Heredoc strings {$obj->values[3]->name}
+FOOBAR;</code></pre>
 
 <h2>Known failures</h2>
 <p>There are certain edge cases where Prism will fail.
diff --git a/tests/languages/php/string-interpolation_feature.test b/tests/languages/php/string-interpolation_feature.test
index 2e20b9f..b0d57f8 100644
--- a/tests/languages/php/string-interpolation_feature.test
+++ b/tests/languages/php/string-interpolation_feature.test
@@ -7,6 +7,12 @@
 "the return value of getName(): {${getName()}}"
 "the return value of \$object->getName(): {${$object->getName()}}"
 "{$foo->$bar}, {$foo->{$baz[1]}}"
+<<<FOO
+Heredoc strings $also->support {${$string->interpolation()}}
+FOO;
+<<<"FOO_BAR"
+	{${$name}}, but not {\${\$name}}
+FOO_BAR;
 
 ----------------------------------------------------
 
@@ -95,6 +101,37 @@
 			["punctuation", "}"]
 		]],
 		"\""
+	]],
+	["heredoc-string", [
+		["delimiter", [
+			["punctuation", "<<<"], "FOO"
+		]],
+		"\r\nHeredoc strings ",
+		["interpolation", [
+			["variable", "$also"], ["operator", "-"], ["operator", ">"], ["property", "support"]
+		]],
+		["interpolation", [
+			["punctuation", "{"], ["variable", "$"], ["punctuation", "{"],
+			["variable", "$string"], ["operator", "-"], ["operator", ">"], ["function", "interpolation"], ["punctuation", "("], ["punctuation", ")"],
+			["punctuation", "}"], ["punctuation", "}"]
+		]],
+		["delimiter", [
+			"FOO", ["punctuation", ";"]
+		]]
+	]],
+	["heredoc-string", [
+		["delimiter", [
+			["punctuation", "<<<\""], "FOO_BAR", ["punctuation", "\""]
+		]],
+		["interpolation", [
+			["punctuation", "{"], ["variable", "$"], ["punctuation", "{"],
+			["variable", "$name"],
+			["punctuation", "}"], ["punctuation", "}"]
+		]],
+		", but not {\\${\\$name}}\r\n",
+		["delimiter", [
+			"FOO_BAR", ["punctuation", ";"]
+		]]
 	]]
 ]
 
diff --git a/tests/languages/php/string_feature.test b/tests/languages/php/string_feature.test
index bbd94ea..cac7a94 100644
--- a/tests/languages/php/string_feature.test
+++ b/tests/languages/php/string_feature.test
@@ -1,3 +1,13 @@
+<<<FOO_BAR
+Heredoc string
+FOO_BAR;
+<<<"FOO"
+	some
+	content
+FOO;
+<<<'NOWDOC'
+This is a nowdoc string
+NOWDOC;
 "https://example.com"
 " /* not a comment */ "
 "multi-line
@@ -8,6 +18,33 @@ string'
 ----------------------------------------------------
 
 [
+	["heredoc-string", [
+		["delimiter", [
+			["punctuation", "<<<"], "FOO_BAR"
+		]],
+		"\r\nHeredoc string\r\n",
+		["delimiter", [
+			"FOO_BAR", ["punctuation", ";"]
+		]]
+	]],
+	["heredoc-string", [
+		["delimiter", [
+			["punctuation", "<<<\""], "FOO", ["punctuation", "\""]
+		]],
+		"\r\n\tsome\r\n\tcontent\r\n",
+		["delimiter", [
+			"FOO", ["punctuation", ";"]
+		]]
+	]],
+	["nowdoc-string", [
+		["delimiter", [
+			["punctuation", "<<<'"], "NOWDOC", ["punctuation", "'"]
+		]],
+		"\r\nThis is a nowdoc string\r\n",
+		["delimiter", [
+			"NOWDOC", ["punctuation", ";"]
+		]]
+	]],
 	["double-quoted-string", ["\"https://example.com\""]],
 	["double-quoted-string", ["\" /* not a comment */ \""]],
 	["double-quoted-string", ["\"multi-line\r\nstring\""]],