From fb6a08d5de3d11933c7d5884a7d5760646bdd249 Mon Sep 17 00:00:00 2001
From: "N. Taylor Mullen"
Date: Mon, 14 Sep 2015 20:46:25 -0700
Subject: [PATCH] Allow `TagHelper`s inside of text/html typed script tags.
- To limit the impact of the change ensured that we only do extra work in the case that we detect a script tag with a `type` attribute.
- The parsing changes include normal HTML parsing behaviors when we detect that a script tag has a `type` attribute with value `text/html`.
- Added unit and code generation tests to validate `text/html` script tag behavior.
#502
---
.../Parser/HtmlMarkupParser.Block.cs | 14 +-
.../Parser/HtmlMarkupParser.Document.cs | 61 +++-
.../CSharpTagHelperRenderingTest.cs | 55 +++-
.../TagHelperParseTreeRewriterTest.cs | 267 ++++++++++++++++++
.../NestedScriptTagTagHelpers.DesignTime.cs | 71 +++++
...ptTagTagHelpers.DesignTime.lineMappings.cs | 49 ++++
.../Output/NestedScriptTagTagHelpers.cs | 114 ++++++++
.../Source/NestedScriptTagTagHelpers.cshtml | 16 ++
8 files changed, 641 insertions(+), 6 deletions(-)
create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.DesignTime.cs
create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.DesignTime.lineMappings.cs
create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.cs
create mode 100644 test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Source/NestedScriptTagTagHelpers.cshtml
diff --git a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs
index 09e7f06b40..31a778fd5e 100644
--- a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs
+++ b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs
@@ -5,8 +5,8 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
-using Microsoft.AspNet.Razor.Editor;
using Microsoft.AspNet.Razor.Chunks.Generators;
+using Microsoft.AspNet.Razor.Editor;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Text;
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
@@ -937,9 +937,17 @@ namespace Microsoft.AspNet.Razor.Parser
}
else if (string.Equals(tagName, ScriptTagName, StringComparison.OrdinalIgnoreCase))
{
- CompleteTagBlockWithSpan(tagBlockWrapper, AcceptedCharacters.None, SpanKind.Markup);
+ if (!CurrentScriptTagExpectsHtml())
+ {
+ CompleteTagBlockWithSpan(tagBlockWrapper, AcceptedCharacters.None, SpanKind.Markup);
- SkipToEndScriptAndParseCode(endTagAcceptedCharacters: AcceptedCharacters.None);
+ SkipToEndScriptAndParseCode(endTagAcceptedCharacters: AcceptedCharacters.None);
+ }
+ else
+ {
+ // Push the script tag onto the tag stack, it should be treated like all other HTML tags.
+ tags.Push(tag);
+ }
}
else
{
diff --git a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Document.cs b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Document.cs
index 88647b0087..8173ce1b12 100644
--- a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Document.cs
+++ b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Document.cs
@@ -2,6 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Diagnostics;
+using System.Linq;
+using Microsoft.AspNet.Razor.Chunks.Generators;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
@@ -9,6 +12,9 @@ namespace Microsoft.AspNet.Razor.Parser
{
public partial class HtmlMarkupParser
{
+ private static readonly char[] ValidAfterTypeAttributeNameCharacters =
+ new[] { ' ', '\t', '\r', '\n', '\f', '=' };
+
public override void ParseDocument()
{
if (Context == null)
@@ -50,7 +56,7 @@ namespace Microsoft.AspNet.Razor.Parser
return;
}
- // We should behave like a normal tag that has a parser escape, fall through to the normal
+ // We should behave like a normal tag that has a parser escape, fall through to the normal
// tag logic.
}
else if (NextIs(HtmlSymbolType.QuestionMark))
@@ -79,7 +85,9 @@ namespace Microsoft.AspNet.Razor.Parser
Optional(HtmlSymbolType.ForwardSlash);
Optional(HtmlSymbolType.CloseAngle);
- if (scriptTag)
+ // If the script tag expects javascript content then we should do minimal parsing until we reach
+ // the end script tag. Don't want to incorrectly parse a "var tag = '';" as an HTML tag.
+ if (scriptTag && !CurrentScriptTagExpectsHtml())
{
Output(SpanKind.Markup);
tagBlock.Dispose();
@@ -107,5 +115,54 @@ namespace Microsoft.AspNet.Razor.Parser
tagBlock.Dispose();
}
}
+
+ private bool CurrentScriptTagExpectsHtml()
+ {
+ var blockBuilder = Context.CurrentBlock;
+
+ Debug.Assert(blockBuilder != null);
+
+ var typeAttribute = blockBuilder.Children
+ .OfType()
+ .Where(block =>
+ block.ChunkGenerator is AttributeBlockChunkGenerator &&
+ block.Children.Count() >= 2)
+ .FirstOrDefault(IsTypeAttribute);
+
+ if (typeAttribute != null)
+ {
+ var contentValues = typeAttribute.Children
+ .OfType()
+ .Where(childSpan => childSpan.ChunkGenerator is LiteralAttributeChunkGenerator)
+ .Select(childSpan => childSpan.Content);
+
+ var scriptType = string.Concat(contentValues).Trim();
+
+ // Does not allow charset parameter (or any other parameters).
+ return string.Equals(scriptType, "text/html", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ }
+
+ private static bool IsTypeAttribute(Block block)
+ {
+ var span = block.Children.First() as Span;
+
+ if (span == null)
+ {
+ return false;
+ }
+
+ var trimmedStartContent = span.Content.TrimStart();
+ if (trimmedStartContent.StartsWith("type", StringComparison.OrdinalIgnoreCase) &&
+ (trimmedStartContent.Length == 4 ||
+ ValidAfterTypeAttributeNameCharacters.Contains(trimmedStartContent[4])))
+ {
+ return true;
+ }
+
+ return false;
+ }
}
}
diff --git a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
index 90f4b3a627..42739d0399 100644
--- a/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs
@@ -1459,6 +1459,58 @@ namespace Microsoft.AspNet.Razor.Test.Generator
contentLength: 1),
}
},
+ {
+ "NestedScriptTagTagHelpers",
+ "NestedScriptTagTagHelpers.DesignTime",
+ DefaultPAndInputTagHelperDescriptors,
+ new[]
+ {
+ BuildLineMapping(
+ documentAbsoluteIndex: 14,
+ documentLineIndex: 0,
+ generatedAbsoluteIndex: 495,
+ generatedLineIndex: 15,
+ characterOffsetIndex: 14,
+ contentLength: 17),
+ BuildLineMapping(
+ documentAbsoluteIndex: 182,
+ documentLineIndex: 5,
+ generatedAbsoluteIndex: 1033,
+ generatedLineIndex: 35,
+ characterOffsetIndex: 0,
+ contentLength: 12),
+ BuildLineMapping(
+ documentAbsoluteIndex: 195,
+ documentLineIndex: 5,
+ documentCharacterOffsetIndex: 13,
+ generatedAbsoluteIndex: 1136,
+ generatedLineIndex: 41,
+ generatedCharacterOffsetIndex: 12,
+ contentLength: 30),
+ BuildLineMapping(
+ documentAbsoluteIndex: 339,
+ documentLineIndex: 7,
+ documentCharacterOffsetIndex: 50,
+ generatedAbsoluteIndex: 1385,
+ generatedLineIndex: 49,
+ generatedCharacterOffsetIndex: 6,
+ contentLength: 23),
+ BuildLineMapping(
+ documentAbsoluteIndex: 389,
+ documentLineIndex: 7,
+ generatedAbsoluteIndex: 1692,
+ generatedLineIndex: 56,
+ characterOffsetIndex: 100,
+ contentLength: 4),
+ BuildLineMapping(
+ documentAbsoluteIndex: 424,
+ documentLineIndex: 9,
+ generatedAbsoluteIndex: 1775,
+ generatedLineIndex: 61,
+ characterOffsetIndex: 0,
+ contentLength: 15),
+ }
+ },
};
}
}
@@ -1505,7 +1557,8 @@ namespace Microsoft.AspNet.Razor.Test.Generator
},
{ "DuplicateAttributeTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
{ "DynamicAttributeTagHelpers", null, DynamicAttributeTagHelpers_Descriptors },
- { "TransitionsInTagHelperAttributes", null, DefaultPAndInputTagHelperDescriptors }
+ { "TransitionsInTagHelperAttributes", null, DefaultPAndInputTagHelperDescriptors },
+ { "NestedScriptTagTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
};
}
}
diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
index 931f129c83..43b90ab7a8 100644
--- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
+++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs
@@ -18,6 +18,273 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers
{
public class TagHelperParseTreeRewriterTest : TagHelperRewritingTestBase
{
+ public static TheoryData InvalidHtmlScriptBlockData
+ {
+ get
+ {
+ var factory = CreateDefaultSpanFactory();
+ var blockFactory = new BlockFactory(factory);
+
+ return new TheoryData
+ {
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup(""))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup(""))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup(""))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup(""))
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(InvalidHtmlScriptBlockData))]
+ public void TagHelperParseTreeRewriter_DoesNotUnderstandTagHelpersInInvalidHtmlTypedScriptTags(
+ string documentContent,
+ MarkupBlock expectedOutput)
+ {
+ RunParseTreeRewriterTest(documentContent, expectedOutput, "input");
+ }
+
+ public static TheoryData HtmlScriptBlockData
+ {
+ get
+ {
+ var factory = CreateDefaultSpanFactory();
+ var blockFactory = new BlockFactory(factory);
+
+ return new TheoryData
+ {
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup(""))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup(""))
+ },
+ {
+ "
",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup("")),
+ blockFactory.MarkupTagBlock(""))
+ },
+ {
+ "",
+ new MarkupBlock(
+ new MarkupTagBlock(
+ factory.Markup("")),
+ blockFactory.MarkupTagBlock(""))
+ },
+ };
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(HtmlScriptBlockData))]
+ public void TagHelperParseTreeRewriter_UnderstandsTagHelpersInHtmlTypedScriptTags(
+ string documentContent,
+ MarkupBlock expectedOutput)
+ {
+ RunParseTreeRewriterTest(documentContent, expectedOutput, "p", "input");
+ }
+
[Fact]
public void Rewrite_CanHandleInvalidChildrenWithWhitespace()
{
diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.DesignTime.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.DesignTime.cs
new file mode 100644
index 0000000000..de703c0dd1
--- /dev/null
+++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.DesignTime.cs
@@ -0,0 +1,71 @@
+namespace TestOutput
+{
+ using Microsoft.AspNet.Razor.Runtime.TagHelpers;
+ using System;
+ using System.Threading.Tasks;
+
+ public class NestedScriptTagTagHelpers
+ {
+ private static object @__o;
+ private void @__RazorDesignTimeHelpers__()
+ {
+ #pragma warning disable 219
+ string __tagHelperDirectiveSyntaxHelper = null;
+ __tagHelperDirectiveSyntaxHelper =
+#line 1 "NestedScriptTagTagHelpers.cshtml"
+ "something, nice"
+
+#line default
+#line hidden
+ ;
+ #pragma warning restore 219
+ }
+ #line hidden
+ private PTagHelper __PTagHelper = null;
+ private InputTagHelper __InputTagHelper = null;
+ private InputTagHelper2 __InputTagHelper2 = null;
+ #line hidden
+ public NestedScriptTagTagHelpers()
+ {
+ }
+
+ #pragma warning disable 1998
+ public override async Task ExecuteAsync()
+ {
+#line 6 "NestedScriptTagTagHelpers.cshtml"
+
+
+#line default
+#line hidden
+
+#line 6 "NestedScriptTagTagHelpers.cshtml"
+ for(var i = 0; i < 5; i++) {
+
+#line default
+#line hidden
+
+ __InputTagHelper = CreateTagHelper();
+ __InputTagHelper2 = CreateTagHelper();
+#line 8 "NestedScriptTagTagHelpers.cshtml"
+__o = ViewBag.DefaultInterval;
+
+#line default
+#line hidden
+ __InputTagHelper.Type = "text";
+ __InputTagHelper2.Type = __InputTagHelper.Type;
+#line 8 "NestedScriptTagTagHelpers.cshtml"
+ __InputTagHelper2.Checked = true;
+
+#line default
+#line hidden
+#line 10 "NestedScriptTagTagHelpers.cshtml"
+ }
+
+#line default
+#line hidden
+
+ __PTagHelper = CreateTagHelper();
+ }
+ #pragma warning restore 1998
+ }
+}
diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.DesignTime.lineMappings.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.DesignTime.lineMappings.cs
new file mode 100644
index 0000000000..2d848edd17
--- /dev/null
+++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.DesignTime.lineMappings.cs
@@ -0,0 +1,49 @@
+// !!! Do not check in. Instead paste content into test method. !!!
+
+ var expectedLineMappings = new[]
+ {
+ BuildLineMapping(
+ documentAbsoluteIndex: 14,
+ documentLineIndex: 0,
+ generatedAbsoluteIndex: 495,
+ generatedLineIndex: 15,
+ characterOffsetIndex: 14,
+ contentLength: 17),
+ BuildLineMapping(
+ documentAbsoluteIndex: 182,
+ documentLineIndex: 5,
+ generatedAbsoluteIndex: 1033,
+ generatedLineIndex: 35,
+ characterOffsetIndex: 0,
+ contentLength: 12),
+ BuildLineMapping(
+ documentAbsoluteIndex: 195,
+ documentLineIndex: 5,
+ documentCharacterOffsetIndex: 13,
+ generatedAbsoluteIndex: 1136,
+ generatedLineIndex: 41,
+ generatedCharacterOffsetIndex: 12,
+ contentLength: 30),
+ BuildLineMapping(
+ documentAbsoluteIndex: 339,
+ documentLineIndex: 7,
+ documentCharacterOffsetIndex: 50,
+ generatedAbsoluteIndex: 1385,
+ generatedLineIndex: 49,
+ generatedCharacterOffsetIndex: 6,
+ contentLength: 23),
+ BuildLineMapping(
+ documentAbsoluteIndex: 389,
+ documentLineIndex: 7,
+ generatedAbsoluteIndex: 1692,
+ generatedLineIndex: 56,
+ characterOffsetIndex: 100,
+ contentLength: 4),
+ BuildLineMapping(
+ documentAbsoluteIndex: 424,
+ documentLineIndex: 9,
+ generatedAbsoluteIndex: 1775,
+ generatedLineIndex: 61,
+ characterOffsetIndex: 0,
+ contentLength: 15),
+ };
diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.cs b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.cs
new file mode 100644
index 0000000000..235f76c9d5
--- /dev/null
+++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Output/NestedScriptTagTagHelpers.cs
@@ -0,0 +1,114 @@
+#pragma checksum "NestedScriptTagTagHelpers.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "9e6bc8d09df124eda650118b208b7c5e6e058f6b"
+namespace TestOutput
+{
+ using Microsoft.AspNet.Razor.Runtime.TagHelpers;
+ using System;
+ using System.Threading.Tasks;
+
+ public class NestedScriptTagTagHelpers
+ {
+ #line hidden
+ #pragma warning disable 0414
+ private TagHelperContent __tagHelperStringValueBuffer = null;
+ #pragma warning restore 0414
+ private TagHelperExecutionContext __tagHelperExecutionContext = null;
+ private TagHelperRunner __tagHelperRunner = null;
+ private TagHelperScopeManager __tagHelperScopeManager = new TagHelperScopeManager();
+ private PTagHelper __PTagHelper = null;
+ private InputTagHelper __InputTagHelper = null;
+ private InputTagHelper2 __InputTagHelper2 = null;
+ #line hidden
+ public NestedScriptTagTagHelpers()
+ {
+ }
+
+ #pragma warning disable 1998
+ public override async Task ExecuteAsync()
+ {
+ __tagHelperRunner = __tagHelperRunner ?? new TagHelperRunner();
+ Instrumentation.BeginContext(33, 106, true);
+ WriteLiteral("\r\n\r\n");
+ Instrumentation.EndContext();
+#line 10 "NestedScriptTagTagHelpers.cshtml"
+ }
+
+#line default
+#line hidden
+
+ Instrumentation.BeginContext(439, 129, true);
+ WriteLiteral(" \r\n ");
+ Instrumentation.EndContext();
+ }
+ , StartTagHelperWritingScope, EndTagHelperWritingScope);
+ __PTagHelper = CreateTagHelper();
+ __tagHelperExecutionContext.Add(__PTagHelper);
+ __tagHelperExecutionContext.AddHtmlAttribute("class", Html.Raw("Hello World"));
+ __tagHelperExecutionContext.AddHtmlAttribute("data-delay", Html.Raw("1000"));
+ __tagHelperExecutionContext.Output = await __tagHelperRunner.RunAsync(__tagHelperExecutionContext);
+ Instrumentation.BeginContext(139, 433, false);
+ await WriteTagHelperAsync(__tagHelperExecutionContext);
+ Instrumentation.EndContext();
+ __tagHelperExecutionContext = __tagHelperScopeManager.End();
+ Instrumentation.BeginContext(572, 23, true);
+ WriteLiteral("\r\n \r\n");
+ Instrumentation.EndContext();
+ }
+ #pragma warning restore 1998
+ }
+}
diff --git a/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Source/NestedScriptTagTagHelpers.cshtml b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Source/NestedScriptTagTagHelpers.cshtml
new file mode 100644
index 0000000000..27a29b23b4
--- /dev/null
+++ b/test/Microsoft.AspNet.Razor.Test/TestFiles/CodeGenerator/Source/NestedScriptTagTagHelpers.cshtml
@@ -0,0 +1,16 @@
+@addTagHelper "something, nice"
+
+
+ }
+
+
+
+
\ No newline at end of file