Commit 31a38d0ca02e6399637a6c693f65f1b007c71c63

Michael Schmidt 2022-03-13T11:01:11

CSHTML: Added support for `@helper` and inline C# inside attribute values (#3355)

diff --git a/components/prism-cshtml.js b/components/prism-cshtml.js
index a917626..4643710 100644
--- a/components/prism-cshtml.js
+++ b/components/prism-cshtml.js
@@ -28,9 +28,15 @@
 	}
 
 	var round = nested(/\((?:[^()'"@/]|<str>|<comment>|<self>)*\)/.source, 2);
-	var square = nested(/\[(?:[^\[\]'"@/]|<str>|<comment>|<self>)*\]/.source, 2);
+	var square = nested(/\[(?:[^\[\]'"@/]|<str>|<comment>|<self>)*\]/.source, 1);
 	var curly = nested(/\{(?:[^{}'"@/]|<str>|<comment>|<self>)*\}/.source, 2);
-	var angle = nested(/<(?:[^<>'"@/]|<str>|<comment>|<self>)*>/.source, 2);
+	var angle = nested(/<(?:[^<>'"@/]|<comment>|<self>)*>/.source, 1);
+
+	var inlineCs = /@/.source +
+		/(?:await\b\s*)?/.source +
+		'(?:' + /(?!await\b)\w+\b/.source + '|' + round + ')' +
+		'(?:' + /[?!]?\.\w+\b/.source + '|' + '(?:' + angle + ')?' + round + '|' + square + ')*' +
+		/(?![?!\.(\[]|<(?!\/))/.source;
 
 	// Note about the above bracket patterns:
 	// They all ignore HTML expressions that might be in the C# code. This is a problem because HTML (like strings and
@@ -44,7 +50,14 @@
 	// To somewhat alleviate the problem a bit, the patterns for characters (e.g. 'a') is very permissive, it also
 	// allows invalid characters to support HTML expressions like this: <p>That's it!</p>.
 
-	var tagAttrs = /(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?/.source;
+	var tagAttrInlineCs = /@(?![\w()])/.source + '|' + inlineCs;
+	var tagAttrValue = '(?:' +
+		/"[^"@]*"|'[^'@]*'|[^\s'"@>=]+(?=[\s>])/.source +
+		'|' +
+		'["\'][^"\'@]*(?:(?:' + tagAttrInlineCs + ')[^"\'@]*)+["\']' +
+		')';
+
+	var tagAttrs = /(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*<tagAttrValue>|(?=[\s/>])))+)?/.source.replace(/<tagAttrValue>/, tagAttrValue);
 	var tagContent = /(?!\d)[^\s>\/=$<%]+/.source + tagAttrs + /\s*\/?>/.source;
 	var tagRegion =
 		/\B@?/.source +
@@ -110,6 +123,21 @@
 		inside: csharpWithHtml
 	};
 
+	var inlineValue = {
+		pattern: RegExp(/(^|[^@])/.source + inlineCs),
+		lookbehind: true,
+		greedy: true,
+		alias: 'variable',
+		inside: {
+			'keyword': /^@/,
+			'csharp': cs
+		}
+	};
+
+	Prism.languages.cshtml.tag.pattern = RegExp(/<\/?/.source + tagContent);
+	Prism.languages.cshtml.tag.inside['attr-value'].pattern = RegExp(/=\s*/.source + tagAttrValue);
+	Prism.languages.insertBefore('inside', 'punctuation', { 'value': inlineValue }, Prism.languages.cshtml.tag.inside['attr-value']);
+
 	Prism.languages.insertBefore('cshtml', 'prolog', {
 		'razor-comment': {
 			pattern: /@\*[\s\S]*?\*@/,
@@ -134,6 +162,8 @@
 					/try\s*/.source + curly + /\s*catch\s*/.source + round + /\s*/.source + curly + /\s*finally\s*/.source + curly,
 					// @if (...) {...} else if (...) {...} else {...}
 					/if\s*/.source + round + /\s*/.source + curly + '(?:' + /\s*else/.source + '(?:' + /\s+if\s*/.source + round + ')?' + /\s*/.source + curly + ')*',
+					// @helper Ident(params) { ... }
+					/helper\s+\w+\s*/.source + round + /\s*/.source + curly,
 				].join('|') +
 				')'
 			),
@@ -155,21 +185,7 @@
 			}
 		},
 
-		'value': {
-			pattern: RegExp(
-				/(^|[^@])@/.source +
-				/(?:await\b\s*)?/.source +
-				'(?:' + /\w+\b/.source + '|' + round + ')' +
-				'(?:' + /[?!]?\.\w+\b/.source + '|' + round + '|' + square + '|' + angle + round + ')*'
-			),
-			lookbehind: true,
-			greedy: true,
-			alias: 'variable',
-			inside: {
-				'keyword': /^@/,
-				'csharp': cs
-			}
-		},
+		'value': inlineValue,
 
 		'delegate-operator': {
 			pattern: /(^|[^@])@(?=<)/,
diff --git a/components/prism-cshtml.min.js b/components/prism-cshtml.min.js
index 537152a..c5c9684 100644
--- a/components/prism-cshtml.min.js
+++ b/components/prism-cshtml.min.js
@@ -1 +1 @@
-!function(e){function s(e,s){for(var a=0;a<s;a++)e=e.replace(/<self>/g,function(){return"(?:"+e+")"});return e.replace(/<self>/g,"[^\\s\\S]").replace(/<str>/g,'(?:@(?!")|"(?:[^\r\n\\\\"]|\\\\.)*"|@"(?:[^\\\\"]|""|\\\\[^])*"(?!")|'+"'(?:(?:[^\r\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'|(?=[^\\\\](?!'))))").replace(/<comment>/g,"(?:/(?![/*])|//.*[\r\n]|/\\*[^*]*(?:\\*(?!/)[^*]*)*\\*/)")}var a=s("\\((?:[^()'\"@/]|<str>|<comment>|<self>)*\\)",2),r=s("\\[(?:[^\\[\\]'\"@/]|<str>|<comment>|<self>)*\\]",2),t=s("\\{(?:[^{}'\"@/]|<str>|<comment>|<self>)*\\}",2),n=s("<(?:[^<>'\"@/]|<str>|<comment>|<self>)*>",2),l="(?:\\s(?:\\s*[^\\s>/=]+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))|(?=[\\s/>])))+)?",i="(?!\\d)[^\\s>/=$<%]+"+l+"\\s*/?>",o="\\B@?(?:<([a-zA-Z][\\w:]*)"+l+"\\s*>(?:[^<]|</?(?!\\1\\b)"+i+"|"+s("<\\1"+l+"\\s*>(?:[^<]|</?(?!\\1\\b)"+i+"|<self>)*</\\1\\s*>",2)+")*</\\1\\s*>|<"+i+")";e.languages.cshtml=e.languages.extend("markup",{});var g={pattern:/\S[\s\S]*/,alias:"language-csharp",inside:e.languages.insertBefore("csharp","string",{html:{pattern:RegExp(o),greedy:!0,inside:e.languages.cshtml}},{csharp:e.languages.extend("csharp",{})})};e.languages.insertBefore("cshtml","prolog",{"razor-comment":{pattern:/@\*[\s\S]*?\*@/,greedy:!0,alias:"comment"},block:{pattern:RegExp("(^|[^@])@(?:"+[t,"(?:code|functions)\\s*"+t,"(?:for|foreach|lock|switch|using|while)\\s*"+a+"\\s*"+t,"do\\s*"+t+"\\s*while\\s*"+a+"(?:\\s*;)?","try\\s*"+t+"\\s*catch\\s*"+a+"\\s*"+t+"\\s*finally\\s*"+t,"if\\s*"+a+"\\s*"+t+"(?:\\s*else(?:\\s+if\\s*"+a+")?\\s*"+t+")*"].join("|")+")"),lookbehind:!0,greedy:!0,inside:{keyword:/^@\w*/,csharp:g}},directive:{pattern:/^([ \t]*)@(?:addTagHelper|attribute|implements|inherits|inject|layout|model|namespace|page|preservewhitespace|removeTagHelper|section|tagHelperPrefix|using)(?=\s).*/m,lookbehind:!0,greedy:!0,inside:{keyword:/^@\w+/,csharp:g}},value:{pattern:RegExp("(^|[^@])@(?:await\\b\\s*)?(?:\\w+\\b|"+a+")(?:[?!]?\\.\\w+\\b|"+a+"|"+r+"|"+n+a+")*"),lookbehind:!0,greedy:!0,alias:"variable",inside:{keyword:/^@/,csharp:g}},"delegate-operator":{pattern:/(^|[^@])@(?=<)/,lookbehind:!0,alias:"operator"}}),e.languages.razor=e.languages.cshtml}(Prism);
\ No newline at end of file
+!function(e){function s(e,s){for(var a=0;a<s;a++)e=e.replace(/<self>/g,function(){return"(?:"+e+")"});return e.replace(/<self>/g,"[^\\s\\S]").replace(/<str>/g,'(?:@(?!")|"(?:[^\r\n\\\\"]|\\\\.)*"|@"(?:[^\\\\"]|""|\\\\[^])*"(?!")|'+"'(?:(?:[^\r\n'\\\\]|\\\\.|\\\\[Uux][\\da-fA-F]{1,8})'|(?=[^\\\\](?!'))))").replace(/<comment>/g,"(?:/(?![/*])|//.*[\r\n]|/\\*[^*]*(?:\\*(?!/)[^*]*)*\\*/)")}var a=s("\\((?:[^()'\"@/]|<str>|<comment>|<self>)*\\)",2),t=s("\\[(?:[^\\[\\]'\"@/]|<str>|<comment>|<self>)*\\]",1),r=s("\\{(?:[^{}'\"@/]|<str>|<comment>|<self>)*\\}",2),n="@(?:await\\b\\s*)?(?:(?!await\\b)\\w+\\b|"+a+")(?:[?!]?\\.\\w+\\b|(?:"+s("<(?:[^<>'\"@/]|<comment>|<self>)*>",1)+")?"+a+"|"+t+")*(?![?!\\.(\\[]|<(?!/))",l="(?:\"[^\"@]*\"|'[^'@]*'|[^\\s'\"@>=]+(?=[\\s>])|[\"'][^\"'@]*(?:(?:"+("@(?![\\w()])|"+n)+")[^\"'@]*)+[\"'])",i="(?:\\s(?:\\s*[^\\s>/=]+(?:\\s*=\\s*<tagAttrValue>|(?=[\\s/>])))+)?".replace(/<tagAttrValue>/,l),g="(?!\\d)[^\\s>/=$<%]+"+i+"\\s*/?>",o="\\B@?(?:<([a-zA-Z][\\w:]*)"+i+"\\s*>(?:[^<]|</?(?!\\1\\b)"+g+"|"+s("<\\1"+i+"\\s*>(?:[^<]|</?(?!\\1\\b)"+g+"|<self>)*</\\1\\s*>",2)+")*</\\1\\s*>|<"+g+")";e.languages.cshtml=e.languages.extend("markup",{});var c={pattern:/\S[\s\S]*/,alias:"language-csharp",inside:e.languages.insertBefore("csharp","string",{html:{pattern:RegExp(o),greedy:!0,inside:e.languages.cshtml}},{csharp:e.languages.extend("csharp",{})})},p={pattern:RegExp("(^|[^@])"+n),lookbehind:!0,greedy:!0,alias:"variable",inside:{keyword:/^@/,csharp:c}};e.languages.cshtml.tag.pattern=RegExp("</?"+g),e.languages.cshtml.tag.inside["attr-value"].pattern=RegExp("=\\s*"+l),e.languages.insertBefore("inside","punctuation",{value:p},e.languages.cshtml.tag.inside["attr-value"]),e.languages.insertBefore("cshtml","prolog",{"razor-comment":{pattern:/@\*[\s\S]*?\*@/,greedy:!0,alias:"comment"},block:{pattern:RegExp("(^|[^@])@(?:"+[r,"(?:code|functions)\\s*"+r,"(?:for|foreach|lock|switch|using|while)\\s*"+a+"\\s*"+r,"do\\s*"+r+"\\s*while\\s*"+a+"(?:\\s*;)?","try\\s*"+r+"\\s*catch\\s*"+a+"\\s*"+r+"\\s*finally\\s*"+r,"if\\s*"+a+"\\s*"+r+"(?:\\s*else(?:\\s+if\\s*"+a+")?\\s*"+r+")*","helper\\s+\\w+\\s*"+a+"\\s*"+r].join("|")+")"),lookbehind:!0,greedy:!0,inside:{keyword:/^@\w*/,csharp:c}},directive:{pattern:/^([ \t]*)@(?:addTagHelper|attribute|implements|inherits|inject|layout|model|namespace|page|preservewhitespace|removeTagHelper|section|tagHelperPrefix|using)(?=\s).*/m,lookbehind:!0,greedy:!0,inside:{keyword:/^@\w+/,csharp:c}},value:p,"delegate-operator":{pattern:/(^|[^@])@(?=<)/,lookbehind:!0,alias:"operator"}}),e.languages.razor=e.languages.cshtml}(Prism);
\ No newline at end of file
diff --git a/tests/languages/cshtml/block_feature.test b/tests/languages/cshtml/block_feature.test
index 59bd8b0..e3d1875 100644
--- a/tests/languages/cshtml/block_feature.test
+++ b/tests/languages/cshtml/block_feature.test
@@ -94,6 +94,10 @@ finally
     // Do critical section work
 }
 
+@helper TrialHelper(string name) {
+    // some code
+}
+
 ----------------------------------------------------
 
 [
@@ -895,5 +899,23 @@ finally
 			["comment", "// Do critical section work"],
 			["punctuation", "}"]
 		]]
+	]],
+
+	["block", [
+		["keyword", "@helper"],
+		["csharp", [
+			["function", "TrialHelper"],
+			["punctuation", "("],
+			["class-name", [
+				["keyword", "string"]
+			]],
+			" name",
+			["punctuation", ")"],
+			["punctuation", "{"],
+
+			["comment", "// some code"],
+
+			["punctuation", "}"]
+		]]
 	]]
 ]
diff --git a/tests/languages/cshtml/issue3354.test b/tests/languages/cshtml/issue3354.test
new file mode 100644
index 0000000..8b27f99
--- /dev/null
+++ b/tests/languages/cshtml/issue3354.test
@@ -0,0 +1,194 @@
+<input type="text" placeholder="@Localize.GetLabelHtml("PLACEHOLDER")"/>
+
+<h1>
+    @Localize.GetLabelHtml("TITLE")
+</h1>
+
+@{
+    var man = "Federico";
+    var text = string.Concat("Nice to meet you", " ", man);
+}
+
+@helper TrialHelper(string name) {
+    var text = string.Concat("Hello", " ", name);
+    <h1>
+        @(text + ", how's going?")
+    </h1>
+    <p>
+        Hello World!
+    </p>
+}
+
+----------------------------------------------------
+
+[
+	["tag", [
+		["tag", [
+			["punctuation", "<"],
+			"input"
+		]],
+		["attr-name", ["type"]],
+		["attr-value", [
+			["punctuation", "="],
+			["punctuation", "\""],
+			"text",
+			["punctuation", "\""]
+		]],
+		["attr-name", ["placeholder"]],
+		["attr-value", [
+			["punctuation", "="],
+			["punctuation", "\""],
+			["value", [
+				["keyword", "@"],
+				["csharp", [
+					"Localize",
+					["punctuation", "."],
+					["function", "GetLabelHtml"],
+					["punctuation", "("],
+					["string", "\"PLACEHOLDER\""],
+					["punctuation", ")"]
+				]]
+			]],
+			["punctuation", "\""]
+		]],
+		["punctuation", "/>"]
+	]],
+
+	["tag", [
+		["tag", [
+			["punctuation", "<"],
+			"h1"
+		]],
+		["punctuation", ">"]
+	]],
+	["value", [
+		["keyword", "@"],
+		["csharp", [
+			"Localize",
+			["punctuation", "."],
+			["function", "GetLabelHtml"],
+			["punctuation", "("],
+			["string", "\"TITLE\""],
+			["punctuation", ")"]
+		]]
+	]],
+	["tag", [
+		["tag", [
+			["punctuation", "</"],
+			"h1"
+		]],
+		["punctuation", ">"]
+	]],
+
+	["block", [
+		["keyword", "@"],
+		["csharp", [
+			["punctuation", "{"],
+
+			["class-name", [
+				["keyword", "var"]
+			]],
+			" man ",
+			["operator", "="],
+			["string", "\"Federico\""],
+			["punctuation", ";"],
+
+			["class-name", [
+				["keyword", "var"]
+			]],
+			" text ",
+			["operator", "="],
+			["keyword", "string"],
+			["punctuation", "."],
+			["function", "Concat"],
+			["punctuation", "("],
+			["string", "\"Nice to meet you\""],
+			["punctuation", ","],
+			["string", "\" \""],
+			["punctuation", ","],
+			" man",
+			["punctuation", ")"],
+			["punctuation", ";"],
+
+			["punctuation", "}"]
+		]]
+	]],
+
+	["block", [
+		["keyword", "@helper"],
+		["csharp", [
+			["function", "TrialHelper"],
+			["punctuation", "("],
+			["class-name", [
+				["keyword", "string"]
+			]],
+			" name",
+			["punctuation", ")"],
+			["punctuation", "{"],
+
+			["class-name", [
+				["keyword", "var"]
+			]],
+			" text ",
+			["operator", "="],
+			["keyword", "string"],
+			["punctuation", "."],
+			["function", "Concat"],
+			["punctuation", "("],
+			["string", "\"Hello\""],
+			["punctuation", ","],
+			["string", "\" \""],
+			["punctuation", ","],
+			" name",
+			["punctuation", ")"],
+			["punctuation", ";"],
+
+			["html", [
+				["tag", [
+					["tag", [
+						["punctuation", "<"],
+						"h1"
+					]],
+					["punctuation", ">"]
+				]],
+				["value", [
+					["keyword", "@"],
+					["csharp", [
+						["punctuation", "("],
+						"text ",
+						["operator", "+"],
+						["string", "\", how's going?\""],
+						["punctuation", ")"]
+					]]
+				]],
+				["tag", [
+					["tag", [
+						["punctuation", "</"],
+						"h1"
+					]],
+					["punctuation", ">"]
+				]]
+			]],
+
+			["html", [
+				["tag", [
+					["tag", [
+						["punctuation", "<"],
+						"p"
+					]],
+					["punctuation", ">"]
+				]],
+				"\r\n        Hello World!\r\n    ",
+				["tag", [
+					["tag", [
+						["punctuation", "</"],
+						"p"
+					]],
+					["punctuation", ">"]
+				]]
+			]],
+
+			["punctuation", "}"]
+		]]
+	]]
+]